[ Android ] - これからの「設計」の話をしよう
釘宮愼之介
はじめまして。
6/1より入社いたしましたAndroidエンジニアの釘宮です。よろしくお願いいたします。
今日はAndroidの設計について語ってみようと思います。
その前に
「良い設計とはなにか」という議論が「正義とはなにか」という議論のようにいつまでたっても結論がでないのは、環境やチームメンバのスキルセット、ステークホルダーによって目指すべきゴールが変わるためだと考えます。
つまるところ、設計に正解はありません。
そのため以下で話すことは、「これが設計の正解だ!!」というわけではなくて、「こういう設計の仕方するとうまくいくっぽい」くらいのノリです。
あと、特にMVCとかDDDとか人によって解釈のズレが起きやすいところなどは、冗長になるのを嫌って自分の解釈で言い切っています。ご了承ください。
設計の目的について
ハードルが下がったところで、早速。
まず設計の目的ってなんでしょうか?
この設計という行為自体は建築や日曜大工でもありますよね。
そしてその目的は共通して「ゴールまでの見通しを立てること」のようです。
プログラミングもほぼほぼそれであってます。あってますが足りません。
というのもプログラミングには「最終的なゴールが存在しない(ことが多い)」という大前提があります。
そのため、ひとまず見えているゴールまでの見通しを立てるだけでなく、それ以降に現れるだろう新しいゴールのことも考えなければなりません。
以上から、プログミラングにおける設計の目的は
「ゴールまでの見通しを立てつつ、将来の変更にも柔軟に変更できるようにすること」
だと考えます。
柔軟に変更できるようにするために
プログラミングがもし「一度完成すれば何があっても変更することがない」というシロモノであれば、もっと世界は簡単になっていたと思います。
どんなにクソコードを書いても、僕らはそれをクソコードと認識することなく、プログラマは幸せな人生を送れたでしょう。
ただ、残念ながら現実は違います。
一度完成してもほぼほぼ「変更や追加」という事象は入ってきます。
僕らはそれらのまだ見ぬ「変更や追加」に備えつつ、かつ今の目の前のゴールとも競合しないように柔軟に作らなければなりません。
こんなのただの未来予知ですよね。どんな人だってなんでもは知りません。知っていることだけです。
しかし、それでもこの曲者に抗うために数々のアーキテクチャが生み出されてきました。
やはり一番有名なのは「Model View Controlle」(以下MVC)ではないでしょうか。
Model View Controllerを作ったTrygve Reenskaugさん曰く「MVCの目的はユーザのメンタルモデルとコンピュータのデジタルモデルのギャップに橋をかけることだ」と言っています。
僕の解釈ですが、Trygve Reenskaugさんの考えをわかりやすく箇条書きにすると
- プログラミングという行為は大きく分けると「なにを実現したいか?」「どう見せたいか?」の2つにおちる
- そしてこの二つは全く別物なので、明確に分けるべきで、また「なにを実現したいか?」が重要である
- 「なにを実現したいか?」というのをそのままプログラミングに落とし込む部分をModelに記述する
- 「どう見せたいか?」をView Controller部分(ReenskaugさんはVCを合わせてToolと呼んでいる)部分に記述する
となります。
プログラミングで実現することは、結局は「なにを実現したいか?」「どう見せたいか?」に落とし込めるので、これを明確に分けて意識することで、将来の変更にも柔軟に変更できるようになるだろうという仮説が根底にあります。
そしてその仮説は年月という実績があるので、正しいと言っても良いようです。
そのため、自分らで実装するときもこのコンセプトを取り入れることで巨人の肩に乗ることができそうです。
ただこれだけでは足りない。
というのもこのMVC、「なにを実現したいか?」をプログラミングに落とし込むことの重要性を説いてはいるものの、じゃぁどうやってプログラミングに落とし込むかについてはほぼほぼ語ってないんですよね。
で、そこに焦点を当ててるのがドメイン駆動設計(DDD)です。
DDDでは「なにを実現したいか?」をどう実現するかについて、プログラミングの設計に収まらずプロジェクトの進め方などのもっと上のレイヤーまで提案しています。
この考え方も取り入れることで、「なにを実現したいか?」と「どう見せたいか?」を明確に分けつつ、かつ「なにを実現したいか?」についてどうやってプログラミングまで落とし込むかまでを見通すことができそうです。
じゃぁ「どう見せたいか?」をプログラミングに落とし込むには?となるんですが、これはプラットフォームによってベストプラクティスは変わってくるので、一般的な設計みたいなものはないようです。
以上をまとめます。
- 「柔軟に対応できるようにする」には「なにを実現したいか?」と「どう見せたいか?」を綺麗に分けて実装すると良い
- 「なにを実現したいか?」についてはDDDが詳しい
- 「どう見せたいか?」についてはプラットフォームによってベストプラクティスが変わる
結局こうなった!
Androidに戻りましょう。今までのことを考慮して吟味した結果、今の僕の考えはこういう感じになっています。
プレゼンテーション層、ドメイン層、インフラ層という三層のレイヤーに分けています。
「どう見せたいか?」をプレゼンテーション層、「なにを実現したいか?」をドメイン層に書きます。で、インフラ層は「どこにデータを保存するか?」について書きます。MVCのコンセプトにはできてませんが、「なにを実現したいか?」と「どこにデータを保存するか?」って実は別物ですよね。だからここもさらに切り分けましょうという考えです。
基本的には下のレイヤーの人は上のレイヤーの人のことを知りません。そうすることで、上のレイヤーをすっぱり切り変えることが可能になります。たとえば、GUIだけじゃなくてコマンドラインからも受け付けるようにしよう!という追加があったとしても、ドメイン層より下は変えずにプレゼンテーション層にコマンドライン用のものを追加するだけでOKということが可能になります。
上のレイヤーの人は下のレイヤーが公開しているinterfaceだけを知っているようにすると良いです。そうすることでinterfaceさえ共通していれば、下のレイヤーもまるっと切り替えることができます。例えば、ローカルDBじゃなくてサーバーにあるDBを使うようにしてくれってなってもRepoisitoryだけ変えれば良いみたいなことも出来るようになるわけです。
また下のレイヤーから上のレイヤーへの通知に関して。
UseCaseからPresenterへの返却は同期と非同期の場合があります。僕は非同期時にはEventBusを使っています。以前は、(Callbackは画面が死んだ時の参照が怖いのでそうではなく)MVCのセオリー通りObserver PatternでPresentation層への通知を実装していましたが、コード量ががあまりに増えすぎてしまって、それが嫌になり最終的にEventBusに落ち着きました。
UseCaseとRepositoryは非同期にする理由がないので、同期で実装します。
以上が簡単なポイントで、以降はそれぞれの層についての説明をしていきます。
プレゼンテーション層について
実はAndroidではMVCの採用は結構つらいものがあります。
というのもアプリ作成には欠かせないActivityやFragmentがViewの表示や作成も担当し、かついろんなイベントの管理もするのでControllerの役割も持ってしまい、どうしても多重責務になってしまうからです。
その問題を解消するためにModel View Presenter(以下MVP)を採用しています。
MVPはMVCとコンセプト自体は全く同じで、違うのは「Viewで発生するユーザイベントを全てPresenterに移譲する」って謳ってるだけのシロモノくらいの認識でいいです。
これをAndroidに適用することでAcitivtyやFragmentで発生するイベントをPresenterに委譲することができるので、Controller部分を取り除くことができ、ActivityやFragmentを完全にViewとして扱うことができるようになるのです。
PresenterはViewからイベントを受け取って、UseCaseに問い合わせし、結果が返ってきたらそれをViewに反映する役割(まさにController)を担います。
ドメイン層について
「何を実現したいか?」を書く一番重要な層です。
それぞれの役割について説明していきます。
Entitiyは各データを表すクラスです。データベースのテーブルと1:1になることが多いです(もちろん例外もあります)。
ValueObjectにはユニーク性のないの値を定義します。
で、実際にロジックを書くのはUseCaseです。
その名の通り、1ユースケースで1クラス作ります。例えば会員登録ユースケースとか。
Entityごとに1Service作る手法もあります。たとえばUserというEntityクラスがあって、UserServiceみたいなUserエンティティを操作するサービスを作る方法です。
この方法でもいいのですが、経験からしてこのやり方は煩雑になることが多かったので、僕はユースケースごとに作るようにしました。
Entityごとに1Serviceを作る場合、例えば会員登録処理、ログイン処理、削除処理とかすべてをUserServiceにぶちこむだけで結構なボリュームになることが必至です。
さらに、他のEntityに跨った処理があると、あるServiceが他のServiceのインスタンスを持たなければならなかったりします。
そうやって、最終的には各Serviceががんじがらめになって、しかも結構なボリュームな混沌ができてしまいます。
そのため、クラス数が多くはなりますがユースケース毎というのが良いと思います1)規模にもよります。。
インフラ層について
上の層がデータの置き場所を意識しなくて良いようにして、CRUDを提供するのがRepositoryです。
API, SharedPreferences, SQLite(Content Provider), メモリキャッシュなどから適切な場所から適切な値を取得したり、更新したりします。
メリット・デメリット
デメリット
デメリットは一つです。それは冗長になりやすいことです。
規模感が小さいなら、こんなに区切らなくても多重責務になったりはしないので、時間の無駄になってつらくなることがあります。
ただ、仕事でやる場合は、最終的に設計が崩壊するよりは多少冗長になる方がマシなので、そんなに意識しなくてもいいのかなと思います。
メリット
そもそもの設計の目的を達成できること以外にも副作用的に得られるメリットが幾つかあります。
レビューしやすい
現在、こういう設計手法でやりますよとプロジェクトのみんなに伝えています。
例えば新機能を作った場合、僕は
- HogeFragment
- HogePresenter
- HogeUseCase
- HogeFragmentTest
- HogePresenterTest
- HogeUseCaseTest
みたいなクラス群を作ることになります。
レビュアーはこのPresenterやUseCaseなどの名前を見ただけで、一体何者なのかというのが大体わかり、UseCaseを特に気をつけてみればいいんだなというのがわかります。
またUseCaseにはActivityやFragmentなどのAndroid特有なコードがほとんどないので、最悪Android知らない人でも見れます。
テストが書きやすい
それぞれinterfaceで区切っているので、テストがしやすいです。
役割が明確で、またあらかじめどんな役割の人がいるのか大抵きまっているのでテストをパターン化しやすいです。
現状はWikiにTest Policyというページを作って
- Activity,Fragment
- Presenter
- UseCase
などの項目を作って、それぞれ下記を書いています。
- 何をする人のなのか
- テストでは何を確認しているのか
- サンプルコード
サンプルコード
[こちら] にサンプルコードを用意しました。画面にユーザー名を表示するだけのものです。これだけの規模だと冗長になってしまいますが、骨組みと流れがわかれば幸いです。
おわりに
今回は、なるべく簡単な言葉で概念や雰囲気を説明しようと思ったので、実際のコードを引用することは省いており、サンプルコードのリンクを貼る程度に抑えています。
もし質問などあればお気軽に@kgmyshin までお声掛けください!
脚注
↑1 | 規模にもよります。 |
---|