Edited at

ようやく Android アーキテクチャのベタープラクティスっぽいものが見えてきたかもしれない

More than 1 year has passed since last update.

こんにちは。Sansan Android エンジニアの @rockwillj です。


はじめに

もともと Java エンジニアな私が Android 未経験で Sansan に入社してからおよそ1年半が経ちました。

2年以上アプリ開発をしている人からしたら、まだまだ圧倒的に経験は足りてないですが、ようやく Android アーキテクチャについて自分なりのベタープラクティスっぽいものが見えてきた気がします。ただの気のせいかもしれないけど、もしかしたら共感してくれる人もいるのではと思い、今の自分の考えを軽くまとめておこうと思います。

※ 注:理想的なアーキテクチャ論を語りたいのではなく、現実的なベタープラクティスについて話せればと思います。色々と突っ込みどころもあるでしょうが、できれば優しい心で読んでいただけると助かります :sweat_smile:


前提条件

どのような開発体制でどのようなアプリを作るかで適切なアーキテクチャは変わってきます。という訳で、私が今関わっているプロジェクト…じゃなくて、私がベタープラクティスとして考えるアーキテクチャで想定されるのは次のようなプロジェクトです。


  • 少人数の開発チーム (1~2人)

  • 小規模なアプリ (頑張れば1人でも開発できる)

  • 仕様が超流動的 (PM/PO レベルでもなかなか仕様が決まらない)

  • リリース日が決まっている (開発スピードが求められる)

もし上記の条件に合わないのであれば、言い換えると「ある程度仕様が固まっている大規模なアプリを多人数チームで時間をかけて開発できる」のであれば、個人的には Clean Architecture を採用し、レイヤー間の依存性をなくして試験性 (テスタビリティ) を確保するのがベストソリューションではないかと思います。


設計思想

