k-shinn’s 雑記

技術メモを書き溜められたら良いな

CleanArchtectureを読んでみたAndroidエンジニアの走り書き

名著と名高いClean Architecture 達人に学ぶソフトウェアの構造と設計をつい最近になって読了しました。

詳細な内容の纏め記事みたいなのは検索すれば先駆者の方々のモノがいくらでも出てきますので、ここではAndroidエンジニアである自分視点でためになった内容の抜粋や感想なんかを纏めてみようと思います。

まだこの本を読んでいない人、特に私と同じAndroidエンジニアの方の参考になればと思います。

(私の理解度が足りていない事や、わかり易さ重視であえてざっくり端折る表現を多用しているため、誤解を生むことも多々あると思います事を、ご了承ください)

CleanArchtectureとはなんなのか?

抜粋とは言いましたが、まず大前提な部分については少し纏めてみたいと思います。

私含め、CleanArchtectureを学んで見ようと思う動機の一つに、「よく見かける同心円状の図(下の図)は何を意味しているのか?」「名前付けや数も含めてアレに沿わなくては行けないのか?」と思うところがあると思います。

f:id:k-shinn:20210809153233j:plain
"Clean Coder Blog" より

端的に言うと、この図は「コンセプト図」であり、名前付けや数はその一例に過ぎません。そしてそのコンセプトこそ、この本で言う「CleanArchtecture」の内容そのものになります。

そのコンセプトとは、主に「関心事の分離」です。

このコンセプトは以下の依存性のルールを守ることにより達成されます。

  • 円の内側は、外側について何も知ってはいけない
  • 円の内側に向かうほど、抽象度と方針のレベルは高まる
  • 円の最も外側は、最下位レベルの具体的な詳細で構成される
  • 円の数はシステムによるが、上記のルールは常に適用される

(「方針≒目的→所謂ビジネスロジック」「詳細≒手段→フレームワークやDB、デバイス等」と考えると分かりやすいと思います)

これらを守り関心事を分離することで、柔軟性に優れたアーキテクチャが構成できると解説しています。

ルールの最後にある通り、円の数≒コンポーネントの数は一定ではありません。構築するシステムによって多くも少なくもなり得ますし、名前もこれに沿わなければいけない訳ではありません。

「CleanArchtecture」とは、具体的な仕組みのことではなく、この依存性のルールそのものであると理解できるわけです。

本書の内容構成について

まだこの本を読んでない人向けに解説すると、そもそもこの「CleanArchtecture」の同心円の図が本の中で出てくるのは、かなり後の方になります。この本の内容の殆どは、「CleanArchtecture」を理解・実践するために必要な要素、歴史的知見、数々のプログラミングの法則及びその実践テクニックになっています。 (これを念頭に置いておかないと、自分が何の本を読んでいるのか途中で見失いそうになります(なりました))

前述した「方針」と「詳細」についでも本の内容に含まれています。

そもそもこの「CleanArchtecture」のコンセプト自体については、著者ご本人が書かれたブログに存在しています。

blog.cleancoder.com

このコンセプトの知識を固めるための本ということになるわけです。

Androidの立ち位置

本書の内容は「アーキテクチャのルールはどれも同じである!」という謳い文句があるように、Android開発においても活かせるものです。

それどころか、「第29章 クリーン組込みアーキテクチャ」において、具体的に触れられている一文があります。

Androidアプリの開発者は、Android APIビジネスロジックを分離できていなければ、ファームウェアを書いていることになる。”

本章では「CleanArchtecture」の依存性のルールを組込みシステム向けに適用し、ハードウェア・ファームウェア・OSをビジネスロジックから切り離すことの重要性を解説しています。依存性のルールの円における外側にOS等が位置するわけです。(「OSは詳細」という分かりやすい小見出しまであります)

つまり、AndroidのシステムAPIも、CleanArchtectureの円の外側に位置するという事です。

Android API の関心分離の実践

では、これを実践するとどうなるだろうと考えてみました。

AndroidAPIとは、つまるところ android androidx 等のandroidパッケージの事です。Activity/Fragment,ViewModelなんかはそれそものですから、否応なしに依存したものになります。

依存性のルールに従うと、これを外側に位置させ、内側はこれを知らないようにしなければいけません。また、この境界線を越える処理は、インターフェイスを作成し、依存性を内側に入り込まないようにします。

