Androidのイカした設計について調べていると、必ずと言っていいほどこの記事が言及されています。
英語とかよくわからない情弱なので、理解するために訳してみました。
(以下、訳文)
はじめに
高品質なソフトウェアを作るのは困難かつ複雑です : 成長・変更に耐えるためには、ただ要件を満たしているだけではなく、強固で、メンテナンスもしやすく、テストもできて柔軟である必要があります。
"クリーンアーキテクチャー" は登場して以来、これはどんなソフトウェア開発においても良いアプローチだろうとされています。
考え方はシンプルです: クリーンアーキテクチャー はシステム開発の経験から生まれた以下の考え方に基づいています。
- フレームワークに依存しない
- テスト可能
- UIに依存しない
- データベースにも依存しない
- その他、外部の何もかもに依存しない
別に(絵の通りに)4層でないといけない訳ではありません。これが唯一正しいとかそういったものではないので。
ただし、 依存のルール だけは覚えておいてください。ソースコードの依存関係は常に円の内側にのみ向いていて、全てを知ることができるのは外側の円だということです。
ここでいくつか、このアプローチをより理解するための言葉を紹介します。
- Entities: アプリケーションのビジネスオブジェクトです
- Use Case: データの流れを組み合わせて entities を形作るユースケースです。 Interactors とも呼びます。
- Interface Adapters: このアダプタの集まりがデータ形式を use case や entities が使いやすいように変換します。Presentor と Controller がこれに該当します。
- Framework と Driver: 説明はいらないでしょう:UI、ツール、フレームワークなどなど。
もっとよく知りたい方は、この記事 または この動画 をあたってください。
シナリオ
うまく始めるために、シンプルなシナリオで始めてみたいと思います:クラウドから友達のリストを取り出して表示し、誰かがクリックされたら新しい画面が開いてそのユーザーの詳細を表示してくれる、単純なアプリを作ります。
私が話したいことの全体像は、ビデオを見てもらえればわかると思います:
Clean Architecture on Android - Sample App
Androidのアーキテクチャ
目的は外界のことを全く知らずにビジネスのルールを保つことによる 関心の分離 です。これによって、外部のいかなる要素にも依存することなくテストが可能になります。
それを成し遂げるため、それぞれが互いから分離された目的と働きを持つ、 3層にプロジェクトを分割すること が私の提案です。
それぞれの層が独自のデータモデルを持つことで独立性を得ていることに価値があります(データの変換をするために、実装時にはデータマッパーが必要になるでしょうが、アプリケーション内で共通の独自モデルを用意したくないのであれば、それは払うべきコストでしょう)。
スキームはこんなかんじです。
注: 私は(JSONパースのためのgson、テストのためのJUnit, mockito, robolectric, espressoを除いて)外部ライブラリを使いませんでした。少しでもサンプルをクリーンにするためです。あなたの人生を楽にしてくれるような、データをディスクに保存するためのORM、依存性注入ツール、それに類するツールやライブラリの追加を躊躇ってはいけません。 (車輪の再発明はグッドプラクティスではありません。)
プレゼンテーション層
ビューやアニメーションに関連するロジックがあるでしょう。ここでは Model View Presenter (以下、MVP)しか使いませんが、MVCやMVVMといったお好きなパターンを使ってください。詳細には踏み込みませんが、 fragment や activity は単なるビュー であり、それらの中にUIロジック以外のロジックは含まれず、ただ表示することだけに徹させてください。
この層の Presenter は、UIスレッドではない新しいスレッドでジョブを走らせ、コールバックを用いてデータを View に描画する interactor (use case) で構成されます。
もしクールな例が欲しければ、友人 Pedro Gómez が MVP と MVVM を使った Effective Android UI を作ったのでご覧ください。
ドメイン層
ビジネスルールはここ:すべてのロジックはこの層で扱います。 Androidプロジェクトに関して、すべての interactor (use case) の実装はここに置きます。
この層はAndroidに依存する何もかもを取り除いた、純粋なJavaモジュールです。 すべての外部コンポーネントは、ビジネスオブジェクトと接続する際にインターフェースを用います。
データ層
アプリケーションが必要とするすべてのデータは、ファクトリを通して条件に応じた異なる情報源を選択する戦略を持つ Repository Pattern を使った UserRepository の実装(インターフェースはドメイン層にある)からもたらされます。 具体的には、ID からユーザー取得するとき、ユーザーがキャッシュに存在すればディスクキャッシュを情報源に選択し、そうでないときはクラウドに関連データを問い合わせた後にそれをディスクキャッシュへ保存します。
この発想は、データの源はクライアントにとって透明であるということに基づいていて、 データがメモリから来ようと、あるいはディスクやクラウドから来ようと気にしませんし、データは到着し取得できることを真実としています。
注: 学習が目的なので、サンプルコードにおいて私は非常にシンプルかつプリミティブに、ファイルシステムとAndroid Preferensesを用いたキャッシュを実装しました。思い出してください、より良い方法で同じ振る舞いをしてくれるライブラリが存在するのであれば 車輪の再発明はすべきでない ということを。
エラーハンドリング
これは常に議論のトピックになるものなので、あなたのソリューションをここで共有するのは素晴らしいことだと思います。
私の戦略ではコールバックを使います。 従って、例えばもしデータリポジトリの中で何かが起きたら、コールバックは onResponse()
と onError()
2つのメソッドを持ちます。最後の一つは ErrorBundle
と呼ばれるラッパークラスで例外をカプセル化します:このアプローチはいくつかの違いをもたらします。エラーによるコールバックの連鎖がプレゼンテーション層に到達して描画されるためです。コードの可読性は多少妥協できます。
もう一方では、何かの間違いが起きた時に event bus の仕組みを用いて実装することもできましたが、この手の解決方法は GOTO を使っているんみたいですし、加えて、私見ですが、もしあなたがイベントを精密にコントロールできないなら、購読しているいつかのイベントを見失いかねません。
テスト
テストすることについて、いくつかの解決策をレイヤーに基づいて選択しました。
- プレゼンテーション層: android instrumentation (訳注: UIAutomatorのことと思われる) と espresso を結合テストと機能テストのために使用した。
- ドメイン層: JUnit と mockito を単体テストに使った。
- データ層: Robolectric (androidに依存したレイヤーのみ)、JUnit、mockito を結合テスト、単体テストに使用した。
コードを見せろよ
コードがどこにあるか気になるでしょう、ですよね? この github のリンクから私が作ったものを見ることができます。フォルダ構造について、特に触れませんが、それぞれのモジュールがそれぞれの層に対応します。
-
presentation
: プレゼンテーション層を表すandroidモジュールです。 -
domain
: androidに依存しないjavaモジュールです。 -
data
: すべてのデータを取り出すandroidモジュールです。 -
data-test
: データ層に関するテストです。Robolectricを用いる都合で、別のモジュールに分離しました。
結論
Uncle Bob は "アーキテクチャの目的は意図であり、フレームワークではない" と言っていて、私もこの文言に完全に同意します。もちろん物事を成し遂げるにはいくつもの異なる方法(異なる実装)がありますし、毎日のように沢山の変更に遭遇するあなた(私も同様)に非常に共感しますが、このテクニックを使うことで、あなたのアプリケーションはこうなるでしょう。
- 維持が簡単
- テストが容易
- まとまりがある
- 疎結合
結びに代えて、あなたがこれに挑戦して、成果物と経験したことを共有することを強くおすすめします。 同じようにあなたが見つけてきた他のアプローチもうまくいくでしょう。 継続的進歩 は常に良いことで、前向きであることを私達は知っています。
この記事が役に立つことを願っています。フィードバックはいつでも大歓迎です。