ベタープラクティスなアーキテクチャに適用される設計思想です。私のプログラマーとしての性格や思想が多分に含まれてます。この設計思想と前述の前提条件を組み合わせると、望ましいアーキテクチャのおおよその形が決まってくるのではないでしょうか。


  • 何より保守性を重視する


    • 拡張性、再利用性、移植性、試験性、効率性…など、様々なソフトウェ品質要因がありどれも重要だが、その中でも保守性を最も重視する


      • 使用性 (ユーザビリティ) については開発に閉じる話ではないので、ここでは考えない



    • 例え優れたアーキテクチャであっても、ルールが多かったり説明が容易ではなかったりで、実装コストが高くなりそうであれば採用しない



  • アーキテクチャは手段であって目的ではない


    • 保守性を確保するための手段としてアーキテクチャを導入するのであって、アーキテクチャを導入することそれ自体が目的ではない


      • アーキテクチャのルールに厳格に従うことで逆に保守性が損なわれるのであれば、ルールを緩和したり例外を認めたりしても構わない





  • 依存性排除 (疎結合) を意識はするが徹底はしない


    • 依存性排除を徹底しようとすると、View ごとにインターフェースを切り、アクション (タスク) ごとにクラスを作り、Android 特有の概念を抽象化するなど、ファイル数やボイラープレートが増えてしまってそれはそれで大変なので、ある程度の依存関係は許容する

    • インターフェースやクラス数が増えると、実際の処理が書かれている場所までが遠かったり細切れっだったりして、あちこちジャンプしながらコードを読むことになり可読性が落ちる恐れがある


      • あまり階層を深くせずにクラス数も増やさないために、ドメイン境界ごとにパッケージやクラスを用意する



    • 簡略化のためインターフェースでやり取りはしないが、レイヤーごとに実装は分けておく


      • ただし「下位レイヤーは上位レイヤーを知らず、上位レイヤーのみ下位レイヤーを知っている」というルールは守る





  • 試験性 (テスタビリティ) を意識はするが徹底はしない


    • 前述の前提条件にあるように仕様が超流動的なため、ユニットテストは必要十分な量と質に留めておく

    • テストを軽視している訳ではなく、あくまでテストを書くことが高コスト (無駄になる) という優先度的な判断


      • テストケースを書けば仕様が厳密化され、バグも減って品質が上がり、TDD だと開発のリズムが出て楽しいし、グリーンランプが灯るのは自己満足感あるけど、超流動的な仕様のテストってどうやって書くの?という話

      • 例えばテストを書くために仕様を固めようとすると、スコープが拡散していく一方で収束することはなく、次の日には仕様が大きく変わったり、あるいはその仕様自体がなくなったり…(略





  • 思想が合わなくても柔軟に対応する


    • 思想が合わなくて意見が対立してしまうこともあるでしょう…。けど突き詰めると結局はどちらでもよかったりするので、早々に妥協して開発に専念しちゃいましょう :innocent:




Lazy Architecture

私がベタープラクティスとして考える〜と、毎回言うのも面倒なので便宜的に名前を付けました。主要なアーキテクチャ (MVP, MVVM, Clean Architecture) と比べると、いろいろなところで怠けているので Lazy Architecture と呼ぶことにします。プログラマーの三大美徳ですね(違う


登場人物

Lazy Architecture の登場人物は次のとおりです。基本的には Clean Architecture をベースに、いくつかのルールを緩和して簡略化し、時には例外も許容される MVP パターンかなと思ってます。


  • Entity


    • モデル (API のレスポンスやデータベースのエンティティを表す)

    • 簡略化のため他の層では Entity のまま扱う


      • Entity が持つべきでない状態やロジックが必要なときは別途 Model を用意する





  • View


    • ビュー (Activity, Fragment, CustomView)

    • なるべくロジックは書かない (Presenter に任せる)



  • Presenter


    • プレゼンテーション層

    • ビューロジックを扱う

    • ビジネスロジックは扱わない (UseCase に任せる)



  • UseCase


    • ドメイン層

    • ビジネスロジックを扱う

    • データソースは意識しない (Repository に任せる)



  • Repository


    • データ層

    • データの CRUD を扱う (API, DB, SharedPreferences, Cache など)

    • 簡略化のため DataStore の役割も兼ねる


      • 複数のデータソースがある場合でも DataStore を作らない (作ってもよい)






パッケージ構成

例えばユーザ一覧を取得するサンプルアプリを Lazy Architecture で作る、とした場合の最小パッケージ構成を考えてみました。Clean Architecture のパッケージ構成を一部参考にしています。

ちょっと縦に長くなっていますが、このあとトップレベルのパッケージ単位で軽く解説しています。もっとこうした構成のほうがいいのでは?という指摘があれば、ぜひぜひコメントください。

├ data

│ ├ network
│ │ └ UserApi.java (interface)
│ └ repository
│ └ UserRepository.java

├ di
│ ├ ApiModule.java
│ ├ ApplicationModule.java
│ ├ RepositoryModule.java
│ ├ SampleComponent.java
│ └ UseCaseModule.java

├ domain
│ ├ value
│ │ └ UserType.java (enum)
│ ├ entity
│ │ └ User.java
│ ├ model
│ │ └ UserModel.java
│ └ usecase
│ └ UserUseCase.java

├ presentation
│ ├ common
│ │ ├ adapter
│ │ │ ├ EmptyAdapter.java
│ │ │ └ ProgressAdapter.java
│ │ ├ dialog
│ │ │ ├ AlertDialogFragment.java
│ │ │ └ ProgressDialogFragment.java
│ │ ├ view
│ │ │ └ CustomView.java
│ │ └ viewholder
│ │ ├ EmptyViewHolder.java
│ │ └ SubHeaderViewHolder.java
│ ├ main
│ │ ├ MainActivity.java
│ │ └ MainPresenter.java
│ └ user
│ ├ UserActivity.java
│ ├ UserAdapter.java
│ ├ UserFragment.java
│ ├ UserPresenter.java
│ ├ UserView.java
│ └ UserViewHolder.java

├ utility
│ ├ DateUtils.java (DateExtension.kt)
│ ├ PreferencesUtils.java (PreferencesExtension.kt)
│ ├ RxUtils.java (RxExtension.kt)
│ └ ViewUtils.java (ViewExtension.kt)

└ SampleApplication.java


data パッケージ

├ data

│ ├ network
│ │ └ UserApi.java (interface)
│ └ repository
│ └ UserRepository.java


  • network


    • API を定義したインターフェースを配置する


      • アノテーションにより Retrofit で実装クラスを自動生成する





  • repository


    • API, DB, SharedPreferences, Cache などの CRUD を実装した Repository を配置する


      • 簡略化のため複数のデータソースを扱う場合でも DataStore は作らない (作ってもよい)

      • 純粋な CRUD メソッドだけだと使いにくいので Repository インターフェースは切らない



    • API, Realm, Sharedpreferences などが DI される




di パッケージ

├ di

│ ├ ApiModule.java
│ ├ ApplicationModule.java
│ ├ MyComponent.java
│ ├ RepositoryModule.java
│ └ UseCaseModule.java


  • DI (依存性注入) 用のパッケージで Dagger 関連クラス (Module, Component) を配置する


    • 新たな依存関係を定義するときに関連クラスをいじるのが意外と手間なのでまとめておく



  • Application (Context), API, UseCase, Presenter, Repository の provide メソッドを定義する


    • Realm, SharedPreferences, Glide, Gson, OkHttpClient, Retrofit などのインスタンス生成もここで行う



  • 中規模アプリの場合は Component を Activity, Fragment, Service, Presenter などの単位で分割してもいいかも


domain パッケージ

├ domain

│ ├ value
│ │ └ UserType.java (enum)
│ ├ entity
│ │ └ User.java
│ ├ model
│ │ └ UserModel.java
│ └ usecase
│ └ UserUseCase.java


  • value


    • API レスポンスなどで用いられる定数を表す列挙型 (enum) を配置する



  • entity


    • API のレスポンスやデータベースのエンティティを表す Entity (POJO) を配置する


      • Serializable (Parcelable) になるようなプリミティブさを持たせる

      • Kotlin の場合はデータクラス (data class) で定義する





  • model


    • Entity が持つべきでない状態やロジックが必要なときに用いる Model を配置する

    • 便宜的に entity と並べている (presentation パッケージにあるほうが適切)



  • usecase


    • ビジネスロジック (ドメインロジック) が書かれた UseCase を配置する


      • 簡略化のため UseCase のインターフェースは切らない



    • Repository, API が DI される


      • API ごとに Repository を作るのは面倒なので、データソースが API しかない場合は例外的に API を使ってよいことにする

      • 基本的には Pure Java にして Android の概念とは切り離す (ただし Context は例外とする)



    • 非同期処理のスレッドを決定する


      • Rx であれば .subscribeOn().observeOn に指定する Scheduler を決める

      • Retrofit であれば API 呼び出しとそのコールバックのスレッドを CallAdapter で決め打ちしてもいいかも






presentation パッケージ

├ presentation

│ ├ common
│ │ ├ adapter
│ │ │ ├ EmptyListAdapter.java
│ │ │ └ ProgressListAdapter.java
│ │ ├ dialog
│ │ │ ├ AlertDialogFragment.java
│ │ │ └ ProgressDialogFragment.java
│ │ ├ view
│ │ │ └ CustomView.java
│ │ └ viewholder
│ │ ├ EmptyViewHolder.java
│ │ └ SubHeaderViewHolder.java
│ ├ main
│ │ ├ MainActivity.java
│ │ └ MainPresenter.java
│ └ user
│ ├ UserActivity.java
│ ├ UserAdapter.java
│ ├ UserFragment.java
│ ├ UserPresenter.java
│ ├ UserView.java
│ └ UserViewHolder.java



  • userlogin などドメイン境界ごとにパッケージを切る


    • 階層を深くせずに同じドメイン境界のクラスをまとめるため



  • Activity, Fragment, CustomView などの View を配置する


    • 簡略化のため View ごとにインターフェースは切らない

    • Presenter が DI される



  • View と UseCase との橋渡し役となる Presenter を配置する


    • 簡略化のため Presenter のインターフェースは切らない

    • View とはインターフェースでやり取りしない (ただし View 本来のメソッドは呼ばない)

    • UseCase が DI される

    • View とライフサイクルを合わせる


      • Rx であれば複数の通知 (Subscription/Disposable) を管理して適切なタイミングで購読解除する






utility パッケージ

├ utility

│ ├ DateUtils.java (DateExtension.kt)
│ ├ PreferencesUtils.java (PreferencesExtension.kt)
│ ├ RxUtils.java (RxExtension.kt)
│ └ ViewUtils.java (ViewExtension.kt)


  • グローバルスコープな Utility クラスを配置する


    • Kotlin の場合は Extension ファイルを配置する



  • チーム内で共有しやすくするためトップレベルのパッケージとする


    • 既に実装された処理を知らずに、重複して実装してしまうという二度手間を防ぐため




ライブラリー構成

サンプルアプリで使用する (予定の) ライブラリー構成です。Lazy Architecture は、以下のライブラリーを組み込むことを前提に設計された現実的なアーキテクチャとなります。ライブラリーをあらかじめ考慮に入れておかないと RealmObject や Glide の ImageView 指定などで立ち止まってしまうことになるので。


  • Kotlin

  • Dagger

  • RxJava

  • Realm (or Orma)

  • OkHttp

  • Retrofit

  • Gson

  • Glide (or Picasso)

以下のライブラリーは途中まで導入していましたが、メリットとデメリットを天秤にかけて現時点では採用を見送りました。


  • DataBinding


    • Binding クラスが各クラスに入り込む (DataBinding からオプトアウトしにくい)

    • XML に色々書くとコードが分散して可読性が落ちる

    • XML 記述時にコード補完などの IDE 支援機能が不十分

    • エラーが分かりにくい (問題解決にかかる時間がもったいない)

    • ビルド時間短縮のため



  • ButterKnife


    • Kotlin の by lazy {} やラムダ式で比較的簡潔に書けるので

    • ビルド時間短縮のため




参考にしたページ


おわりに

私が今関わっているプロジェクトでは実際に Lazy Architecture で開発を進めています。とはいえ Lazy Architecture はまだ叩き台の段階ですので、日々試行錯誤を重ねながら、より実用的なアーキテクチャとなるよう改善を図っています。

需要があるのか不明ですが、当初はプロダクトコードへの組み込み方が分かるようなサンプルアプリを作る予定でした。ただ、なかなかリソースが確保できずに、諸事情によりまずはソースコードなしのままの記事となりました。このアーキテクチャが本当に実用に耐えるかどうかは分かりませんが、より良いプラクティスに気づくきっかけにでもなれたら嬉しいです。

本当は DDD や UCDD, Clean Architecture などをきちんと理解してから考えたほうがいいのですが、来週あたりに「ベタープラクティスが見えてきたかもしれないと言ったがあれはただの気のせいだった」という記事を書いてないことを祈ってます :pray:

それでは、ここまでお読みいただきありがとうございました :bow: