MVVM、MVC、VIPERなど多様なアーキテクチャデザインがありますが、どれが一番良いのでしょうか。この講演では良いiOSアプリアーキテクチャを構成するためにどのような要素が必要なのかを見ていきます。
導入(0:00)
New York Timesで働いているKrzysztof Zabłockiです。オープンソースまたは私が作ったFoldifyアプリを通じて私の名前を聞いたことがある方がいるかもしれません。もしかしたらObjective-C Playgroundsプロジェクトで私の名前を見たことがあるかもしれません。AppleはObjective-C用のPlaygroundsは不可能だとしていましたが、私は、Objective-C Playgroundsプロジェクトでこれが可能だということを証明しました。
今日は良いアーキテクチャとは何かについて話したいと思います。これは非常に重要なテーマでありながら、多くの論争を引き起こす話題でもあります。良いアーキテクチャとは何で、どのように定義することができるでしょうか。このテーマはいつも加熱しやすくコントロールも難しいという点でタブとスペースに対する論争と類似した側面があります。
このテーマについての講演を準備をしていて、数多くのデザインパターンの中でどのような話をしなければならないか悩みました。SOLIDやUncle BobのThe Clean Architectureなどさまざまな概念がありますが、これらの情報は専門書籍で知ることができます。私はThe Gang of Foursの様に果敢な人ではないため、今回の講演でこのような概念については説明しない予定です。
代わりに、このテーマをiOS開発過程で経験しうるより実用的な側面から調査してみたいと思います。私はプロジェクト全体に対するコードレビューを担当するコンサルタント業務を遂行しつつ、アーキテクチャを向上できる方法と正しい開発方向、改善方法などについてプロジェクトチームに助言しています。
私が重要だと考えるものを中心に話す予定ですが、人によっては私の意見が偏りがあるように感じられるかもしれません。今回の講演ではAppleのサンプルコードがなぜ悪いのか、彼らはなぜそうするのかについてもみていきます。WWDC 2016でAppleはとても素敵なデザインパターンを二つ披露しましたが、このパターンを誤った形で使用しました。これについては後ほど見ていきます。私がよく使用するアーキテクチャであるMVVMと関連して、人々がMVVMを使用するときに頻繁に失敗する理由と改善方法について調べてみて、他のアーキテクチャについても見てみることにします。
良いアーキテクチャとは?(2:26)
良いアーキテクチャとは何でしょうか? 特定のアプリケーションのアーキテクチャで私がいつも希望するいくつかの特性があります。
それぞれのオブジェクトは 具体的で、明確な役割 を持っていなければいけません。それを容易に理解することができ、簡単に変更することができなければならず、ソースコードを読んだとき、これが一つの役割を実際に満たしているかどうかまたは作成しようとするロジックがそれを違反したかどうかが直ちに分かる仕組みでなければなりません。
また、 単純なデータフロー である必要があります。理解しやすく、クラッシュまたはエラーが発生した際に簡単にデバッグできなければいけません。いくつか他のオブジェクトの間を行ったり来たりしなければならない仕組みは避けたほうがいいでしょう。同じ共有データ(Shared Resource)を操作して使用する場合、不具合原因を探しにくいため、このような構造もよくありません。データは一方向の流れ(Unidirectional Data Flow)の形が一番良いです。特定の場所にブレークポイントを置いて確認できるためです。このような機能を支援するアーキテクチャが実在します。
アーキテクチャは特定のフレームワークサービスに従属してはいけません。なぜなら、プロジェクトで使用した特定のフレームワーク、またはサービスと作成したコードの間にかんたんな抽象化レイヤーがない場合、この様な従属性がどれほど大きな困難を招くのか、20年以上のプログラミング経験を通じてよく知っているためです。特定のフレームワーク、またはサービスが終了したり、同様の事態が発生するたびにこの事実に言及します。
アーキテクチャは容易に理解することができ、簡単に変えられる 柔軟さ を備えていなければなりません。ここでいう柔軟さは必要以上に複雑なのではなく、単純なものをいいます。200個の抽象クラスが存在してすべてが抽象化され、プロジェクトに参加する誰も理解できない構造であったり、新しい機能を一つ追加するにも関わらず、多くの苦労をしなければならない仕組みは私が考える柔軟さがありません。
テスト容易性(Testability)も重要な考慮事項です。テスト容易性はテスト自体だけを意味しません。この部分については後ほど見ていきます。
関心事の分離(Separation of Concern) も重要です。これは一つの責任だけを担当する非常に明確なデザインパターンの一つです。関心事の分離では明確な境界を定義します。オブジェクトは「ほとんどの場合」に一つの枠割のみを担当しなければなりません。「ほとんどの場合」と表現した理由は、時にはこのような規則を違反しなければならない状況も発生するためです。規則を破っても、これは依然として優れた選択です。そのような構造でなければ、クラスが多くなるからです。時にはオブジェクト間の関係を設定するためにcoordinatorの役割を行うクラスが必要なときにがありますが、ある人はこれがSRP(Single Responsibility Principle)の概念を違反していると話したりします。しかし、これらは依然としてサブシステム感の通信を遂行する一つの責任のみを担当しています。
テスト
アプリケーションを開発する時、それをテストすることで、その挙動を理解することにとても役立ちます。テスト容易性は非常に重要な要素ですが、テストコードを作成する人はまれです。受託開発の場合、クライアントはテストの重要性を理解できないために、テストコードを作成することについてお金を支払いたくないようです。私の場合、全体のアプリを作成して欲しいと依頼してくるクライアントにテストコードを作成することは必須事項だと話します。ある人が家を建てるために建築業者を雇用した場合、業者は彼らに家の建てる方法に関して説明したくはないでしょう。彼らが専門家だと思うから雇ったのですから。
もしある人が、アプリを作るために私を雇用すると、私は自分の能力を最大限発揮してアプリを作り、ここにはテストコード作成も含まれます。テストコードを作成することは大きな違いが生じます。私はアーキテクチャに関して話すたびに、テストがもたらす長所を常に強調しています。
まず、TDD 方式でテストコードを作成すると、テストコードを先に作成することになります。こうすると、アーキテクチャ、クラス、マネージャなどを実装するより前にAPIをデザインすることができます。新たな機能を追加する場合、テストファイルでクラスを生成できますが、最初のテストコードを作成する前にはインターフェースが無いのです。
最初のテストコードを作成し、どのように使用するのか考えながら関数を実装します。つまり、API使用者の観点からコードを実装します。TDD方式で開発しない場合、Publicインターフェースのプロトタイプを作成する時、Objective-Cのヘッダファイルでvoid関数またはSwiftでempty関数を利用してAPIを作成した上で、実際のコードでこれらを使用してみて適切でないインターフェースなら修正する必要があります。一方、TDD方式で最初にすることはインターフェースを実際に呼び出すことです。このような観点の転換は極めて小さな変化に見えることもありますが、API設計面では非常に大きな変化が発生します。APIを作成する時には、テストしやすい形にしたいのです。そうでなければテスト作業が大変だからです。
多くの人がテストコードを作成しない理由の一つはテストコード作成が難しいからです。しかし、テストコードを作成するのが難しいのなら、アーキテクチャに問題がある可能性が高いです。テストコードを最初から作成すると、インターフェースがもっと単純になり、柔軟になります。簡単にテストできる環境を作れば、システム内にある要素を簡単に変更することができます。
TDDで使用される red-green-refactor 方式に沿って、まず失敗するテストコードを作成して、最も簡単な方法でそれをコンパイルします。その後、コンパイルが成功するようにコードを簡単に修正してリファクタリングを行います。この過程を通じて皆さんはテストだけでなく、実装面でも、よりすっきりしたコードにすることができます。
私に任されたすべてのプロジェクトは、このような方法で進めています。もし、特定の機能をテストするために多くのフェイクオブジェクトを作った場合、一つ以上の責任を担当するオブジェクト、依存性の増加などの問題が発生してしまいます。
依存性を減らす(8:41)
FoldifyはParseサービスを基盤に作成しており、現在Parseサービス中断によるマイグレーション作業中です。アプリケーションでモデルオブジェクトを扱う追加的な階層が存在すれば、Parseから他のバックエンドサービスにマイグレーションする際に、SDKを直接使用した場合よりも容易になります。
Parseを初めて導入したとき、Parseサービスの品質は非常に低いものでした。ParseではParseフレームワークの内部に定義されたAssertionによってイメージをダウンロードすることすらできませんでした。プロジェクトとサードパーティサービス感に階層を一つ追加すれば、後に発生しかねない問題を予防することができます。
金銭的な収益をもたらすプロジェクトなら、現在は積極的に開発していなくても、数年間はプロジェクトをメンテナンスするのが良いです。私もFoldifyに新しい機能を追加したり積極的にプロジェクトを管理していないものの、メンテナンスは行っています。
受託開発の様にプロジェクトが完了して納品すると終わってしまう構造ではなく、数年間、生き残ることが可能なアーキテクチャを作ることが重要です。
私はいつも問題を解決しなければならない人が自分であるということを念頭に置いてプロジェクトを実行します。全ての問題を直さなければならない人が結局自分であるという考えでプロジェクトを遂行すれば、問題発生原因をもっと減らすことができるのです。
間違ったアーキテクチャ(10:07)
間違ったアーキテクチャであることを示すサインは何であり、潜在的な問題を容易に確認する方法は何でしょうか?
下記コマンドを利用してファイルの行数を確認することも一つの方法です。
find . -type f -exec wc -l {} + | sort -n
例題ではSwiftで作成したViewControllerコードが3千行程度、AppDelegateコードは約4千行とわかっています。正常なコードの長さに見えます。あるファイルの中に複数のクラスが存在する場合もあり、便宜のために、一つのファイルで管理する場合もあるので、一つのファイルのコード行数が多いと間違ったアーキテクチャということではないですが、コードを見直さなければならないというサインにはなりえます。
私はGitHubで公開中のすべてのObjective-Cプロジェクトについて特定ファイルが長いと警告メッセージを表示するスクリプトを作成して使用中です。このスクリプトは、長いコード行数のファイルがあれば「原則に従ってコードをまともに作成したのか確認して下さい」といったメッセージを表示します。
また、他の方法はGlobal StateとAppDelegateをプロジェクトに使用したかどうかを確認することです。複数のクラスがあり、global propertyを保存するために、AppDelegateを用いていれば、これは依存性の注入もなく、シングルトンを正しく使用していない状況に該当します。これは、単にGlobal Stateであり、後に問題を起こす可能性が高いです。Dependency visualizerを実行してみると、すべてのことがAppDelegateを指していることを確認することができ、多くのクラスがお互いに連結されていて簡単に変えることもできず、モジュール化もされていない構造という事実を知ることができます。
このような状況を避けたいならDependency visualizerと関連された追加情報をGitHub repoで確認してみて下さい。
デザインパターン(12:00)
デザインパターンについて簡単に説明します。すでに多くの文書があるのですべてのSOLID概念については言及しません。
人はデザインパターンをまるで宗教のように考えている場合があり、特定パターンに執着したあげく、全てのところにそれを適用できると信じるようです。デザインパターンは一種の道具であり、特定状況に適したパターンを適用する場合、非常に良い結果となります。
シングルトンはもともと悪いデザインパターンではありません。間違って使用することが多く悪いようにみえるだけです。良いデザインパターンであっても適切に使用しなければアーキテクチャは台無しになることがあります。
どんな状況で、どのデザインパターンを使用するべきか知るためには、十分な開発の経験と時間が必要です。デザインパタンに対する私のアプローチはいつも実用性に重点を置いています。特定のアーキテクチャパターンに従わなければならない場合でも不足した点が発見されるならば、テストしやすく、もっと素敵なコードを作ることができるきっかけになるし、異なるパターンを適用できるかどうか検討するスタート地点になるはずです。
ここにいる人たちにシングルトンについて問うなら、おそらくシングルトンは良くないので使用しないとしたり、他の代案がないので仕方なく使用していると答えるでしょう。シングルトンを使用する数多くの理由がありますが、ほとんどの場合適切に使用できていません。もちろんシングルトンパターンそのものが悪いわけではありません。もしデータベースにアクセスしたり、一つのインターフェースだけを許可される状況ならシングルトンを使用することができます。しかし、iOSではほとんどGlobal State Syncのための用途でシングルトンを使用しており、すべてのことをシングルトンに渡します。
アプリでLoggerはほとんどシングルトンで実装しますが、Swiftではシングルトンを使用しているという事実を公開する必要はありません。シングルトンを公開しなければアーキテクチャで見事な構造を維持することが容易です。
シングルトンを使用する理由は、簡単にアクセスできるからです。特定の箇所やクラスでログメッセージを出力するために、すべてのクラスにLoggerを渡すという方法ではなく、クラッシュが発生した場合だけログメッセージを出力したり、Loggerにアクセス可能であり、サーバまたはファイルにログメッセージを記録するなど、デバッグのためのログ情報を得られるLoggerが必要です。
シングルトンを持っていても、外部に公開する必要はありません。特にSwiftでは、こういった形で実装することが可能です。内部的にシングルトンを使用する基本実装(Default Implementation)を持ったLoggableというプロトコルを作成すると、シングルトンを使用したという情報が外部に露出せず、初期設定(initial configuration)を除く外部ではloggerにアクセスできません。
struct MyType: Loggable {
func someFunction() {
log(.Info, "log function is instance method provided by default implementation of Loggable")
log(.Warning, "Nothing in the app can access Logger directly, outside of it's initial configuration")
}
}
Loggableプロトコルを遵守するクラスを生成すると、log関数をすぐに使用でき、log関数内部ではシングルトンを使用していますが、アプリでは誰もこの事実を知りません。テストコードの作成時にdefault implementationを変更してLoggableの実装方法を変更することができ、オーバーライドもできます。正確にログが動作するかテストすることもできるし、関心のあるクラスのログも確認することができます。
シングルトンの外部公開に関係なく、シングルトンが間違って使用された例は多くあります。例えば、ネットワークマネージャーとしてシングルトンをたくさん使用しています。Image ViewerのURLを設定するために通常シングルトンを使用します。これは依存性の注入が欠如した話題であるため、依存性の注入で問題を解決することができます。
Composition(16:34)
Composition は誰にでもおすすめしたいほどのパターンです。 このパターンが好きな理由は簡単です。Compositionを正しく使うと、他の良い選択らと自然に連結されて、single responsibilityとDRY(Don’t Repeat Yourself)を遵守するコードになります。Single Responsibilityを持つようになる瞬間が特別なオブジェクト(Particular Object)を持つ時点であり、これはすでにDRYです。DRYコードを持つようになれば時間を節約することができるし、完全に再利用可能なコードを保有することになって、同じコードが重複しない構造を持つようになるのです。
Compositionパターンはさまざまな長所があります。Compositionは自然にSRPとDRYの概念を遵守します。小さなオブジェクトで構成すれば、他のオブジェクトにこれをInjectすることも可能であり、Fakeに差し替えられるため、テストが用意になります。オブジェクトに対するテストでも非常に小さな単位のインターフェースに対してのみテストを遂行できる長所があります。
また、小さなオブジェクトに分けた時も、可変性が少ないためにテストするコードが減ることになります。
Compositionを流用して使用している分野はまさにゲームです。Unityを使用した多くの3Dグラフィックゲームをたくさん見ることができます。アングリーバードスペースゲームだけでなく、他の多くのゲームが同一のUnityエンジンを使用しています。
全体UnityシステムはComposition基盤であり、Compositionを活用したエンティティシステム基盤です。オブジェクトを作り、そのオブジェクトに行動を追加することができます。例えば、ゲームでmonsterインスタンスを作成すると仮定してみましょう。最初は動かないで火の矢を撃つMonsterを作った後、ゲームデザイナーが移動機能を追加してプレーヤーを追随することにすれば、この機能を持った新しいオブジェクトを追加してMonsterに当該オブジェクトをInject、Monsterはこれからプレーヤーを追いかけることができるようになります。
私の経験の中でCompositionの最も良い例はUnityです。Unityを利用して数多くの他のゲームを作ることができ、他の機能、他の姿を表現することができます。複雑性の面でも様々な種類が可能です。Unityは一つのパターンに沿った単一のエンジンでありこのパターンは素晴らしく、多くのメリットがあります。
Compositionとともに使用するもう一つのパターンはまさに 依存性の注入(インジェクション) です。小さなオブジェクトで作ったなら、それらをハードコードして作る必要があるでしょうか。インジェクションを利用して新しいインスタンスを生成できるにも関わらず、ハードコードで依存性を作る必要があるでしょうか。
インジェクションが可能なオブジェクトが存在するならば、ハードコードをし続ける理由はありません。Unityで正常なオブジェクトを他のものに変更することを許さないならば、変更事項を適用するために新しいクラスを作らなければならないのです。プレーヤーのためのクラス、敵(Enemy)のためのクラスを別々に設けなければならないという意味です。UnityではCompositionを利用して他の設定が適用された敵クラスを作成することができます。
依存性の注入をテストにも活用することができ、依存性の注入を利用して実装コードを変えることもできます。一部だけ変更された同一のオブジェクトを作ることもでき、完全に異なる作業を行うこともできる非常に有用な方法です。また、これはもっと少ないテストで、同じテストカバレッジを達成できる長所があります。AppleもWWDC 2016でコードインジェクションを推薦しており、強力な依存性の除去、コード再利用のメリットについて言及しました。しかし、彼らはWWDCで完全に誤った方法で使用しましたが、この部分についてはしばらく後に見てみるようにします。まず、いくつかの人気のあるアーキテクチャパターンについて調べてみます。
人気のあるアーキテクチャパターン(20:39)
Bogdan Orlovが書いた”iOS Architecture Patterns”という素晴らしい記事があります。この記事で扱っているアーキテクチャからMVC、VIPER、MVVMについて見てみます。MVCが基本であり最も多く使用するパターンであるため、これから見ていきます。参考に、AppleはMVCを完全に誤った方法で使用しています。
MVC
Apple MVCでない、古典的なMVCは古くに作られており、ウェブでも使われています。コントローラはモデルデータを取得してビューに表示します。モデルが変更されるとコントローラは新たなビューを作ります。iOS環境では特定のフレームワークを使用しなければならないので、この古典的なMVCパターンを適用するのが難しいです。
Appleは、ビューとモデルを仲介するコントローラを作成するためコントローラの割合は大きくないものと考えていたが、ほとんどの人はMVCを構成する際にコントローラの割合が非常に大きなmassiveビューコントローラの形で使用しました。ビューコントローラはビューライフサイクルと非常に強く結合しているので、これを完全に分離できません。ビューコントローラを再利用できないため、ビューも再利用できないし、モデルだけ再利用可能な形で残ることになり、これはリソースを消費することになります。
ビューコントローラとビューがすべての責任を負っています。人々は、ネットワーク、ダウンロード、データ処理のため、これらを使用します。このアーキテクチャでテストコードを書いたことがあれば、人々がどうしてテストを嫌がるのか知ることができます。テスト自体が間違ったのではなく、テストしようとするアーキテクチャに問題があるのです。この構造にはCompositionもなく、インジェクションもないのが一般的です。
VIPER
次にVIPERを見てみます。先程のパターンは全体アプリのためのアーキテクチャパターンというよりはUIパターンに近いです。VIPERは初めて全体のアーキテクチャを扱った立派なパターンで新しい概念がいくつか登場します。
例えば、ビジネスロジックを担当するInteractorがあります。InteractorはCompositionを使用するその他のサービスと連携されます。ルータはビューとビューコントローラに他のプレゼンテーションコンテキストを適用することができ、これを利用して同じ画面を数か所に表示することができます。プレゼンターはUIKitとは独立的なプレゼンテーションロジックを含めているクラスです。UIKitは使用しておらず、画面に表示するデータに関するフォーマット処理などを担当します。
VIPERは非常に良いパターンですが、多くの基盤コードを生成しなければならない不便さがあるのでVIPERで必要なモジュールを生成してくれるcode generatorの様なものを使用するのが一般的です。VIPERを使用しない主な理由は多くのクラスを生成しなければならないという点ですが、このような問題を解決するため、VIPERクラスを自動的に生成してくれるcode generatorプロジェクトがGitHubにあるので参考にしてみて下さい。
MVVM
iOSで最も人気のあるパターンはMVVMでしょう。もっとも重要なのはUIViewControllerとUIViewをビューと分類するという点です。ビューライフサイクルと強く結合しているのでUIViewControllerはビューの階層に分類します。
ビューモデルをテストできるのでMVVMはよく使われるパターンです。ビューモデルにはUIKitと関連したコードがありません。したがって、ビューモデルはビューコントローラローディングと依存性がないため、ビューモデルを利用してビジネスロジックをテストすることができます。
MVCのテスト容易性が30%くらいならMVVMは70%くらいになります。残りの30%はUIテストで補えば良いです。MVVMの短所はバインディングをサポートするライブラリを一緒に使用しないと、多くの基盤コードを作成しなければならないという点です。これらのライブラリを使用しても、MVVMを完璧なFRP構造にする必要はありません。ReactiveCocoaとRxは素敵なライブラリですが、使い方が容易でなく、慣れるまでかなりの時間が必要です。私はMVVMでバインディングを使用する目的でこれらのライブラリを好んで使用しています。
簡単なObservableクラスだけ備えてもMVCよりもはるかにましな構造になります。MVVMは最も人気のあるパターンですが、多くの人が間違って使用しています。MVVMの長所はテスト容易性です。ビューモデルではUIKitに関連したコードがないのでUIから独立したテストを行うことができます。ビューコントローラのライフサイクルを複製する方法、Nibファイルでアウトレットをロードできるようにビューコントローラのgetterを呼び出すべきかについて悩まなくても良いです。きれいで素晴らしいビューモデルによってこのような問題が解決されており、ビューモデルと相互作用するすべてのものをテストできるようになりました。
しかし、MVVMはビューコントローラとどのようにバインディングするかについての問題が残っています。MVVMにはVIPERのルータの役割に該当するものがないからです。WWDCでAppleはルータがなければ依存性の注入を使用するよう推奨しました。彼らはこの方法がコードの再利用を高めるために良いと言いながら、特定のビューコントローラに他のビューコントローラを入れながら依存性を注入することを例に挙げました。
Appleの説明は問題があると思っています。1つの依存性を持った1つのビューコントローラがあると仮定してみましょう。新しいビューコントローラを作る度にそれを注入することは問題なく動作することでしょう。しかし、ビューコントローラをさらに追加した後、ビューコントローラでデータを監視したい場合どうすればいいでしょう?
例を挙げてみます。ビューコントローラAにビューコントローラBの依存性を追加した後、また、他のビューコントローラCを追加すれば依存性がビューコントローラAとビューコントローラBに追加されます。結局、最初のビューコントローラであるAは他のすべてのビューコントローラと同期化されることでしょう。つまり、ビューコントローラAは全てのビューコントローラに対する依存性を持つようになります。
一般的にアプリでは数多くのナビゲーションを持つことができるので、この様な現象が多く発生するでしょう。多くの依存性が含まれたビューコントローラのコードは可読性がとても落ちます。ソースコードを見てもどれが実際に必要なのかを把握することは難しいです。ビューコントローラでデータベースも使用して、イメージプロバイダも使用して、Loggerも使用して、その他にのものたちも使用たり、実際に使用するのは1つしかない可能性もあります。または何も使用していない可能性もあります。
最初のビューコントローラが歓迎のメッセージだけを表示し、依存性は無いと仮定してみましょう。もしすべてを最初のビューコントローラに注入するなら、問題が発生するはずです。ルータがなければみなさんはこのような構造を持つようになります。ルータの不在は、コードの面でもif
文が増加する短所があります。例えば、iPadとiPhoneで同じビューコントローラを表示しようとします。iPadではpush方式で表示して、iPhoneではmodalで表示する場合、if
文を使用しなければなりません。
func doneButtonTapped() {
let vc = ChildViewController(prepareNeccessaryState())
if Device.isIPad() {
navigationController.pushViewController(vc, animated: true, completion: nil)
} else {
var nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = UIModalPresentationStyle.Popover
var popover = nav.popoverPresentationController
popoverContent.preferredContentSize = CGSizeMake(500,600)
popover.delegate = self
popover.sourceView = self.view
popover.sourceRect = CGRectMake(100, 100, 0, 0)
presentViewController(nav, animated: true, completion: nil)
}
}
私はこの様な方法を選択しません。クライアントが使用者の設定に関する機能変更を要求し、プロフィール画面で表示したユーザーの設定画面を他の画面で表示する場合、if
文を追加して対処しなければならないのです。このようなやり方は状態(state)を追加するためにどのようなコンテキストで実行されるかを確認する作業を追加しなければならない不便さが発生します。この様な構造は多くの依存性を起こし、後にコードを読むことも難しいですし、コードを変更することも難しいです。
ルータの不在はMVVMの最大の強みであるテスト容易性にも影響を与えます。上の例で見たようにビューコントローラのpushによって依存性が高まったり、iPadで実行されることを確認しなければならない仕組みでテストを行うのはとてもむずかしいです。
MVVM - ルータの不在(29:33)
ルータのないMVVMの短所を見てみます。まず、不必要な依存性が発生します。自分がビューモデルに含まれた場合を除いては、ビューモデルが他のビューモデルについて知る必要はありません。コード再利用性も低く、if
文が多く使用されたスパゲッティコードが表示されます。また、テストが難しいです。ビューモデルをテストしたい場合は、ビューコントローラをstubにしなければなりません。私が望むことは簡単なテストで、作成しやすくて、実際にどのような仕事を遂行するのか把握するのも容易でなければならないですが、現在の構造ではテストコードの可読性が落ちるだけです。テストコードの品質は実際のコードと似たようなレベルを維持しなければならないため、これは重要な問題です。
MVVM + Flow Coordinators
私の友人のJib Roepckeから MVVM + Flow Coodinators というパターンを紹介してもらいました。最初はこのパターンに懐疑的でしたが、現在は多くの人がこのアプローチを改善して発展させています。
これは基本的により責任を持ったルータとすることができます。このテーマを詳しく説明した記事があるのですが、1つは私のブログにあり、Flow Controllersを利用したアーキテクチャの改善策についての内容を盛り込んでいます。
もう1つは Coordinators Redux を扱ったKhanlouの記事です。これはルータと似ていますが、複数のビューコントローラがある際には以下のような構造を持ちます。
このパターンではビューコントローラの構成を担当するオブジェクトが1つあります。もしMVVMを使用する場合、ビューモデルを構成する役割を担当するでしょう。このパターンはMVVMだけでなく、MVCにも適用することができます。上の絵から見ると矢印が双方向に表示されてますが、このパターンを使用していることをビューコントローラが知らなければならないという意味ではありません。
私はビューコントローラをブラックボックスのように使用することが好きです。ビューコントローラは担当業務に対する情報を提供するインターフェースだけ公開する方式です。例えば、EditUserSettings
ビューコントローラがいるのであれば、このビューコントローラは保存や取消を処理するのに私が関心を持っているものはこれだけです。
この場合、Flow Coordinatorは特定のコンテキストでビューコントローラを生成することになります。EditUserSettings
ビューコントローラがユーザープロフィールで表示される場合と別の画面で表示される場合、それぞれ異なる設定が適用されます。
プロフィール画面が良い例だと思います。スナップチャットのようなアプリで、プロフィールを見る時、左にあるメニューを利用してユーザープロフィールを確認することができます。デザイン変更によってこれからプロフィールを確認するためには、投稿画面に移動して投稿作成者のアイコンをクリックしなければならない場合があるでしょう。他のpresentationコンテキストに異なる設定コンテキストが必要な状況です。
Flow Coordinatorがすることは複数の設定可能なメソッドを提供して特定のシナリオによって設定することができるようにすることです。ビューコントローラはどんな状況でこれが設定されるのか知りません。この様な構造は同じビューコントローラを様々な箇所で再利用できるようにしてくれます。例えば、同じビューコントローラをお互いに他のビューコントローラの子コントローラに使用することができます。どれほど多くのトリガーがあるかによって、デリゲートまたはクロージャを使用することができます。簡単なものならクロージャだけでも十分間に合います。
MVVM + Flow Coordinator
Flow Coordinatorはコンテキストに合わせて設定したビューコントローラまたはビューモデルを生成する役割を担当します。image pickerがプロフィール画面と設定画面の両方で、イメージが含まれた投稿画面で表示されることもあります。Flow Coordinatorを使用すると同じコードを利用することができ、if
文を使用したコンテキストの判定処理を省略することができます。
iPad、iPhoneで異なる形式の画面を表示することができるので、コンテナビュー、コントローラの子ビューコントローラに使用する際にも同様です。たくさんの箇所でコードを再利用することができます。また、Flow CoordinatorはCoordinatingを通じてビューモデルまたはビューコントローラのイベントを監視することで、反応する処理を実行できます。
例えば、あることをmodelの形で見せたければflow coordinatorで設定した後、キャンセルまたは保存と関連したトリガーが発火すればクロージャーで処理することができます。すべての情報がビューモデルまたはビューコントローラの外部にあるととても便利です。
必要な時に注入できるという点もflow coordinatorの長所です。それぞれのビューコントローラまたはビューモデルは依存性を定義することができ、依存性リストをソースコードですぐ確認することができます。また、同期化を行ったり、これを他のオブジェクトに伝達する必要が無いので、実際にどのようなものが使用されているのか簡単に把握することができます。このような構造はドキュメント化の面で大きな助けに鳴り、他に何が使われているのか悩まなくて済むため、テストも容易になります。
MVVM + ViewModel
ビューモデルはプレゼンテーションに対する情報を知らず、一般的にUIKitと関連したコードがありません。identifierにURLを使用したUIImageも必要無いでしょう。もちろんUIImageを使用することはできますが、ビューモデルには基本的にビューとビューコントローラは存在しません。
テストの面では、ビューモデルをブラックボックスのように扱います。非常にシンプルなインターフェースを用いてテストするという意味です。また、ビューモデルは再利用できるために数カ所で同じパターンを使用することができます。
ReSwiftと開発支援ツール(35:24)
今まで私が現在使用しているMVVMについて見てきました。最近、iOSコミュニティで関心が高いパターンの一つをさらに紹介すると、それはReSwiftです。ReSwiftはバックエンドやウェブ開発で使用されるReduxをiOSに移植したライブラリであり、一方向の流れ(Uni-directional flow)アーキテクチャです。状態を変更するためには純粋関数(pure function)を使用しなければならず、純粋関数ほどテストが簡単なものはないでしょう。純粋関数は必ず入力と出力があり、これがみなさんがテストしなければならない対象です。
ReSwiftはすべての状態が一箇所(Single Source of Truth)にあります。Reduxをベースに作成されているためにアプリにstate構造体はただ一つだけあります。特にReSwiftは立派な開発支援機能を持っています。一方向のアーキテクチャなので、簡単にFlowの特定地点にあるものを追加して変更作業をしたり、追加機能を付け加えられます。
すべてのmutating関数に対するログを記録したい時、Reduxを利用すると簡単に実装することができます。内部で他の関数を追加しさえすればすべてがそこを通過するためです。Hot Reloadingと同じものを作ることもでき、バグを再現させることもできます。
特定箇所でクラッシュが発生したと仮定しましょう。頻繁に発生する問題ではなく、全体のうち1%のユーザーにだけこの様な現象が発生し、サーバにラグが発生したりする特定の状況でのみ発生する不具合ならどうですか?
これは非常に大きな問題です。このような不具合を再現することはご存知の通り非常に難しいです。不具合の再現のためにはすべての状態が一箇所で管理されていなければならず、状態を変更させる全てのアクションを保存しなければならないためです。このすべてのことを記録できるのであれば、不具合を再現することも可能です。クラッシュが発生したらクラッシュ発生前の地点に移動してすべてのものを再現してみて、デバッグでCall stackを確認して、何が問題を起こしているか確認することができます。私はこれがクラッシュレポートをよむよりもはるかに良い方法だと思います。
リモートワーク環境でもReSwiftが役に立ちます。ReSwiftを利用すると、特定の機能を作業する際、当該機能が提供されなければならない状態をロードすることができます。同僚にそれを送れば、彼らはアプリケーションの該当状態をロードして確認でき、このような過程をお互いに繰り返すことができます。
Playgroundsのような私のコードを見た方は、私がコード注入好きという事実を知っていらっしゃると思います。私はリアルタイムプログラミングが好きで、コード注入のためにフレームワークを使用せず、ネイティブで使用することを望みます。ReSwiftパターンはコード注入とよく合います。状態をロードするタイミングがプロジェクトを再コンパイルしたり、状態が再ロードすることができている時点だからからです。
リアルタイムにプログラミングしながら画面を通じて新たに追加したビューと機能がどのように反映されるかを確認できるので、コード注入を使用してみたことがなければ、絶対に使用してみて下さい。遅くてイライラしたPlaygroundレベルの話ではありません。実際のアプリケーション開発でコード注入を使用するとどれほど速いかを感じてみて下さい。
コード注入のための2つのツールのうち、Xcode injectionプラグインをおすすめします。ソースコードから変更した内容がシミュレーターにリアルタイムで反映されるため、作業時間を画期的に減らすことができ、特にUIKit関連の作業であればさらに大きな効果を発揮します。単にデザインを変更したい時に使用しても良いです。私はこのプラグインを利用して多くの開発時間を節約することができたので強くおすすめしたいです。
ReSwiftは私が期待するプロジェクトの一つですが、現在バージョン4.0がリリースされています(講演時点ではバージョン1.0です)。ReSwiftはまだ難しく感じるため、プロジェクトには使用していません。ReSwfitはウェブ観点から始まったプロジェクトであり、MVCでUIKitを利用してビューを直接表現することを許容しています。
このパターンを使用する幾つかのライブラリがあります。それらはReduxの概念を基盤に作られたライブラリです。
iOS Dev Weeklyに紹介された Render もその一つです。RenderはFunctional Viewの概念を使用し、Functional Stateが設定されたビューは、画面に描くことができます。Renderプロジェクトがさらに成熟されれば非常に便利そうです。私はウェブ開発者でもなく、ウェブをそんなに好きでもないですが、不具合を再現できる方法が好きで、速いテンポで作業して休むのが好きです。また、私は本当に楽しむことができることをすることが好きで、このような作業を支援するツールは簡単でなければならず、テストも簡単でなければならないと考えます。
結論(40:05)
良いアーキテクチャとは正解がない難しい主題です。一つのアーキテクチャが全ての問題を解決することはできません。開発課題とクライアントの要求条件に合うアーキテクチャが何かいつも考えている姿勢が大切です。
Reflectionを基盤に構築された新聞・出版プラットフォーム環境で作業した経験を例に挙げてみます。Reflectionが使用されたプロジェクトを行っていましたが、ウィジェット基盤のプレミアリーグのアプリケーションでした。使用者は他の情報が含まれたウィジェットを見たくなるはずです。コメントやビデオ、試合結果のようなものがありますよね。このアーキテクチャが素敵な点はObjective-Cの動的機能を活用してファイルでチェックさえすれば特定の機能を活性化したり、無効化できるという点です。
もし特定の機能を無効化したいなら、関連があるファイルを選択した後、ターゲットは選択しない状態でプロジェクトをコンパイルして該当機能を無効化できます。特定の機能を追加したい時は特定パターンに従うクラスを追加した後にコンパイルすると、新しい機能が追加されます。プロジェクトに10人の開発者たちを追加してもコア部分は手をつける必要なく独立的に働くことができます。
要求条件に合わせ、アーキテクチャは完全に変わるでしょう。私の場合は今はMVVMを基本的に使用していますが、これからはReSwiftや他のアーキテクチャを使用するかもしれません。プログラミングは常に何かを学ぶことができるという点が魅力だと思います。私が今正しいと考えることが2年度には完全に変わることも考えられます。もしそうならないなら、それは十分に勉強していないからでしょう。
すっきりしたコードを作ることができるデザインを志向しながらも、固定観念を持たないように努力しています。TDD、compositionの様な単純な概念に従うことにしても、私が過去10年間作業したプロジェクトよりずっとましなアーキテクチャを構成できると信じています。
単純ながらも柔軟なアーキテクチャを作りたいです。過度な抽象化はやめて、KISS(Keep It Simple, Stupid)原則を守りたいです。
過度な抽象化についてもう少し話してみます。以前にゲームエンジンとフレームワークを作ったことがありますが、私と同僚のアプローチは完全に違っていました。
私の友人は可能なすべてのシナリオと設定ができるように、すべてのものを抽象化して作りました。Collisionを作成するときもすべての状況を処理できるCollisionを作ろうと思っていました。友人は6〜7個のでもゲームが含まれたゲームエンジンをリリースし、その中にはパックマン3Dバージョンのようなゲームもありました。その後その友人は就職しました。
オープンソースだったの自分のエンジンを彼はありとあらゆるところに使っていました。子供向けゲームを作る会社に就職した後は、パックマン3Dバージョンと類似したゲームを開発しました。パックマンの代わりに魚がいるという点だけを除いてです。
彼が前にデモで作ったゲームとゲームプレーやグラフィック観点ではほとんど差がなかったのですが、彼はゲーム開発ソースを基盤からまた作成していました。友人の姿を見ながら、単純で容易に理解することができ、発展させることができる柔軟さについて悩みました。この様な柔軟さは抽象化のため過度な設計をした時に、人々がよく見落としている部分です。
抽象化によって得られる利点があるのは確かですが、バランスよく設計する際、その長所が発揮され、SOLID原則に従うのが良い出発点だと思います。SOLIDは、アーキテクチャを話す時ごとに登場する概念であり、cleanアーキテクチャと関連した文章でいつも言及される概念です。
質疑応答(44:14)
MVVMをflow controllerとともに実装しようとする時、参照できる記事やソースコードがあれば紹介してください
私のブログにある記事とKhanlouのブログにある flow controllers、coordinator redux についての記事をお勧めします。グーグルで”Improving Architecture、and it’s Flow Controllers”または”Coordinator Redux”を入力してみてください。記事には若干の例題コードとアーキテクチャに対する説明が含まれています。気になった点を私のツイッターに問い合わせてもらえれば喜んで回答します。簡単な例題は少しありますが、複雑なアプリの例はないようです。皆さんがオープンソースで公開することもよさそうですね。採用面接の時にオープンソース開発の経歴があるかどうかを尋ねる場合もあります。
このような難しいパターンを使用すると、UIトランジションやアニメーションを処理することが大変になる恐れもあると思いますが、UIトランジションやアニメーション作業をカスタマイズしたアーキテクチャを作ることは難しいですか。
Flow coordinatorでは基本的にビューを設定することができます。ビューが特定トランジションdelegateを使用するようにすることができ、既存の設定方式と比較してみた時、そんなに難しくありません。むしろ、Flow Coordinatorを利用すると、ビューを設定した次の数カ所で使用できるという長所があります。例えば、小さなイメージをクリックすると大きなイメージに変化し、投稿に移動する時は遷移アニメーションを使用し、同じ投稿プッシュ通知から開いた時は遷移アニメーションが必要無いと仮定してみましょう。このような場合、Flow Coordinatorの長所が輝くようになります。delegateを使用して一つのコントローラでのみ遷移アニメーションを実現すれば良いからです。
Coordinatorを使用するとStoryboard Segueの長所は利用できないという意味ですか?(Storyboardを使用できないという意味)
Segueを利用することができます。前にObjective-C環境でのStoryboard Propagationの動作方式を変更して、そうしたことがあります。私が作ったSegueはFlow Coordinatorでも呼び出されますが、若干の努力が必要になります。基本的に提供されているSegueでは無理です。私は、インターフェースビルダーが好きで、私が作成したSegueの動作方式について講演も何度もしており、多くのプロジェクトでそれを使用してきましたが、アーキテクチャと関連してはSegueを推奨したくないです。SwiftでSegueを利用する方法は、文章で残しておいたので参考にして下さい。Segueを使用した際に得た利点は多くはないですが、Segueがぜひとも必要であれば使用することもできます。tableViewCellクリックをSegueに設定できますが、同一デザインを使用する箇所でなければ該当tableViewを他の場所でリサイクルできないというのが大きな短所です。
AppleがSDK内部に使用するパターンを発展させない理由は何だと思いますか?
MVCパターンはiOSが誕生する前の80年代から存在していたパターンです。MVCは非常に古いパターンですが、Compositionを使用してTDD方式に従うなら、多くのことをテストすることができ、コントローラをCoordinateできるので、MVCを使用することが悪いコードと評価することはできません。Appleが基本的なパターンを変更することは非常に難しく、多くの変更が伴う作業になってしまいます。文書化だけでなく、多くのことを変更しなければならないのです。
現在Appleはコミュニティの流れに従うため、努力中です。実際にApple開発者たちからインターフェイスビルダーの改善方法に関する問い合わせを何度も受けました。Appleはコミュニティで起きていることを注意深く観察しており、異なるパターンを使用するユーザーも関心を持って見守っています。AppleがMVVMを使用するようにと言うかもしれません。代わりに彼らはバインディング機能を提供しなければならないです。バインディング機能がなければMVVMを構成することが難しくなってBoilerplateコードを多く必要とするからです。FRPは複雑すぎるため、AppleはFRPの流れに賛成しないはずだと思います。VIPERのようなパターンを使用し、人々に説明することができて、パターンを理解するのに多くの時間が必要です。
MVCはiOSアプリを初めて開発する時の良いスタート点になることができます。MVCを利用して問題なくiOSアプリを配布することもできます。ここまでは問題ないですが、徐々に時間が流れてプロジェクトが大きくなってくるといつかは問題が発生します。より良いパターンを使用することはできず、Compositionも使用できませんから。
私はこの問題を接近性の観点から見なければならないと思います。MVCは非常に簡単に接近することができます。説明しやすいからです。完璧ではないですが、説明しやすいということです。MVVMは説明は簡単ですが、バインディングが必ず必要です。バインディングがなければ作業が難しくなって多くの基盤コードを必要になります。
VIPERは人たちに説明するのが難しいです。特に初めてこの概念に接する人にはもっと難しいです。
多くの開発者がSwift開発に参加しており、iPadでは子どもたちがコードを簡単に学べるPlaygroundsアプリを提供しています。非常に嬉しいことです。
プラットフォームの接近性は改善され続けています。ウェブ開発をしていた開発者なら慣れた方式を使用するReSwiftをアーキテクチャに選択できるようになったのです。異なる背景と異なる経験の水準をもつ人々も容易に接近することができるようにするのが優先だと思います。
多くのAppleエンジニアたちが自分たちが提供するサンプルコードはよいアーキテクチャを示す例ではなく、特定の技術を説明するためのものだと言います。そのため、私たちはサンプルコードにあるビューコントローラの実装方式に沿ってはいけません。
整理しますと、MVCと関連した問題は接近性に対する観点の違いと考え、Appleは異なるパターンを使用しようとせず、MVCをもっと柔軟な構造で維持しています。
ありがとうございます。
About the content
This talk was delivered live in June 2016 at mDevCamp. The video was transcribed by Realm and is published here with the permission of the conference organizers.