はじめに
VIPERアーキテクチャで新規開発する経験がいくつかあり、その経験を踏まえてVIPERアーキテクチャについて思ったことを書きます。
そのあとに、VIPERアーキテクチャでリファクタリングで工夫したこと、苦労したことをポイントとして挙げようと思います。
(アドベントカレンダー遅れてすみません。。あとでもう少し丁寧に書き直します。)
定義
まずはプロトコルの定義をしようと思います。サイトによって命名規則などが異なるので。。
EventHandler
- ViewからPresenterへのProtocol (ViewのEventを検知するという意味合いがある。)
UserInterface
- PresenterからViewへのProtocol (UIの操作をするという意味合いがある)
InteractorInput
- PresenterからInteractorへのProtocol (Interactorへの入力)
InteractorOutput
- InteractorからPresenterへのProtocol (Interactorからの出力)
RouterInput
- PresenterからRouterへのProtocol (Routerへの入力)
VIPERの個人的感想
いいところ
テスタビリティが非常に高い
全てprotocol経由でそれぞれのモジュールをつないでいるので、モックによる差し替えが容易になります。
設計が揃いやすい
かなり制約の強いアーキテクチャーパターンなので変な書き方をしたとしてもすぐに設計のおかしさに気づくことができます。そういう意味では設計もしやすさはもちろんのこと、コードスメルの検知のしやすさでもかなり優れていると感じます。
また、設計をするときにも「VIPERでいうところの〜」というような会話もできるので、認識が非常に合わせやすいです。
routerが意外に扱いやすい
VIPER特有のrouterが存在するおかげで、画面遷移の処理のコードがViewControllerから分離され、viewの作り込みと画面遷移分けて考えられるところがとても良いです。単一責任の法則が守られているということですかね。
残念なところ
ファイル数がとても多い
VIPERのデメリットとしてよく挙げられますが、テンプレートで何とかしましょう。。
ライブラリと相性がかなり悪い
alamofireのようなただ通信をするだけのライブラリであればinteractorにDIするだけで良いのですが、firebaseのようにUIを自動で立ち上げてくれるようなライブラリにとても弱いです。
モーダルとして出すpresentingViewControllerを指定するようなライブラリのメソッドを叩くときは透明なViewControllerを噛ませて誤魔化したりしなければVIPERとしての形態を維持するのがかなり難しいです。
Presenterがただの橋渡ししかしていないときはあまり効果がない
一つのテキストしかないような画面であっても基本的にはVIPERを維持しなければならず、一見すると何のために書いてるのかわからないようなテストがちらほら生まれることがあります。もちろんシンプルなロジックであろうが今後の拡張を見据える上では一つ一つが大切なテストですが、そう思ってしまうこと自体は避けようがないですね。
個人的には複雑な画面を構成するときはMVVMかなと思っていて、MVVMにするほど複雑ではないときにVIPERとして選択肢が上がってくるイメージです。
なので比較的単純かつ静的な画面を多く構成するアプリにはとてもちょうどいいといった所感もここから得られました。
Protocolとの相性が悪い
下記の記事にもある通り、associatedType
を使っていい感じなprotocolを作ろうとすると仕様上弾かれてしまいます。
これから説明するリファクタリング案が複雑になってしまうのもこのせいです。。
リファクタリング案
基本的にはprotocol oriented programming の発想で、必要なVIPERモジュールに対してprotocolを継承するだけでその拡張メソッドが実行できるようにするというものです。
ただ、この愚直に考えたアイデアには限界を感じています。この方法だとSwiftのプロトコルの仕様上ジェネリックプログラミングがかなり困難でキャストさせるためのプロパティがどうしても必要になるため少々残念です。また、前提としてVIPERはそれぞれのクラスが抽象に依存することができるように設計されていますが、この方法だとうまく作らなければ結合度が高くなってしまうのでProtocol Extensionの制約を工夫する必要があります。
サンプルソースのリポジトリはこちらです。
Interactorの処理の共通化
Interactorは2パターンあると思っています。一つはServiceのようにPresenterに対して複数DIしてくるパターンと、Presenterに対して1対1でDIするパターンです。Serviceのような使い方だと、必要なInteractorを差し込んでくる形になります。
もう一つは1対1でDIするパターンで、こちらはPresenterがシンプルになるのでわかりやすいです。また、Presenterに渡す前にある程度データ加工をして吸収したいケースなどもあり、そういう場合にInteractorはあくまでもビジネスロジックとの橋渡し役と考えると意外とすっきりしたりします。
1対1でDIするパターンを採用した場合は、Interactorに対してDIしてくるような形をとります。これで簡単に共通化できます。
また、グローバルなイベントを吐き出すようなDelegateを用意しておき、一部のinteractorから受け取るようにするという工夫も検討できます。
routerの共通化
通信エラー画面やダイアログなど、多くの画面から遷移される画面がある場合に、共通化できると便利です。
エラー画面に行くためのRouterInputをプロトコルとして用意しておき、適切にProtocol Extensionをすると、エラー画面に行きたいRouterInputがそのプロトコルを継承するだけで実装できます。必要ない場合はProtocolを継承しなければいいだけなので、かなりわかりやすいです。
また、プロトコルを継承しているためViewやInteractorからもそのプロトコルの所持の有無で処理を分けることができます。もし仮にRouterInputがエラー画面に行くことができるのであればエラー画面を表示するといったNull安全な書き方もできるため、Presenterに引っ掛けるプロトコルの共通化が期待できます。
今回は認可エラーを想定して、エラーのときにログイン画面を出現させるExtensionを実装してみました。
(サンプルアプリではスイッチをオフにするとエラー扱いになります。)
UserInterfaceの共通化
ナビゲーションバーの操作など、どの画面にも現れるようなUIについて共通化することができます。これは比較的簡単で、
protocol HogeUserInterface {
}
extension HogeUserInterface where Self: UIViewController {
}
上記のようにUIViewControllerを制約としてProtocol ExtensionすればViewやNavigationControllerなどをよしなにいじることができます。
viewからrouter/interactorを間接的に叩く
よくあるのがUIViewControllerのライフサイクル、ウェブビューや各種UIのデリゲートから取得するイベントに応じて何かをしたいというパターンです。
今回はLoggerとして、viewDidAppear
が呼ばれたら自動的に画面の名前を記録できるExtensionを実装してみました。UIViewControllerを継承した基底クラスとうまく連携させることで、EventHandlerを呼ぶタイミングも共通化することができます。
interactorからviewを間接的に叩く
よくあるのがAPIとの通信状態に応じてローディング画面を出させるパターンです。
リファクタリング案の感想について
少し時間がなく、急足でアイデアを具現化したのでまだまだ検証が足りていませんが、今の所の所感を述べておきます。
これらの方法を使うことで、各種のVIPERモジュールに共通した処理をまとめて書くことができ、なおかつPOP的な書き方をすることができるようになります。ですが、少々欠点があり、あまりお勧めできる方法ではありません。今後も新しいアイデアがあれば採用して検証を繰り返しできたらいいなあと思っています。
まずProtocol Extensionを使うと、依存関係が非常にわかりにくくなってしまいます。
EventHandlerなのに、その拡張がどうしてもRouterやInteractorのプロトコルに弱く依存せざるを得ず、もともと抽象に依存させる目的で使われているProtocolがだんだん結合度が高くなっていく恐れがあります。
もう少し設計を考えることで安全な作り方ができるのではないかと考えています。
また、associatedTypeやジェネリクスと相性が非常に悪く、共通化したとしてもやや冗長なことをしなければ実現できない点が多く、こちらについても改善の余地が十分にあると考えています。