msawady’s tech-note

フルスタックエンジニアの学んだことや考えていること

【読書メモ】Clean Architecture 達人に学ぶソフトウェアの構造と設計

システムアーキテクチャの設計と方針

  • GWにClean Architecture を読みました
  • アーキテクチャや設計という抽象的なところをかなり良い感じに説明してくれてます
  • 「コードは一通り書けるが、ゼロからシステムを設計する自信はイマイチ」という方にオススメ出来る本です。

Clean-Architecture-達人に学ぶソフトウェアの構造と設計

設計・アーキテクチャの価値とは

ズバリ「保守、機能追加にかかるコストを低く保つこと」と定義しています。 多少乱暴な感じがしますが、以降の話を進める上で非常に重要な視点です。

ゼロから作る段階では、機能間・コンポーネント間の依存を意識しなくとも、充分なスピードで開発を進めることが出来ます。 しかし、機能が増えていくことで、「機能間の依存関係により開発がスムーズに進まない」「デプロイにあたっての調整が複雑になる」 などの理由でオペレーションが増え、スピードが落ち、段々とコストが上がっていきます。 具体的なシグナルとして、「開発メンバーが増えているのに、機能追加のペースが変わっていない、あるいは落ちている」といった状況が挙げられています。

こういった状況にならないように、システムの構造(=アーキテクチャ)を設計し、より良いものにします。

3つのプログラミング・パラダイム

ソフトウェア開発におけるプログラミング・パラダイムは、大きく3つに分けられます。

  • 構造化プログラミング
    • GOTO文(直接的な処理の移行)への制約
  • オブジェクト指向プログラミング
    • 関数間の依存関係(間接的な処理の移行)への制約
      • ポリモーフィズムによる依存関係のコントロール
  • 関数型プログラミング
    • 代入への制約

大事なことは、全て「制約」で有るということです。また、この50年ほどの間に様々な進歩が有りましたが、パラダイムは増えることはありませんでした。

これらの制約がシステムの構造を良くするためのベースの道具であり、設計のベースとなります。

SOLID原則

SOLID 原則はオブジェクト指向プログラミングにおけるクラス/モジュール設計の原則をまとめたものです。

  • SRP: 単一責任の原則
    • 変更する理由は1つでなければならない
  • OCP: 解放閉鎖の原則
    • 拡張に対して開き、修正に対して閉じていなければならない
  • LSP: リスコフの置換原則
    • サブクラスは、そのスーパークラスで置換可能でなければならない
  • ISP: インタフェース分離の原則
    • クライアントが利用しないメソッドへの依存を強制してはならない
  • DIP: 依存性逆転の原則
    • 上位の方針を実装するコードは、下位の詳細を実装するコードに依存してはならない

どれも、アーキテクチャの価値である「保守、機能追加にかかるコストを低く保つ」ために重要な原則です。

特に重要なのが DIP です。Interface を用いた依存関係の逆転(=コントロール)は、より粒度の大きなコンポーネントレベルにおいての設計にも効いてきます。

コンポーネント設計

クラスよりも粒度の大きなコンポーネント設計を行うにあたって、いくつかの原則があります。 基本的にはSOLID原則をより大きな単位で当てはめていくことで、良いコンポーネント、それらの結合を設計します。

凝集度の設計

どのようにクラスをコンポーネントをまとめるか、を考えるにあたって以下の3つの原則を考慮します。

  • REP: 再利用/リリース等価の原則
    • 再利用とリリースの単位は等しくなる
    • これが満たされないと: コンポーネントの再利用性が下がる
  • CCP: 閉鎖性等価の原則
    • 一つの理由による変更が行われるクラスは一つのコンポーネントにまとめる
    • これが満たされないと: 一つの変更で多くのコンポーネントの修正が必要になる
  • CRP: 全再利用の原則
    • 実際に利用しない他コンポーネントへ依存してはならない
    • これが満たされないと: 不要なビルド/リリースの回数が増える

これらはお互いにトレードオフを抱えています。

f:id:msawady:20190504101636p:plain
テンション図
時々のニーズに応じて、適切な「落とし所」を探りながらアーキテクチャを修正していく必要があります。

結合の設計

