dely tech blog

クラシル・TRILLを運営するdely株式会社の開発ブログです

クラシルAndroidはなぜRepositoryを採用しなかったのか

f:id:meilcli:20220310181730j:plain

こんにちは、クラシルAndroidエンジニアの@MeilCliです。先日Androidチームで設計についてお互いの認識を合わせ、今後のクラシルAndroidのアーキテクチャー設計をどうするか決めたので共有します

基本的な考えについてはテックリードのうめもりさんが書いた記事にありますのでよかったら読んでください*1

tech.dely.jp

レイヤー構成

f:id:meilcli:20220310165909p:plain
レイヤー構成

クラシルAndroidには3つのレイヤーが存在します

  • UI Layer
    • Viewの描画・ユーザ操作のハンドリング・ViewにまつわるStateの管理
  • Domain Layer
    • データの加工・UI Layerへの公開
  • Data Layer
    • データ操作を提供

それぞれのレイヤーにおいて複数のクラスが関わってきますが、今の所Domain LayerにはUseCase、Data Layerにはプリミティブなデータ操作を提供するDataSourceしか定義されていません。先日うめもりさんが書いた記事やGoogleのドキュメントにはRepositoryの存在が予見されていました。しかし、チームでの話し合いの元、今はUseCaseとDataSourceの中間の存在を定義しないという結論になりました

要約すると以下の感じです

  • JSON色付け係のようになっている今のクラシルAndroidでは、RepositoryがただDataSourceのメンバーを公開するだけの存在になってしまう
  • Data LayerとDomain Layerで利用するEntityを別々にし、変換を挟むというのであればRepositoryのような中間の存在の必要性が出てくるが、現状のクラシルAndroidはそれを必須にして行う規模感ではない
  • DataSourceの安全なアクセスを提供するための中間の存在にはメリットもあるが、UseCaseがそれの代替を行うことが可能
  • 仮に変換ロジックを挟むなどの場面が出たとしても、それはRepositoryという名称で行わずにもっと狭い名前で行った方が良い
  • 現状では中間の存在を切り出すほどの場面がないので必要に応じて定義する

f:id:meilcli:20220310171543p:plain
事実上のレイヤー構成

そのため、事実上は画像のような構成になっているという解釈で問題ありません

UI Layer

UI LayerはJetpack Composeにおける役割とほとんど変わらないと思います

  • Viewを描画する
  • ViewのStateを管理する
  • 画面遷移などの遷移処理
  • Domain Layerとの通信

UI Layerは主にこれらの役割を担っています。また、現在はAndroidViewを利用する実装ですが、将来的にJetpack Composeを利用する形に変更していきたいという思惑があります。そのため今は移行期間であり、UIの書き方を少しずつ変えていっている段階なので具体的なコードは省かせていただきます

なんにしろ、UI LayerはDomain Layerから受け取ったデータをただ表示するだけという形を目指していくことになります

Data Layer(DataSource)

Domain Layerの前にData Layerの説明を行います

DataSourceには様々な種類に対応する名前が用意されています。そのためコード上ではDataSourceという名前ではなくそれぞれの名前が用いられています

  • Rest Client: サーバ側との通信を行う
  • Preferences: SharedPreferencesへの保存を行う
  • Db: SQLiteへの保存を行う
  • Cache: In-memory cacheを行う
  • etc...

これらのDataSourceの役割は非常にシンプルです

  • プリミティブなData functionを提供
  • プリミティブなData functionの整合性を保証

プリミティブなData functionというのはバックエンド側の用語で言うところのCRUD(Create, Read, Update, Delete)のような単純な操作のことを指しています。そしてそれらの操作の整合性(たとえばThread safe)を保証するのもDataSourceの役割です

たとえばですが、In-memory cacheにuserを保管するとしたら以下のような感じになります

class ExampleUserCache {

    // 実際にはthread safeな実装にする
    private val users = mutableListOf<User>()

    // クラスの責務内のデータ群に関する単一の操作(get, create, update, delete, etc...)関数を提供する
    fun addUser(user: User)

    fun removeUser(user: User)

    fun getUsers(): List<User>
}

Domain Layer(UseCase)

最後にDomain Layerです。なぜ最後に説明を持ってきたのかというと、実際のチームでの話し合いにおいてもDomain Layerで何をすべきなのかが決めにくかったためです。そこで、まず最初にData Layerを決め、次にUI Layerを決め、最後にDomain Layerについて話すことにしました。そうすることでData LayerとUI Layerで行わないことすべてがDomain Layerで行うことという考えができたためです*2

そして、アプリケーション開発のコーディングにおいて必要な部分の残り物がUseCaseの役割となったわけですが、おおまかに以下のものになりました

  • Data Layerからデータを取得・操作する
  • データを組み合わせ・加工・フィルタリング・バリデーションする
  • Domain Layerの操作を組み合わせる
  • データをUI Layerが見えるかたち(見ても良いかたち)に変換する
  • UI Layerにバリデーションロジックを公開する

一方で、特定の画面でしか使わないような処理の命名難しいよねという問題もありました。理想的には画面によらない名前付けをしたいところですが、我々は未来を見通す力を完全に持ち合わせているわけではないので、その場で将来に渡って適切な名前を付けることは非常に難しいです。そのため特定の画面でしか使わないような処理には画面名を付けても良いということになりました

具体的には以下の運用です

  • 画面によらない共通処理: UseCase
  • 画面固有の処理: ScreenUseCase
    • 複数の画面で使い回すときにはScreenUseCaseのまま使わずにRenameを行う

一方で、悪く言うと余り物で構成された役割をUseCaseが持ってしまっているので、いつかのタイミングで、処理がFatになってるよね・この処理を設計レベルで切り離せるよねということがあるんじゃないかなと思います。前述のRepositoryの話のように、そういうタイミングが訪れればあらたな存在・役割を定義するということになると思います

終わりに

クラシルAndroidはリライトしてから約1年が経ちました。最初の1年はざっくりとした感覚でそれぞれがコーディングしていたので、設計がバラバラであったり、コーディング手法が確立されていなかった箇所で記述が複雑になっていたりしました

また、レシピカードや今後登場する新保存機能においてこの新しいアーキテクチャー設計に沿った実装を実際に行ってみました。感触としてはUseCaseを適切に分割できていればFatな存在になってしまうのは避けれそうという感じでした。今後はこれからの設計が確立したのでそれに沿った開発を行っていく所存です

*1:続きは催促しておきます

*2:はさみうちの原理に似てますね