LoginSignup
176

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-06-12
1 / 61

はじめに

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

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

はじめに

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


オススメ書籍

51mQrYTahJL.jpg


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

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

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


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


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

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

iOSのMVCには課題あり

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

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


VIPERとは

初代GT最速の市販車
2_3064.jpg
のことではない


VIPERとは


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


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


重要なSOLID原則

  • SRP: 単一責任の原則
    • 1つのモジュールはたった1つの役割に対して責務を負う
  • OCP: オープン・クローズドの原則
    • 拡張に対して開かれており、修正に対して閉じている
    • 要は、変更が発生した場合、既存のコードは修正せず、新しくコードを追加して対応する
    • オブジェクト指向設計の核心で、再利用、保守、柔軟性のメリットが受けられる
  • LCP: リスコフの置換原則
  • ISP: インターフェイス分離の原則
  • DIP: 依存関係逆転の原則
    • 抽象モジュールを具象モジュールに依存させるべきではない。具象を抽象に依存させるべき

依存関係逆転の原則(DIP)

non-dip.png

  • この構成は、抽象(BusinessRule)が具象(SQLDatabase)に依存している:heavy_multiplication_x:
    • ダメな理由は2ページ後で説明

依存関係逆転の原則(DIP)

dip.png

  • 具象(SQLDatabase)が抽象(BusinessRule)に依存するようになった:heavy_check_mark:
    • このほうが柔軟なシステムとなる
  • Clean Architectureにおいて特に重要な原則

なぜ逆転させるのか

  • 技術は変わりやすく廃れやすい
  • 使っていたOSSが更新されなくなったので乗り換えたい!
    • けど上位レイヤーが依存しまくっている…
  • パフォーマンスが悪いので別のDBに乗り換えたい!
    • けど上位レイヤーが依存しまくっている…
  • 一方で上位レイヤーのビジネスルールはそう簡単には変わらない
  • 変わりにくいものを変わりやすいものに依存させると、システム全体が不安定になる
  • 依存関係を逆転させれば、抽象モジュールへの影響を最小限に抑えつつ具象モジュールを変更できる
  • 単体テストの作成・実施も容易になる
    • インタフェースに適合するモックを用意すれば、テスト用の具象モジュールは不要

クリーンアーキテクチャーとは(本題)


一枚図

CleanArchitecture.jpg


図の説明

前頁の図で、

  • 矢印は依存関係を示している
  • 右下の図は制御の流れを示している
  • UIもDBも外側にいる
  • 円の外は外界

各レイヤー(下に行くほど変わりやすい)

CleanArchitectureCone.jpg


レガシーな階層アーキテクチャーは大体こんな感じだった

[プレゼンテーション層(UI)]
   ↓
[ドメイン層(ビジネスルール)]
   ↓
[永続化層(DB)]


基本原則

  • ソースコードの依存関係は、内側(上位レベルの方針)だけに向かっていなければならない
    • ビジネスルールはフレームワークに依存しない
    • ビジネスルールは単体でテスト可能
    • ビジネスルールはUIに依存しない
    • ビジネスルールはデータベースに依存しない
    • ビジネスルールは外界のインターフェイスに依存しない
  • 図は概要なので、上記原則が満たされていれば何層でも構わない
    • ただし過剰な階層化は生産性を下げるので注意

境界線を引くための指針

  • 重要なものと重要でないものの間に線を引く
    • DBはビジネスルールにとって重要でないので、線を引く
    • UIもビジネスルールにとって重要ではないので、線を引く
  • 変更の軸があるところに線を引く
    • UIはビジネスルールと異なる理由・頻度で変更されるので、線を引く
    • ウェブとのインターフェイスは以下略
  • 単一責任の原則(SRP)が境界を引くための指針となる

コンポーネントのレベル

  • 入出力との距離でコンポーネントのレベルを判断する
    • 入出力に近いほど下位
  • 下位のコンポーネントを円の外側に、上位を内側に配置する
  • 下位を上位に依存させる(依存関係の逆転)
上位    Data Conversion
↑         Encryption Translation
│
↓   Database   View   HTTP
下位      File

制御の流れ

名称未設定.png

  • すべての制御は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)


今回のアプリ設計(コンポーネント図)

components_75percent.png


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

参考文献


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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
176