Subscribed unsubscribe Subscribe Subscribe

Augmented Usamimi

it { is_expected.to be_blog.written_by(izumin5210) }

Clean Architecture + DDD + Redux + RxJavaをAndroidでやるときにどこまで分割するか問題

Android ZATSU

(追記) 本記事,頭のなかを整理しきれていない状況で書いたためよくわからないことになっていますが,Clean ArchitectureやRedux,DDDの優位な点を解説するような記事ではないことをご了承いただけると幸いです.

全体の構成がどうなっているか・モチベーション・pros・cons等については後日別記事にまとめようと考えています.


いま書いているアプリがClean ArchitectureになりそこねたMVPと中途半端なDDDを組み合わせたようなアーキテクチャになっている. このアプリをある程度キチンとClean ArchitectureとDDDに寄せるにあたり,DDDのレイヤ分け(Data/Domain/Presentation)をどこまで厳格にやるかで悩んでいる.

現状

だいたいこんな感じ.

  • Data/Domain/Presentationのすみ分けはしていない
    • 実装上は意識してる
    • いまは大丈夫だけど,他の人が来た時に厄介かもしれない
    • どちらにしろちゃんとしたドキュメントあること前提
  • Presentation層からDomain/Data層へのアクセスはなるべくDroiduxStore経由
    • ReduxAndroid向け実装
    • Domain/Data層のデータの流れを一方向に制限したかった(unidirectional data flow: flux的な)
  • usecaseの代わりにactionDroiduxAction
    • Repositoryへのアクセスはここでやる
    • 本家Reduxと違ってActionはクラスなのでテストもしやすいしいいかな?という感じ(ActionCreatorが存在しない)
    • これからの「設計」の話をしよう』のUseCaseが近いか
  • activity/fragmentパッケージは作らない
    • Module/Component(DaggerのDIコンテナ単位)でパッケージを分けてる
    • Custom Viewsだけviewパッケージ切ってるのがちょっと違和感?
  • 画面(Activity/Fragment)ごとに3クラス2インタフェースほど余計に作ることになるのが面倒
    • Presenterに処理を集中させたら結構なメソッド数になりそうだったので.Clean Architectureにならってinput port/output portを分けてる
    • Controllerまで作るの冗長すぎる気がするし,よほどデカいモジュールじゃないかぎりはActivity/Fragmentに含めちゃっていい気もした

f:id:izumin5210:20160124200123p:plain

パッケージ構成は以下のとおり:

├action
├adapter
├api
├cache
├entity
│ └mapper
├model
├module
│ ├main
│ │ ├MainActivity.java
│ │ ├MainComponent.java
│ │ ├MainModule.java
│ │ ├MainController.java
│ │ ├MainPresenter.java
│ │ └MainView.java
│ └login
│ │ ├LoginActivity.java
│ │ ├LoginComponent.java
│ │ ├LoginModule.java
│ │ ├LoginController.java
│ │ ├LoginPresenter.java
│ │ └LoginView.java
├reducer
├repository
│ └datasource
├util
├view
├App.java
├AppComponent.java
├AppModule.java
└RootStore.java

現状だとData層は主にInfra層(データアクセス周り)を抽象化してるだけだなー.

Motivation

  • infra層(datasource)ではAndroid Frameworkが深く絡んでこないようにしたい
    • テスタビリティの問題
    • ただでさえ安定しないAndroidAPIのなかでも,とくにわけわからんことになってるBLE等は個別にテストしたい
    • infra層的には,自分がどのスレッド・どのライフライクルで実行されてるのかなんて知ったこっちゃない
    • 各層のinterfaceはとりあえずrx.Observable<T>返すようにしとけば実行スレッドは呼び出し側でなんとでもなる
  • このぐらい分割することができれば,infra以外のテストは「適切な対象に,適切にinteractionできるか」だけでよくなる
    • kyobashi.dex #2の資料に載せたコードみたいな感じ
    • DIの恩恵によりinteractionする先はすべてMock/Spyになる
    • mockへのテストばかりになってくるので不安にはなる
    • ややこしいUI周りのテストも「Mockが正しく呼ばれているか」だけになったら素敵じゃないです?

techbooster.fm #10では「Androidで分割するlogic/domainってどれだよ」「ぜんぶdomainじゃね?」みたいな話になってたけど, AndroidやフロントエンドJavaScriptの世界でなんらかの設計のフレームワークを持ち込むのって 「presentation/interaction logic・storeの状態遷移・infraを切り分ける」のが目的になってくるのかなと思った. そもそもフロントエンドじゃMVW(Model-View-Whatever)はスケールしないのでみたいな話も(ry.

