dely Tech Blog

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

Guide to "kurashiru android" app architecture vol.1 概要編

はじめに

こんにちは。クラシルのAndroidアプリチームのテックリードのうめもりです。

android-developers.googleblog.com

12/14に新しいアプリアーキテクチャガイドがAndroid公式からアナウンスされました。読まれた方もいらっしゃると思いますが、非常によくまとまったアーキテクチャガイドであり、新しくアプリを作る際も、既存のアプリのアーキテクチャを整理する際にも役に立ちそうな文章です。

クラシルのAndroidチームは去年の2月にAndroidアプリをリアーキテクチャしたのですが、そのアーキテクチャがアプリアーキテクチャガイドと似通った個所が多く、クラシルのアプリアーキテクチャを説明するのにもちょうど良さそうな文章だと思いました。

ですので、今回は新しいアプリアーキテクチャガイドとほとんど同じ構成で、クラシルのアプリアーキテクチャについて解説してみようと思います。なお、元の文章は一般的なアーキテクチャについてのTipsや考え方について説明している個所もあり、そういった個所については記述を割愛しますので、是非元の文章を読んでみてください。

この記事のタイトルは、アプリアーキテクチャガイドの Guide to app architecture から拝借して Guide to "kurashiru android" app architecture とさせていただいてます。

なお、分量が多いため、今回は概要の部分を公開します。vol.2, vol.3ではより詳しいレイヤーの話をしていく予定です。(この構成もGuide to app architectureに倣ってます)

余談

どうせ日本語版はすぐ用意されないだろう…と思って英語版を頑張って翻訳しながらブログを書いていたら、年が明けたら日本語版が公開されていました。悔しかったので自家翻訳版のまま公開しており、日本語版とはニュアンスが違う部分が多々あるかもしれません。

概要

クラシルのアプリアーキテクチャ

クラシルは、Android公式の推奨されたベストプラクティス https://developer.android.com/jetpack/guide#common-principles に従ってアプリを構成しています。

そして、クラシルは3つのレイヤーでアプリケーションを構成しています。

  • アプリケーションデータを画面に表示するためのUI layer
  • ビジネスロジックを内包し、アプリケーションデータを他の層に公開するためのData layer
  • Data layerをUI layerから隠蔽し、複雑なビジネスロジックを内包するためのDomain layer

f:id:delyumemori:20220104113259p:plain

そして、UI layerとDomain layer+Data layerは明確に別のモジュールに分離されています。Domain layerとData layerを別のモジュールにするかは、将来的なアプリケーションの拡張次第ですが、今のところ分離する予定はありません。

UI layer

UI layerの役割はアプリケーションのデータを画面に表示することです。ユーザーによる操作(例えばボタンのタップ)や外部的な入力(例えばネットワークレスポンス)などでデータが変わるたびに、UIはデータの変化を反映します。

UI layerは二つの要素で成り立っています。

  • データを画面にレンダリングするためのUI要素。クラシルではまだJetpack Composeは導入されていないので、Android Viewを使っています。
  • データを保持するためのState holder。クラシルではMVIアーキテクチャを採用し、Android ViewからUIイベントを発行するためのIntentUI StateをAndroid Viewに反映するためのViewUIイベントや外部的な入力をハンドリングし、Stateに反映するためのModelの三つの部分でState holderが出来ています。

f:id:delyumemori:20220104152559p:plain

去年のAdvent Calendarでクラシル版MVIアーキテクチャに触れているので、ご興味ある方はぜひこちらをご覧ください。

tech.dely.jp

Data layer

Data layerはビジネスロジックを内包します。ビジネスロジックはクラシルのアプリに価値(value)をもたらすものであり、アプリがどのようにデータを作成し、保存し、変更するかを決定するルールで構成されています

Data layerはRepositoryで構成されており、それぞれが0から複数のDataSourceを含むことが出来ます。クラシルで扱うデータの種類ごとにRepository classを作成する必要があります。例えば、Recipeに関連するデータには RecipesRepository classを、Userに関連するデータには UsersRepository classを作成するとよいでしょう。

f:id:delyumemori:20220104162511p:plain

Repository classは以下のような役割を担っています。

  • データをリポジトリーの外に公開する。
  • データの変更を一か所に集約する。
  • 複数のデータソース間の競合を解決する。
  • リポジトリーの外からデータのソースを隠蔽し、抽象化する。
  • ビジネスロジックを内包する。