…この方針は、Androidでよく見る、ViewModelからUseCaseをインターフェイス越しに呼ぶ形式そのものです。

簡易的に書くと以下のような形です。

class SampleViewModel() : ViewModel() {

    // 実装をクラスをInjectして使用する
    @Inject
    lateinit var useCase: SampleUseCaseInterface

    fun hoge(value: Int) {
        useCase.execute(value)
        ...
    }
    ...
}

呼び出されたUseCase…内側は、外側の事を知ってはいけません。 つまり、UseCaseはAndroidAPIはもとより、FragmentやViewModelの実装に依存しないようにすればよいわけです。

これはUseCaseへの引数として、ViewやViewModel、AndroidContextなどを渡さないようにする事で概ね達成されると思います。(勿論UseCase内で生成もしません)

分かりやすい指標として、UseCaseクラスのimportパッケージにこれら(円の外側の要素)が含まれていない事が挙げられてます。

package com.sample.domain.usecase

// Modelは円の内側である事が殆ど
import com.sample.model.Hogehoge
import com.sample.model.Fugafuga
...

interface SampleUseCaseInterface {
    fun execute(value: Int)
}

class SampleUseCaseImpl : SampleUseCaseInterface {
    override fun execute(value: Int) {
        val hogehoge = Hogehoge(value)
        ...
    }
}

これはCleanArchtectureを理解していなくても実践している方も多いと思います。なぜなら、AndroidパッケージやActivityを含まないクラスは、非常にテストが用意だからです。そしてこの「テスト容易性」も、「CleanArchtecture」 が実現する特性の一つです。

結局の所、「CleanArchtecture」が提唱する仕組みはAndroid開発にも大いに役立ち、また知らずしらずの内に実践している事が多いと思われます。

UseCaseの考え方の参考

前述のUseCaseクラスのimportパッケージで依存を考える方法ですが、数年前のDroidkaigiで正しくこの事を別視点から解説されている方が居りました。

ぼくのかんがえた最強のUsecaseの作り方~あるいはビジネスロジックとはなにかという1つの回答~ - Speaker Deck

CleanArchtectureを理解していなかった当時にこの発表を拝見していたのですが、今読み返してみますとより納得できる内容でした。機械的にではなく、よりドメイン的に納得して実現する参考になると思います。

Androidお馴染みのフレームワークへの適応

先程から述べている通り、AndroidAPIは「詳細(円の外側の要素)」の一つです。 「詳細」はこれ以外にも、DBやフレームワークなどの具体的な実装手段が含まれるとも言及しました。

これをAndroid開発的に考えてみると、非常に馴染み深い一例が出てきます。DeveloperガイドにもあるRepositoryパターンです。

f:id:k-shinn:20210809172145p:plain
"Android Developerガイド" より

用語や矢印のせいで少し混乱するかもしれませんが、CleanArchtectureとして整理するなら、「Room」や「Retrofit」はデータを取得する具体的な実装手段なので「詳細」になります。

RepositoryはRoom/Retrofitの実装を呼び元から隠蔽してくれます。 本来はViewModel~Repository間にUseCaseが存在していると考えると、内側であるUseCaseから、外側であるRoom/Retrofitへの依存を無くして(依存性逆転)くれている事になります。

Repositoryの存在によって、テストがしやすく、フレームワーク依存を他のコンポーネントに伝播させないようにできる事は、このパターンを実装したことがある人には実感できることかと思います。

「テスト容易性」は前述のとおりですが、「フレームワーク非依存性」も、「CleanArchtecture」が実現する特性です。

「CleanArchtecture」はコンセプトであり、あらゆるアーキテクチャに適用できる/されているルールであると本では述べています。つまり、ここにもそのルールが適用されているという訳です。

色々な「詳細」の分離

これはDBやRepositoryに限ったことではありません。

Android開発では、様々なサードパーティ製のライブラリに頼る事が多々あると思います。そして、この依存性に…例えばライブラリの破壊的なアップデートによる影響修正に…悩まされる事は多いのではないでしょうか。

このライブラリを「詳細」であると捉えると、これを分離し、非依存性を担保する道筋が見えてきます。簡単にはインターフェイスを作成し、内側と捉えたコンポーネントが、この「詳細」のパッケージに依存しなければ良いのです。