特にBluetoothLeScannerとかBluetoothGattみたいな意味不明なクラス使うなら余計にinfra抽象化しないとテストできなくて死にそうな目に遭う.

Clean ArchitectureてFluxとどう違うねんみたいな話は『DDD - 持続可能な開発を目指す ~ ドメイン・ユースケース駆動(クリーンアーキテクチャ) + 単方向に制限した処理 + FRP - Qiita』に載ってた.

Fluxアーキテクチャとの違いは?

Fluxアーキテクチャの特徴は次の通りだ。ベストプラクティスとされるパターンに命名して確立しただけで、決して革新的なわけではない。

  • 全ての処理をイベント駆動に統一した。
  • メッセージングを単方向に制限した。
  • イベント同士の依存性を管理するのは、Dispatcherというハブが行う。

全体をイベント駆動にする場合は、Fluxアーキテクチャを参考にできる。また、メッセージングや処理を単方向に制限するのは有用だ。ストアへの処理の時系列を整理するハブについては、クリーンアーキテクチャにはUsecaseがあるし、これの方がビジネスロジックに直結していて分かりやすく、色んなインターフェース方式に対する処理を含む点で汎用的だと思う。

とりあえずそんなこんなでClean Architecture + DDD + Redux + RxJavaやってる.

改善案

package分割

AndroidではMVCよりMVPの方がいいかもしれない - Konifar's WIP』のサンプルアプリのパターン.

├data
│ ├cache
│ ├entity
│ │ └mapper
│ ├exception
│ ├executor
│ └repository
│
├domain
│ ├exception
│ ├executor
│ ├model
│ ├repository
│ └usecase
│
├presentation
│ ├di
│ │ ├component
│ │ └module
│ ├presenter
│ ├service
│ └view
│   ├activity
│   ├adapter
│   ├component
│   ├fragment
│   └util
└MainApplication.java

pros

  • package分けてるだけなのでコストは発生しない
  • (module分割に比べると)ファイルツリーがコンパクト
  • 1モジュールなのでdependenciesも単純

cons

  • package分けてるだけなので制約も発生しない
    • packageが違うので一応意識することはするか
    • Domain層にData層への参照等は起きうる

module分割

android10/Android-CleanArchitecture』が採用しているパターン. 基本的にはさっきのやつのdata/domain/presentationが別モジュールになってるだけ.

依存関係はこんな感じになる:

// data/build.gradle
dependencies {
  compile project(':domain')
  
  // snip
}
// presentation/build.gradle
dependencies {
  compile project(':domain')
  compile project(':data')
  
  // snip
}

また,Data moduleはAndroid Library,Domain moduleはJava Libraryになる.

pros

  • Data/Domain/Presentation間の依存に制約が発生する
    • 「Data -> Presentation」や「Domain -> それ以外」のような参照が発生しなくなる
    • 設計に対するきょうせいギプス的な効果
  • Domain層へのAndroidフレームワークの混入も防げる

cons

  • ファイルツリーがデカくなる
    • 新規作成するの大変
  • 各moduleの依存管理が大変
    • 何も考えずにやるとDon't Repeat Yourselfもクソもない感じになる
    • gradle/dependencies/dependencies.{gradle,properties}みたいなので共通化・集中管理?
    • 秘伝のgradleタスクになる恐れが
  • Domain層にAndroid Frameworkに依存するライブラリを含めることができない
    • 適切に設計されていれば発生することは少ないか…?
    • むしろいい制約になるという説はある
    • DataBinding系のAPIは含めたい気もする…

どうする?

「設計」って「縛りプレイ」じゃないとブレてくると思っているので,module分割方式はすごく魅力的. 一方で,module分割することで跳ね上がるプロジェクト全体の複雑性(主にdependenciesまわり)の問題やファイルツリー肥大化問題は避けられない感じもするので,どうしたものか…と悩んでる.

References

エリック・エヴァンスのドメイン駆動設計

エリック・エヴァンスのドメイン駆動設計

実践ドメイン駆動設計 (Object Oriented SELECTION)

実践ドメイン駆動設計 (Object Oriented SELECTION)

[改訂版]Android Studioではじめる 簡単Androidアプリ開発

[改訂版]Android Studioではじめる 簡単Androidアプリ開発