Why VIPER is a bad choice for your next application

昨年、VIPERについて多くの宣伝があり、誰もがそれに触発されています。 これらの記事のほとんどは、それについて偏見を持っていて、それがいかにクールであるかを実証しようとしている。 そんなことはない。 他のアーキテクチャ・パターンと同じように、少なくともそれ以上の問題を抱えているのです。 この記事では、VIPER が宣伝されているほど優れておらず、ほとんどのアプリケーションに適していない理由を説明したいと思います。

アーキテクチャの比較に関する記事の中には、VIPER は MVC ベースのアーキテクチャとはまったく別物だと主張するものがあります。 この主張は正しくありません。VIPERは通常のMVCで、コントローラをインタラクタとプレゼンタの2つの部分に分割しています。 ビューはそのままですが、モデルはエンティティに改名されます。 ルーターは特別な言葉に値します。 たしかに、他のアーキテクチャでは、この部分を省略形として宣伝しませんが、暗黙のうちに (そう、pushViewController を呼び出すときは、単純なルーターを書いているのです)、またはより明示的に (FlowCoordinator など) 存在します。

では、VIPER があなたに提供するメリットについてお話したいと思います。 この本は、VIPERについての完全なリファレンスとして使用するつもりです。 まず、2つ目の目標であるSRP(単一責任原則)に適合することから始めましょう。 ちょっと乱暴な言い方だが、これをメリットと呼べる奇特な人がいるのだろうか。 あなたはタスクを解決することで報酬を得ているのであって、バズワードに準拠することで報酬を得ているわけではないのです。 そう、あなたはまだ TDD、BDD、ユニットテスト、Realm や SQLite、依存性注入など、覚えている限りのものを使っていますが、ただ使っているのではなく、顧客の問題を解決するためにそれを使っているのです。 テスト容易性は非常に重要な関心事です。 多くの人がそれについて話していますが、自分のアプリケーションを本当にテストしている人はごくわずかで、それを正しく行っている人はさらに少ないので、独立した記事に値します。 assert 2 + 2 == 4 でユニット テストを作成することについての記事はたくさんありますが、実際の例はありません (ちなみに、Artsy はアプリをオープン ソース化して素晴らしい仕事をしているので、彼らのプロジェクトを見てみるとよいでしょう)。 それは、テストを容易にするかもしれませんが、常にそうであるとは限りません。 そう、単純なクラスのユニットテストを書くのは簡単ですが、このテストのほとんどは何もテストしていません。 例えば、プレゼンターのメソッドを考えてみましょう。 このプロキシのためにテストを書けば、コードのテストカバレッジは向上しますが、 このテストは何の役にも立ちません。 また、副作用があります。メイン コードを編集するたびに、これらの役に立たないテストを更新する必要があります。

テストへの適切なアプローチは、インタラクタのテストとプレゼンターのテストを同時に行う必要があります。 さらに、2 つのクラスに分かれているため、1 つのクラスしかない場合と比較して、はるかに多くのテストが必要です。 クラス A には 4 つの可能な状態があり、クラス B には 6 つの可能な状態があるので、A と B の組み合わせには 20 の状態があり、すべてをテストする必要があります。

テストを単純化する正しいアプローチは、複雑な状態を単に多くのクラスに分割するのではなく、コードに純粋性を導入します。 ビューは、状態をプロパティのセットとして公開し、これらのプロパティから視覚的な外観を継承します。 そして、FBSnapshotTestCase を使用して、状態を外観と一致させることができます。 それでも、カスタム遷移のようないくつかのエッジケースは処理できませんが、どのくらいの頻度で実装するのでしょうか。

Overengered design

VIPER は、元企業 Java プログラマーが iOS 界に侵入するとこうなる。 -n0damage, reddit comment

正直なところ、誰かがこれを見て言えるだろうか。 「サーバーからの更新をトリガーするボタンと、サーバーの応答からのデータを含むビューがあります。 この変更によって、どれだけのクラス/プロトコルが影響を受けると思いますか。 そう、少なくとも3つのクラスと4つのプロトコルが、この単純な機能のために変更されることになります。 Springがどのように抽象化から始まり、AbstractSingletonProxyFactoryBeanで終わったか、誰も覚えていないのだろうか?

Redundant components

前にすでに述べたように、プレゼンターは通常、ビューからインタラクタの呼び出しをプロキシするだけで、非常に間抜けなクラスです (このようなものです)。

“DI-friendly” amount of protocols

この略称でよく混乱することがあります。 VIPER は SOLID 原則を実装しており、DI は「インジェクション」ではなく「依存関係の逆転」を意味します。 依存関係インジェクションは Inversion of Control パターンの特別なケースで、依存関係の逆転とは異なります。 たとえば、UI モジュールはネットワークまたは永続性モジュールに直接依存してはなりません。 Inversion of Control はそれとは異なり、あるモジュール (通常は変更できないライブラリのもの) が別のモジュールに何かを委任し、それが通常依存関係として最初のモジュールに提供される場合です。 そう、UITableViewにデータソースを実装するとき、IoCの原則を利用しているのだ。 同じ言語機能を異なる高レベルの目的で使用することは、ここでの混乱の元です。

