はじめに
昨年からの大きな案件でClean Architectureを使った
- Platforms: Android/iOS
- Languages: Kotlin/Swift
はじめに
勉強会向け資料なので、クリーンアーキテクチャー自体の解説もある程度含まれます。
逆に、時間の都合上、歴史背景や細かい部分までは行き届いていません。
もし間違いがあればご指摘ください。
オススメ書籍
アーキテクチャーを選定する目的
- 求められるシステムを構築・保守するために必要な人材を最小限に抑えるため
- 「アーキテクチャーは上位レベル、設計は下位レベル」のように区別されることがあるが、両者の間に明確な境界はなく、上位から下位に至るまで、決定の連続である
スマホアプリ開発で代表的なアーキテクチャー
- AndroidはMVVM(Googleが推奨)
- iOSはMVC(AppleがCocoa applicationに採用)
では、Android、iOS両方作る場合はどうする?
ロジックは極力共通化したい
- Android↔︎iOS間で、実装の一部を共通化したほうが、並行開発が楽である
- 対象はビジネスロジックなど
- XamarinやFlutterのようなX-Platform開発手法もあるが、受託案件では何か起きたら迅速にアップデートできるほうがよいので、以下リスクを考慮して採用しなかった
- X-Platform:保守できるエンジニアが少ない、Trouble shooting事例が少ない、など
iOSのMVCには課題あり
- ViewにもModelにも属さないコードをControllerに実装しがち
- 気がつけば膨大なControllerと、小さなView&Modelになっていることが多い
iOSでは代わりにVIPERを使おうという流れがある
VIPERとは
VIPERとは
- iOSでMVCに置き換わるとされているクリーンアーキテクチャーの一種
- View、Interactor、Presenter、Entity、Routerの略語
- 単一責任の原則に基づく
- Androidでは、ViewとRouterの分離が難しいので、適用が難しい
- VIPE+Rと考える案も一応ある
ならもうクリーンアーキテクチャー適用で充分では?(経緯はちょっと違うかも)
その前に、今出てきた「○○の原則」をもうちょっと説明
重要なSOLID原則
-
SRP: 単一責任の原則
- 1つのモジュールはたった1つの役割に対して責務を負う
-
OCP: オープン・クローズドの原則
- 拡張に対して開かれており、修正に対して閉じている
- 要は、変更が発生した場合、既存のコードは修正せず、新しくコードを追加して対応する
- オブジェクト指向設計の核心で、再利用、保守、柔軟性のメリットが受けられる
- LCP: リスコフの置換原則
- ISP: インターフェイス分離の原則
-
DIP: 依存関係逆転の原則
- 抽象モジュールを具象モジュールに依存させるべきではない。具象を抽象に依存させるべき
依存関係逆転の原則(DIP)
- この構成は、抽象(BusinessRule)が具象(SQLDatabase)に依存している
- ダメな理由は2ページ後で説明
依存関係逆転の原則(DIP)
- 具象(SQLDatabase)が抽象(BusinessRule)に依存するようになった
- このほうが柔軟なシステムとなる
- Clean Architectureにおいて特に重要な原則
なぜ逆転させるのか
- 技術は変わりやすく廃れやすい
- 使っていたOSSが更新されなくなったので乗り換えたい!
- けど上位レイヤーが依存しまくっている…
- パフォーマンスが悪いので別のDBに乗り換えたい!
- けど上位レイヤーが依存しまくっている…
- 一方で上位レイヤーのビジネスルールはそう簡単には変わらない
- 変わりにくいものを変わりやすいものに依存させると、システム全体が不安定になる
- 依存関係を逆転させれば、抽象モジュールへの影響を最小限に抑えつつ具象モジュールを変更できる
- 単体テストの作成・実施も容易になる
- インタフェースに適合するモックを用意すれば、テスト用の具象モジュールは不要
クリーンアーキテクチャーとは(本題)
一枚図
図の説明
前頁の図で、
- 矢印は依存関係を示している
- 右下の図は制御の流れを示している
- UIもDBも外側にいる
- 円の外は外界
各レイヤー(下に行くほど変わりやすい)
レガシーな階層アーキテクチャーは大体こんな感じだった
[プレゼンテーション層(UI)]
↓
[ドメイン層(ビジネスルール)]
↓
[永続化層(DB)]
基本原則
- ソースコードの依存関係は、内側(上位レベルの方針)だけに向かっていなければならない
- ビジネスルールはフレームワークに依存しない
- ビジネスルールは単体でテスト可能
- ビジネスルールはUIに依存しない
- ビジネスルールはデータベースに依存しない
- ビジネスルールは外界のインターフェイスに依存しない
- 図は概要なので、上記原則が満たされていれば何層でも構わない
- ただし過剰な階層化は生産性を下げるので注意
境界線を引くための指針
- 重要なものと重要でないものの間に線を引く
- DBはビジネスルールにとって重要でないので、線を引く
- UIもビジネスルールにとって重要ではないので、線を引く
- 変更の軸があるところに線を引く
- UIはビジネスルールと異なる理由・頻度で変更されるので、線を引く
- ウェブとのインターフェイスは以下略
- 単一責任の原則(SRP)が境界を引くための指針となる
コンポーネントのレベル
- 入出力との距離でコンポーネントのレベルを判断する
- 入出力に近いほど下位
- 下位のコンポーネントを円の外側に、上位を内側に配置する
- 下位を上位に依存させる(依存関係の逆転)
上位 Data Conversion
↑ Encryption Translation
│
↓ Database View HTTP
下位 File
制御の流れ
- すべての制御はUse Casesを介して行われるべき
- 外側のコンポーネント同士が直接データをやり取りすべきではない(環状道路の罠)
Entities
- Enterprise(企業の) Business Rules(ビジネスルール)
- 最重要ビジネスルール・最重要ビジネスデータを指す
- 銀行の利子計算とか
- オンラインストアなら商品(を売ること)かな
- 企業(組織)内で共通するルール・データを記述する部分
- 企業のソフトウェアでなければアプリケーション固有のビジネスルール・データ(後述)となる
- DBとかE-Rモデルでいうところの「エンティティ」とは少し違うので注意
- この層は特定のデータベースに依存しないため
- 「ルール」なので何らかのメソッドを持ったクラスかもしれない
Use Cases
- アプリケーション固有のビジネスルール
- そもそもビジネスルールって?
- ビジネスルールは、ソフトウェアが存在する理由である
- 「手段」と混同してはいけない
- データベースやUIなど下位の詳細に関わるべきではない
1. テキストボックスに入力された宛先・件名・本文を受け取る
2. 宛先を検証する。宛先が適切なら次に進む
a. 宛先が空か不正ならダイアログでエラーを通知して終了
3. 件名を検証する。件名が適切なら次に進む
a. 件名が空なら警告を表示する
i. ユーザがOKボタンを押したら承認したら次に進む
ii. ユーザがキャンセルボタンを押したら拒否したら終了
4. メールを送信する
Interface Adapters
- ユースケースと外界(UIやデータベース)とのデータ変換などを担う部分
- MVxxアーキテクチャはここに属する
- View, ViewModel, Presenter, Controllerなど
- Modelはユースケースの集まりと考えられるので上位
- エンティティとデータベースのフォーマットとの間の変換もここで行う
- SQLならSQL文、RealmならRealm Object
Frameworks & Drivers
- 外部との境界で、フレームワークやツールを使う部分
- あまりコードを書かない
- 実際つくるとしたらInterface Adaptersと一体になる気がする
メインコンポーネント
- システムの入り口(main関数)を含む部分
- AndroidならApplicationやMainActivityを含むモジュール
- 最下層に属する
- ので、唯一すべてのコンポーネントへの参照が許される
- DIフレームワークとかを使って依存関係の注入を行う
「詳細」なもの
- データベースは詳細
- データの保存にファイルを使うのか、RDBMSを使うのかは、アーキテクチャ的に重要ではない
- ウェブは詳細
- ウェブは集中と分散の歴史をひたすら繰り返している(つまり変わりやすい)
- ウェブは入出力デバイスのひとつと考える
- フレームワークは詳細
- フレームワークは便利だが、そこに依存すると簡単に抜け出せなくなる
- フレームワークと結婚するな。フレームワークとは距離を置け(使うなとは言っていない)
これらが円の内側に入り込まないように気をつける
ビジネスルールは本当に具象に依存すべきでないのか
- すべての具象を排除することは不可能
- Stringは具象だが、Stringなしで開発することはできない
- Stringは安定しているので使っても問題ないと考える
- Listなどのコレクションも同様に考える
- じゃあRxは?
- Rxが今後も廃れる心配がないなら使ってよいと思う
- Android Clean ArchitectureのサンプルはRxJavaを使っているらしい
優れたアーキテクチャとは
- 開発しやすい
- 適切に分割されたコンポーネントがあれば分担しやすい
- テストしやすい
- ユースケースがフレームワークに依存しないので独立したテストができる
- 保守しやすい
- 「洞窟探検」のコストを抑える
- 選択肢を残しておく
- データベース、UI、プロトコルなど詳細に関する決定を先送りにできる
- 「ソフト」とは柔軟に変更できるという意味。変更できることにソフトウェアの価値がある
まとめ
- Clean Architectureとは
- ユースケースをシステムの中心として捉え
- ユースケース(上位の方針)とフレームワーク等(下位の詳細)を分離し
- 依存関係の逆転によって下位を上位に依存(プラグイン化)させ
- コンポーネントごとのテストを容易にし
- 詳細部分の置き換えを容易にする設計手法
「速く進む唯一の方法は、うまく進むことである」(Robert C. Martin)
今回のアプリ設計(コンポーネント図)
uiモジュール
- UIを実装。OS依存
- ViewはActivityやFragmentを指す。ViewModelのデータを表示し、ユーザの入力に応じてViewModelにコマンドを発行する
- ViewModelはInteractorから受け取ったデータを表示可能な形式に変換したり、コマンドを受け取ってInteractorのメソッドを呼び出す
domainモジュール
- ビジネスロジックを実装する。OS非依存
- Interactorはユースケースを表し、ViewModelから見たModelに相当する。ビジネスロジックはここに書く。ユースケースごとにクラスを作成する
- Presenterは処理したデータをViewModelに渡すためのインタフェース。表示に適したデータ加工(ソートなど)は行わない
- Repositoryはデータへのアクセス手段。データがサーバーにあるかローカルにあるかをInteractorが意識しなくて済むよう隠蔽する。サーバから取得したデータをDBに保存するなどの処理を行う。データのCRUDに専念し、複雑なロジックは書かない
- Serviceはサーバや端末センサーにアクセスするためのインタフェース
- DataStoreはローカルのデータベースにアクセスするためのインタフェース
- **Model(図では省略)**はエンティティ(ドメインモデル)の集まりで、アプリで扱うデータを単純なデータクラスで表現する
dataモジュール
- データの永続化を行う。OS依存。今回はRealmを使用
- UIに関する設定はここではなくuiコンポーネント内でより簡素な方法で保存する
backendモジュール
- サーバーとの通信を行う。OS依存。今回はRetrofit/OkHttpを使用
sensorモジュール
- 端末センサーからのデータ読み出しを行う。OS依存
utilityモジュール
- ビジネスロジックと直接関係のないユーティリティはここに実装する
- ログ機能、拡張関数など
-
OS非依存(domainからも参照可能にするため)
- OS依存のAPIを使う場合はinterface化して実装を別コンポーネントに持たせる
appモジュール
- メインコンポーネント。各コンポーネントの初期化などを行う
- DIコンテナ(Kodein/Swinject)はここで管理し、各クラスのコンストラクタやプロパティを通じて、依存性の注入を行う
クリーンアーキテクチャーを導入してみて(メンバーへのアンケート結果)
Q1: クリーンアーキテクチャーの理解はスムーズでしたか?
- そう思う(5pts)………………2人
- ややそう思う(4pts)………………0人
- どちらとも思わない(3pts)………………3人
- ややそう思わない(2pts)………………1人
- そう思わない(1pt)………………0人
平均:3.50pts
Q2: クリーンアーキテクチャーを使ったことにより、保守しやすいコードが書けたと思いますか?
- そう思う(5pts)………………2人
- ややそう思う(4pts)………………4人
- どちらとも思わない(3pts)………………0人
- ややそう思わない(2pts)………………0人
- そう思わない(1pt)………………0人
平均:4.33pts
Q3: クリーンアーキテクチャーを使ったことにより、質の高いコードが書けたと思いますか?
- そう思う(5pts)………………4人
- ややそう思う(4pts)………………2人
- どちらとも思わない(3pts)………………0人
- ややそう思わない(2pts)………………0人
- そう思わない(1pt)………………0人
平均:4.67pts
Q4: クリーンアーキテクチャーを使ったことにより、開発時間が短縮できたと思いますか?
- そう思う(5pts)………………0人
- ややそう思う(4pts)………………1人
- どちらとも思わない(3pts)………………3人
- ややそう思わない(2pts)………………1人
- そう思わない(1pt)………………1人
平均:2.67pts
よかった点(1)
- 構造化の度合い
- 各クラスの責務が小さくなり、内容を理解しやすかった
- 各クラスの役割がある程度明確になった
- それぞれのレイヤー毎に単体テストできた
- 上手くモジュール分けされているので"この部分が未定だけどそこはstubにして他を先に実装する"がしやすい
よかった点(2)
- 一貫性・保守性
- 指針が明確になったことで、誰が作っても同様の設計になり保守性が上がった
- レビュー時にも「クリーンアーキテクチャーに基づいているかどうか」という明確な観点からコードを見ることが出来てやり易かった
- ほぼほぼSOLID原則に従った実装になりやすかった
- 仕様変更時の改修量が少なかった(例:サーバーAPIが急に変更になったが、backendのJSONObjectクラス1つを修正するだけで済んだ)
- ほとんどのクラスがコンパクトになった
- モジュール毎に機能を分けて書いていき、それぞれに必要以上に依存関係を持たせないので保守がしやすい
- 質の高いコーディング以外受け付けないので、強制的に質が高くなるところが良い
よかった点(3)
- 理解可能性
- 修正箇所を見つけるのも意外と苦ではなかった
- どの機能においても似たようなコーディングの仕方になるので、後から"この機能どうなっているんだっけ"と確認する時に探しやすかった
改善すべき点(1)
- 構造化の度合い
- uiモジュールの各クラスが肥大化した(ViewとNavigationを両方含んでいるからと思う)
- PresenterとViewModelを分離すべきだったかも
- もしくは、ControllerとPresenterの実装を分けるべきだった
- いちいちUseCase(Interactor)を通さないといけない場合があるので、それが面倒
- クリーンアーキテクチャの模範例に従いすぎずモジュールに柔軟性も持たせた方がよかったかも
- ファイル数が多い。
- 仕様が読みづらくなるので、品質の高い仕様書(UML)を書くべき。
改善すべき点(2)
- 理解可能性について
- アーキテクチャーの学習コストがかかった(選定に関わった人は習得が早いが、そうでない人はちょっと苦戦する傾向あり)
- 敷居が高い。理解するまでに時間がかかった。
- どこに責務を持たせるか、人によって判断の違いが若干あり、その場合議論が起きやすい(レビューの長さが時々ボトルネックになっていた)
当日の質疑応答の内容
クリーンアーキテクチャーの苦手なポイントとは
- MVCなど他アーキテクチャーを使っていたら移行に長い時間がかかる
- 一部の開発者はこのパターンを理解するのが困難
- スクラムでやるくらいの規模の開発には向いているが、個人開発だとやりすぎ感あるし、(チーム的にも量的にも)大規模な開発は苦手と感じた
- Entity部分の肥大化に伴う構成の複雑化、保守性や可読性の低下が問題となってくると強みを活かせない
- が、各クラスが小さくなるというメリットは大規模になっても活きる
データのコピーや変換による速度低下・オーバーヘッドは問題にならないか
- 異なるモジュールにデータを渡せばコピーや変換は発生するが、繰り返し何度も変換するわけではない
- 例えばdata→domainならDBに依存する型からdomainで扱える簡素な型への変換が必要だが、domain→UIは変換は必要なく、描画処理で吸収すればよい
- 今回の開発では、多くてもbackend→domain→data→domainの3度の変換で済み、目に見える速度低下は無かった
- すごく長いリストを扱うと、トータルで何割かのオーバーヘッドが起きて問題になるかもしれない
- が、そのせいでアーキテクチャーを諦める前に、リストの最大数を制限する仕様としたり、言語のアンチパターンを取らないなど、他の努力でカバーできる
- また、理由があればjson形式のまま表示処理まで渡してよいなど、柔軟な対応をしたほうがよいと考える
他に検討したアーキテクチャーは?
- MVC
- MVP
- MVVM
- Flux
- Redux
- MVW
- Angular
何故クリーンアーキテクチャーを選んだのか
- スマホアプリのクロスプラットフォーム開発に向いているとの情報が散見されたから
- iOSのMVCの課題を解決する見込みがあったから
- 品質に重きを置いてほしいと言われていたから
- 単体テストしやすいアーキテクチャーとして選んだ
- メンバーの中に得意とする人がいたから
- …あとは直感(大事)
- エンジニア招集から開発着手まであまり日数が無かったため、1つ1つのアーキテクチャーをじっくり選定している時間は無かった
参考文献
- Clean Architecture 達人に学ぶソフトウェアの構造と設計 Robert C.Martin(著), 角 征典(訳), 高木 正弘(訳)
- https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
- https://proandroiddev.com/mvvm-architecture-viewmodel-and-livedata-part-1-604f50cda1
- https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html
- https://cheesecakelabs.com/blog/ios-project-architecture-using-viper/
- https://cheesecakelabs.com/blog/using-viper-architecture-android/
- https://qiita.com/gomi_ningen/items/02c42e2487d035f9c3c8
- https://medium.com/eureka-engineering/go-open-closed-principle-977f1b5d3db0
- https://docs.google.com/presentation/d/1zamntmxKcupdPDtvQS2SSKxLiw1RxcjwF18litud6os