…勿論、そんな簡単に何もかも分離できるわけでは無いと思います。特にView周りなんてライブラリまみれになること必至だと思います。それでも、影響範囲を少なくするための道筋にはなると思います。

円の内側について考える

「CleanArchtecture」における円の最も内側は「エンティティ」とされています。これは本の内容的には「最重要なビジネスデータを操作するビジネスルール」とされています。とてもざっくり言うと、「お金を払ってマンガが読める」くらいざっくり感の「システム実装に寄らない価値」と、「顧客とマンガと料金表」くらいざっくり感の「価値を実現するための必要なデータ群」です。

そして、この「エンティティ」の一つ外側にあるのが「ユースケース」…本の内容的には、「エンティティ(ビジネスルール)を実現するためのアプリケーション特有のルール」です。

これらは殆どAndroid実装に転用できる概念だと思います。

どんなシステムにもコアとなるデータクラスは存在します。ユーザデータや料金データ、マンガサービスなら画像データ、動画サービスなら動画データなどです。当たり前ですが、これらのデータはそれ単体は何にも依存しません。(他のデータを内包している等は別として)。これらはそのまま「エンティティ」の概念であると言えます。

では「ユースケース」はどうかと考えると、「お金を計算・消費し、画像を取得して返す」といった処理の実装が、そのまま「エンティティ(ビジネスルール)を実現したユースケース」と言えるでしょう。そしてこれらは、アプリ実装ではUseCase層やDomain層等と呼ばれる、FragmentやViewModelから呼ばれ、Repositoryなどを呼び出している層のことであると言えます。(殆ど場合、そのまま「ユースケース」だと思います)

そしてこの時、依存性のルールに則り更に外側への依存を排することで、柔軟でクリーンなアーキテクチャが実現できるという事になります。

依存性ルールに反する例

逆に、外側への依存を許してしまう瞬間とはどういう場合があるでしょう。

一つに、前述したUseCaseへの引数にFragmentやViewModelなど、Androidフレームワークの要素を含めてしまうことが考えられます。これは、処理の分割を適切に行えば解決できる場合が多いかと思います。

他にパッと思いつく…私自身やらかしていたことがある…例としては、Roomのデータ都合・依存をエンティティ(ここでは円の最も内側に位置するデータの意味)に含めてしまう事が挙げられます。

具体的には、Roomに保存するデータと、APIから取得するデータ形式と、UseCaseやViewとやり取りするデータ形式を一つのデータクラスとして扱っていた事です。

不都合なくこれで動いている間は問題に気付きにくいのですが、サーバ都合でデータの一部形式が変更された時、View都合で一時的なデータを増やした時、何れの場合にもRoomのマイグレーションが必須になってしまい、保守に手間が掛かり、バグの温床にもなってしまった事です。

「エンティティ」のデータはデータベースの都合とは無縁であるべきであり、 データベースの都合は他のコンポーネントは無縁であるべきという良い(悪い)例でした。

境界線の超え方

こういった問題についても、本書では道筋を示してくれています。 曰く、「境界線を越えるデータは、独立した単純なデータ構造にすべき」「常に内側の円にとって便利な形式にすべき」との事です。

これの単純な実践としては、前述の例では、多少手間でもRoomのデータと「エンティティ」としてのデータは別で定義し、常に変換を挟んで使用する事が考えられます。

マルチモジュールでの実践

Android開発ではここ数年でマルチモジュール構成にすることが特に推進されています。依存性の排除や整理の観点でみても、マルチモジュール構成にすることは非常に有用です。…というより、マルチモジュールにしたい理由の大部分にこれが挙げられると思います。

メインとなるモジュール(デフォルトではappモジュール)の依存を整理するのは非常に難しいですが、Room等の外側のモジュール、データとしてのエンティティ・ユースケースである内側のモジュールから整理していく事で、自ずとクリーンなアーキテクチャになっていくと思います。

(メインモジュールに関しての考え方も本にはありますが、ここでは割愛します)

まとめ

「CleanArchtecture」の本には、Android開発にも非常に役立つ、多くの知見が纏まっています。ここに抜粋したようなアーキテクチャの例や知見は内容のほんの一部であり、アーキテクチャを維持する・崩れそうになるサインを見極めるための知見、守るべき数々の法則等、様々な要素が載っています。

まだ読んだことがなく、「アーキテクチャはどう考えれば良いんだろう?」と悩んでいる方は、是非一読してみることをお勧めします。

参考