VIPERに話を戻しましょう。 モジュール内の各クラス間には多くのプロトコル(最低5つ)が存在する。 しかもそれらは全く必要ない。 PresenterとInteractorは別のレイヤーのモジュールではありません。 IoCの原則を適用することは理にかなっていますが、自分に質問してみてください。1つのビューに対して少なくとも2つのプレゼンターを実装することはどれぐらいありますか? ほとんどの人がゼロと答えたと思います。

また、これらのプロトコルのために、IDE でコードを簡単にナビゲートすることはできません。

パフォーマンスの問題

これは非常に重要なことですが、多くの人がそれを気にしないか、あるいは、間違ったアーキテクチャの決定による影響を過小評価しています。 もちろん、特にオートインジェクションを使用する場合、パフォーマンスに影響がありますが、VIPER はそれを使用する必要はありません。 その代わり、ランタイムとアプリの起動について、また、VIPER が文字通りあらゆるところでアプリを遅くしていることについてお話したいと思います。 アプリの起動時間はほとんど議論されませんが、重要なトピックです。アプリの起動が非常に遅いと、ユーザーはそのアプリを使用しないようになります。 昨年のWWDCでは、アプリの起動時間の最適化に関するセッションがありました。 TL; DR: アプリの起動時間は、クラスの数に直接依存します。 100個のクラスがあっても、誰もそれに気づかないでしょう。 しかし、あなたのアプリが100クラスしかない場合、この複雑なアーキテクチャは本当に必要でしょうか? しかし、アプリが巨大な場合、例えば、Facebookアプリ(18kクラス)に取り組んでいる場合、この影響は非常に大きく、前述のセッションによると1秒程度になるそうです。 そうです、アプリを低温で起動すると、すべてのクラスのメタデータをロードするだけで、他には何もしないので、1 秒かかります。 これはより複雑で、プロファイル化するのがはるかに難しく、ほとんどの場合 Swift コンパイラーにのみ適用されます (ObjC には豊富なランタイム機能があり、コンパイラーはこれらの最適化を安全に実行できないため)。 あるメソッド(2番目の用語はSwiftでは必ずしも正しくないので、私は「メッセージを送信する」の代わりに「呼び出し」を使用します)を呼び出すときに、アンダーフードで何が起こっているかについて説明しましょう。 Swiftには3種類のディスパッチがあります(速いものから遅いものへ):静的、テーブルディスパッチ、そしてメッセージの送信です。 最後のものはObjCで使われている唯一のタイプで、ObjCのコードとの相互運用が必要なときや、メソッドをdynamicと宣言するときにSwiftで使われるものです。 もちろん、ランタイムのこの部分は大いに最適化されており、すべてのプラットフォーム用にアセンブラで書かれている。 しかし、コンパイラがコンパイル時に何が呼び出されるかの知識を持っているので、このオーバーヘッドを避けることができるとしたらどうでしょうか? それはまさに、Swiftコンパイラが静的ディスパッチとテーブルディスパッチで行っていることです。 静的ディスパッチは高速ですが、コンパイラは式中の型に100%の信頼がなければ使うことができません。 しかし、変数の型がプロトコルである場合、コンパイラはプロトコルウィットネステーブルを介してディスパッチを使用することを余儀なくされます。 文字通り遅いわけではありませんが、ここで1ms、あそこで1ms、合計の実行時間は純粋な Swift のコードを使用して達成できるよりも1秒以上長くなっています。 この段落は、プロトコルについての前の段落に関連していますが、私は、単に無謀な量の未使用プロトコルについての懸念と、それによるコンパイラの実際の混乱とを分ける方が良いと思います。

弱い抽象化の分離

それを行うための 1 つの – そしてできれば唯一の – 明らかな方法があるはずです。 一方では、物事を正しく行うための多くのルールがありますが、もう一方では、多くのものが意見に基づいています。 例えば、CoreDataをNSFetchedResultsControllerで処理したり、UIWebViewで処理したりといった複雑なケースもあります。 しかし、UIAlertControllerを使用するような一般的なケースでさえも、議論の対象となるのです。 ここでは、ルータでアラートを処理していますが、ビューでアラートを表示しています。

特別なケースは、ルールを破るほど特別ではありません。

ええ、でもなぜここでは、その種の警告を作成するファクトリがあるのでしょうか。 だから、UIAlertControllerのような単純なケースでも混乱するんだ。

Code generation

Readability counts.

これがどうしてアーキテクチャの問題なんだ? 自分で書かずにテンプレートクラスを大量に生成してるだけじゃん。 それのどこが問題なんだ? 問題は、ほとんどの場合 (使い捨てのアウトソーシング会社で働かない限り) あなたはコードを読んでいるのであって、書いているのではないことです。 つまり、あなたはほとんどの時間、実際のコードと混ざった定型的なコードを読んでいるわけです。 それは良いことでしょうか?

Conclusion

私は、VIPER の使用を思いとどまらせることを目的にはしていません。 まだ、すべての恩恵を受けることができるいくつかのアプリがあることができます。 しかし、アプリの開発を始める前に、いくつかの質問を自分に投げかける必要があります:

  1. このアプリは長い寿命を持ちそうですか? また、小さな変更であっても、延々と膨大なリファクタリングを行うことになります。
  2. 自分のアプリを本当にテストしていますか。 9890>

すべての質問に「はい」と答えた場合のみ、VIPERはあなたのアプリにとって良い選択かもしれません。

最後に、自分自身の頭を使って決断する必要があります。 「Xを使いましょう、Xはクールです。 この人たちも間違っている可能性があるのです。