クリーンアーキテクチャーでスマホアプリ開発した感想(勉強会用)


はじめに

昨年からの大きな案件でClean Architectureを使った

  • Platforms: Android/iOS
  • Languages: Kotlin/Swift

はじめに

勉強会向け資料なので、クリーンアーキテクチャー自体の解説もある程度含まれます。
逆に、時間の都合上、歴史背景や細かい部分までは行き届いていません。
もし間違いがあればご指摘ください。


オススメ書籍


アーキテクチャーを選定する目的

  • 求められるシステムを構築・保守するために必要な人材を最小限に抑えるため
    • 「アーキテクチャーは上位レベル、設計は下位レベル」のように区別されることがあるが、両者の間に明確な境界はなく、上位から下位に至るまで、決定の連続である

スマホアプリ開発で代表的なアーキテクチャー


では、Android、iOS両方作る場合はどうする?


ロジックは極力共通化したい

  • Android↔︎iOS間で、実装の一部を共通化したほうが、並行開発が楽である
    • 対象はビジネスロジックなど
  • XamarinやFlutterのようなX-Platform開発手法もあるが、受託案件では何か起きたら迅速にアップデートできるほうがよいので、以下リスクを考慮して採用しなかった
    • X-Platform:保守できるエンジニアが少ない、Trouble shooting事例が少ない、など

iOSのMVCには課題あり

  • ViewにもModelにも属さないコードをControllerに実装しがち
    • 気がつけば膨大なControllerと、小さなView&Modelになっていることが多い

iOSでは代わりにVIPERを使おうという流れがある


VIPERとは

初代GT最速の市販車

のことではない


VIPERとは


ならもうクリーンアーキテクチャー適用で充分では?(経緯はちょっと違うかも)


その前に、今出てきた「○○の原則」をもうちょっと説明


重要な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つのアーキテクチャーをじっくり選定している時間は無かった

参考文献


ご清聴ありがとうございました。