どのようにコンポーネント間の結合(=依存関係)を設計するか、を考えるには以下の3つの原則を考慮します。

  • ADP: 非循環依存の原則
    • コンポーネントの依存関係に循環依存があってはならない。(Acyclic であるべき)
    • これが満たされないと: 1コンポーネントへの影響が大きくなる、ビルド/リリースの単位が大きくなる
  • SDP: 安定依存の原則
    • より安定度の高い方向に依存する
    • 安定度とは: 不安定度が低いこと
      • 不安定度 = (コンポーネント外へ依存するクラス数) / ( (コンポーネント外へ依存するクラス数) + (コンポーネント外から依存されるクラス数) )
      • より多くのコンポーネントから依存されるほど安定度が高い
      • より多くのコンポーネントへ依存するほど安定度が低い
    • これが満たされないと: 不安定なコンポーネントの修正が、安定度の高いコンポーネントを通して、多くのコンポーネントに影響を及ぼす
  • SAP: 安定度・抽象度等価の原則
    • コンポーネントの抽象度はその安定度と同程度である
    • 抽象度 = (コンポーネント内の抽象クラス,インターフェイス数) / (コンポーネント内のクラスの総数)
    • これが満たされないと:
      • 苦痛ゾーン: 安定度が高く、抽象度が低い
        • DBのスキーマ: 変更の際の苦痛が大きい
        • Stringなどのユーティリティは変動性が低いため問題にならない
      • 無駄ゾーン: 安定度が低く、抽象度が高い

これらの原則に基づいて依存関係をコントロールします。その際の手法として

  • DIP(インターフェイスを用いた依存関係の逆転)を適用する
  • 新たなコンポーネントを作って、依存する/されるクラスを移動する

などが挙げられます。

アーキテクチャ

アーキテクチャの目的は「できるだけ多くの選択肢を残すこと」であり、大方針となる戦略は「『重要ではない詳細』の選択肢を残すこと」です。

重要ではない詳細とは、以下のようなものを指します。

  • フレームワーク
  • UI
  • データベース
  • Webサーバ

これらを序盤に確定せず選択肢として残しておくことで、システムのライフサイクルコストを下げるとともに、『重要な方針』、すなわちビジネスルールにフォーカスします。

切り離し

SRP(単一責任の原則)やCCP(閉鎖性共通の原則)に基づいて、一つの理由で一つのコンポーネントが変更されるようシステムを切り分けます。

  • レイヤーによる切り離し(水平切り離し)
    • ビジネスルール、UI、データベースなどシステム機能での切り離し
  • ユースケースにより切り離し(垂直切り離し)
    • 注文登録、注文削除などユースケースでの切り離し

こうすることで、開発やデプロイの単位を分割し、ライフサイクルコストを低く保つことが出来ます。 このときに「重複」を恐れないことが重要です。重複しているコードやデータ構造であっても、変更頻度や理由が異なる(=偶然の重複)ならば無理にまとめる必要はありません。

具体的な分割のレベルは以下の3段階があります。

  • ソースレベル: サービスは1つ、デプロイの単位は全ファイル
  • デプロイレベル: サービスは1つ、デプロイの単位はコンポーネントごと
  • サービスレベル: サービスが複数

これらのうち、どのレベルまで分割するべきか、は状況によって変わっていきます。 運用のニーズが無いのであれば無理にマイクロサービスにする必要はない(むしろ通信のコストが増える可能性)のです。 状況に応じて分割/統合できるようなソフトなアーキテクチャを保つことが重要です。

そしてクリーンアーキテクチャへ

上記の議論をもとに、フレームワーク、UI、DB、外部システムなどの『詳細』とビジネスルールを分離したものがクリーンアーキテクチャの図になります。

f:id:msawady:20190504133255p:plain

  • エンティティ: ドメインのビジネスルールを実装する
  • ユースケース: アプリケーション固有のビジネスルールを実装する
  • インターフェイスアダプタ: ユースケース/エンティティと『詳細』コンポーネントの間のデータ変換を行う
  • フレームワーク/ドライバ: データベースやUIなど

大事なことは、円の外側から内側に向かって依存する、ということです。エンティティやユースケースはデータベースやフレームワークを意識しません。

  • 境界を超える際は、常に外側から内側に向かって依存する
    • DIP(インターフェイスを用いた依存関係の逆転)を適用する
  • 境界を超えるデータは、シンプルかつ独立したデータ構造を持つようにする
    • Entity や DB のレコードを直接やり取りしない
  • Humble Object パターンを利用して、データのマッピング/加工ロジックをテスト可能にする

Main コンポーネント

コンポーネントの作成、調整、監督を行うコンポーネントを Main コンポーネントとします。

  • DI の設定
    • interface に対する Implementation の設定
      • DBドライバの設定、UIの設定
  • ネットワークの設定などのインフラストラクチャの設定

これらの『詳細』な設定を行った上で、アプリケーションを起動し中心となるコンポーネントの処理を開始するという責務を持ちます。

開発/テスト/本番といった環境や、国や権限などの状況にそったMainコンポーネントを用意することで、中心となるビジネスルールが関与することなく設定を扱うことができます。

終わりに

書いてみたら、中々のボリュームになりました。本線となるところは大方かけたと思います。 自分自身が必ずしも理解していなかったことが、この本を読んでかなり理解できました。

他にもマイクロサービスやファームウェアについての話もあり、全体的に楽しく読むことが出来ました。 個人的には、「エンジニア必読の書」に入るかと思います。

Clean-Architecture-達人に学ぶソフトウェアの構造と設計