Androidアプリの設計の話 - Re.Ra.Ku アドベントカレンダー day 4
Re.Ra.Ku アドベントカレンダー 4日目です。
こんにちは、安部です。Androidアプリを担当しています。初投稿です。
今回はおそらくAndoidアプリ開発者は常に頭を悩ませる問題である、設計の話です。私もずっと試行錯誤を繰り返し、現在進行系で悩んでる問題でもあります。少し見えてきた部分があるので現在の状況を共有したいと思います。
抱えてる問題
Androidアプリ開発していくうえで、よくぶつかる壁です。
ライフサイクル
Androidのライフサイクルは複雑でViewの状態をどのように管理すればよいのか難しいです。今はActivityだけではなくFragmentもあるので、更に複雑にしていると思います。
また、画面の回転などの対応を考えたりすると更に大変です。
非同期処理
通信は非同期にしなければいけません。そのためUI側もそれを意識しなければいけなくなり、Viewの状態管理も複雑になっていきます。
テスト
ContextなどAndroid固有のものを必要とするクラスが自然と増えてしまうので、すぐにテストがしにくくなってしまいます。
最近はこのあたりはDagger2でDIすることでだいぶ改善されつつありますが、まだまだコストが高い印象です。チーム等で検証してバランスを取ってやっていくのが良いかなと。
DDD
DDDをやらなきゃと思って、どこに何を書けば良いのかわからない状況に陥りやすいです。いざやってみると頭を抱えることが多くなります。
現在の状況
現在はMVVM+DDDみたいな形でやっています。
レイヤー分離
現在のレイヤーの分け方は次のようになっています。
- Presentation
- Viewに対しての操作や状態管理、イベントハンドリングを行います。
- Application
- Domainの直接のクライントになります。ここはPresentationから呼び出され、Domainへ処理を移譲するものになります。
- Domain
- モデルやビジネスロジックやリポジトリとAPIアクセスのインターフェースなどがあります。
- Infrastructure
- 詳細な実装を記述します。DBアクセスやAPIアクセスなどの詳細な実装になります。
依存関係は Presentation → Application → Domain になります。InfrastructureについてはDIを使って依存を解決するので、他のレイヤーに直接でてくることはありません。
packageも同じような分け方になります。
Presentationレイヤー
ここではUIに関するものを処理します。
現在はDataBindingを使った、MVVMっぽい感じにしています。まだまだ改善の余地があると思っていますが。
MVPでもMVVMでもやることは変わりません。UIなどの表示とイベントに関する責務を負います。それ以外のことはしないようにします。逆に他のレイヤーにこれらの関心ごとが漏れないようにします。
ここではビジネスロジックを処理しませんが、おそらく一番コード量が多いレイヤーになるかと思います。特にViewの状態を管理したりするのはライフサイクルなども絡んできてかなり複雑になると思います。
私の場合ですが、ここから直接Domainレイヤーの処理を呼ばずに、後述のApplicationレイヤーを経由するようにしています。
このあたりのDataBindingとMVVMについては来年のDroidKaigiで話す予定です。
Applicationレイヤー
ここはDomainの直接のクライアントになります。Presentationレイヤーから呼び出されDomainレイヤーへ処理を委譲します。Domainレイヤーに受け渡すためのデータの加工を少しやったりもします。
Domainレイヤー
ここが一番どうしたら良いのかが分からなくなることが多いと思いますが、最近思うのはAndroidアプリではDomainレイヤーはビジネスロジックがそこまで多くない気がします。
アプリの性質にもよりますが、だいたいAPI経由で処理をサーバーに移譲している思うので、ほぼサーバー側にビジネスロジックが実装されていることになります。そのためDomainレイヤーにはAPIに対するインターフェースとモデルが多い感じなっています。
ただ、しっかりとモデルを設計する必要はあるので、そこまで単純ではないと思います。
ここではAPIやDBの実装の詳細は記述しません。例えば、API通信の処理についてはインターフェースのみを定義して、実装はInfrastructureレイヤーに記述するようにします。
リポジトリの場合は次のような感じのみです。
public interface UserRepository { User getUser(); }
APIのインターフェースは私はGateWayと命名しています。ここでは戻り値をRxJavaのObservableにすることで非同期を扱いやすくしています。
public interface UserGateWay { Observable<User> getUser(); }
また、ドメインモデルにも実装の詳細が入り込まないようにします。例えば、Realmを使ってる場合でRealmObjectを継承したりはしないようにします。DBテーブル情報はInfrastructureレイヤーのほうで定義してあげて、それからドメインモデルに変換してあげるようにします。
ここでは基本的にAndroidのフレームワークに依存するようなものが無い状態(import android.*
がない)が理想だと思います。しかし、一部Parcel
などは例外にしています。最初はPresentationレイヤーでDTOみたいなのを作って対応していたのですが、ごちゃごちゃしてきてしまったので、Parcel
は例外的にOKにしました。
Infrastructureレイヤー
Domainレイヤーに定義されたインターフェースを実際に実装します。
ここはPresentationレイヤーの次にコード量が多くなる気がします。
APIやDBから取得したレスポンスをドメインモデルへの変換もここで行います。
public class RealmUserRepository implements UserRepository { public User getUser() { // DBから取得して、ドメインモデルのUserへ変換 } }
public class APIUserGateWay implements UserGateWay { public Observable<User> getUser() { // APIから取得して、ドメインモデルのUserへ変換 } }
DI
Infrastructureレイヤーにおいたものは基本的に他のレイヤーで直接依存するようがないことが重要です。そのためDIで依存を解決してあげます。
変更に強くなったり、テストしやすくなったり、BuildVariantごとに差し替えたりできるようになります。可能であれば初めから導入しておくことをおすすめします。あとから導入すると結構大変なので。
すべては書きませんが、Dagger2の場合は次のような感じのモジュールを使う感じなります。
@Module public class DomainModule { @Provides @Singleton UserRepository provideUserRepository(Context context) { return new RealmUserRepository(context); } }
これをApplicationレイヤーでInjectしてあげます。
public class UserService { private UserRepository repository; @Inject UserService(UserRepository repository) { repository = repository; } public User getUser() { return repository.getUser(); } }
このようにすればInfrastructureレイヤーの詳細な実装が他のレイヤーに漏れることはありません。
まとめ
現在の設計の状況をざっくり書いてみました。本当はサンプルを用意できればよかったのですが…
実際に試さないとわからない部分も多くあるので、最初は多少オーバーキル的な箇所があっても良いと思います。ただあまり時間をかけすぎるのも駄目なのでバランスを取りながらやっていくと良いと思います。
設計に正解はないと思っています。今の設計もまだまだ改善すべきところはあります。このあたりは随時改善していきたいです。