各DataSource classにはファイル、ネットワーク、ローカルデータベースなど、1つのデータソースのみを扱う責務を持たせるべきです。DataSource classは、アプリケーションとシステム間のデータ操作の橋渡しをします。

Domain layer

Domain layerはUI layerとData layerの中間のレイヤーです。

Domain layerは複雑なビジネスロジックや、複数のUI Modelで再利用されるシンプルなビジネスロジックをカプセル化する役割を果たします。クラシルではさらにData layerをUI layerから隠蔽する役割を担っており。オプションではなくUI layerは必ずDomain layerを通してData layerへアクセスします。これはデータへのアクセスのやり方を統一し、アプリケーション全体の依存関係をシンプルに保つための意思決定としてそれを行っています。

f:id:delyumemori:20220104163227p:plain

クラシルでは、モジュールごとに単一、または複数のファサードインターフェースとしてFeature接尾辞を持つインターフェースをDomain layerからUI layerに公開し、それを通してDomain layerの機能にアクセスします。

モジュールごとのファサードの実装が大きく、複雑になりそうな場合は、Interactorクラスに処理を分割することを検討します。(その場合、単一のInteractorクラスは単一の責務を持つことが好ましいです。)

コンポーネント間の依存関係を管理する

クラシルではDependency injection (DI) パターンを使ってクラス間の依存関係を整理しています。クラスのオブジェクトインスタンスの生成やライフサイクルの管理はDIコンテナに依存出来るため、実装者は依存関係のオーケストレーションの手間のことを考えずに適切なインターフェース、クラス設計に専念すべきです。また、テスト用の実装の差し替えなどはDIコンテナの機能を活用して行うことを推奨します。

クラシルでは、 https://github.com/stephanenicolas/toothpick を使っています。ToothpickはJSR-330( https://jcp.org/en/jsr/detail?id=330 )の仕様を満たしており、Dependency Injectionの機能的には必要十分ですし、今後これ以上複雑な機能をもったDIライブラリーに移行する予定はありませんが、KSP等のビルド時間が高速になることが期待できる仕組みを利用したスタンダードなDependency Injectionのライブラリーがリリースされた場合は、そちらに移行していく可能性があります。

クラシルの設計プラクティス

クラシルはコードベースの堅牢性、保守性、テスタビリティを高めるため、なるべく以下のようなプラクティスに従っています。

UI Componentのライフサイクルよりも短命な領域にデータソースを置かない

UI ComponentのModelオブジェクトや、その他インスタンスフィールドをデータソースとして指定するのは避けてください。Domain layer及び、各Componentのシリアライズ可能なStateをデータソースとすべきです。

Android framework SDK APIへの依存を局所的にする

クラシルのアプリコンポーネントの中で、Android framework SDK APIに依存する個所は局所的にし、それらのコンポーネントへのアクセスは抽象インターフェースや、抽象メソッドを通して行うこととします。それによってテストのしやすさを高め、Android SDKへの結合度を下げることが出来ます。

各モジュール間のアクセスは抽象度の高いインターフェースを通して行うようにする

モジュールの具体的なクラスに対して直接アクセスするショートカットを作りたくなるかもしれませんが、モジュール同士が密結合することは、コードベースの進化に伴った技術的負債の発生につながりやすくなります。

クラシルが提供すべき価値に焦点を当てるために、ボイラープレートコードを削減する

可能であればJetpackなどのスタンダードなライブラリーや、その他ボイラープレートコードを削減できるライブラリーの活用を検討し、ビジネスロジックやUIロジックの実装にエンジニアが集中できるようにします。

おわりに

実は、今回この記事を書くにあたって、既存のクラシルのアーキテクチャをAndroid公式のものに沿っていくつか再解釈したところがあります。今まで作ってきたアプリケーションの構造としては変わらないものの、今まで明確にそういった役割を持たせていなかった部分に対して名前を付け、整理するのに、アプリアーキテクチャガイドはとても役に立ちました。皆さんも是非、Guide to app architectureを使って自分たちのアーキテクチャを見直してみてはいかがでしょうか。

クラシルのAndroidチームでは、生産性の高いアーキテクチャを維持・アップデートしつつも楽しくプロダクト開発できる人、したい人を求めています。 今回の記事を読んで興味が出てきた方、是非お話ししましょう! 下記の採用リンクから応募いただいてもOKですし、Twitterから気軽にDM頂けてもOKです!

dely.jp

twitter.com