目次
- 動機
-
- プログラミングパラダイム
- 構造化プログラミング
- オブジェクト指向プログラミング
- 関数型プログラミング
- パラダイムのまとめ
- SOLID原則
- 単一責任の原則 (SRP: Single Responsibility Principle)
- オープン・クローズドの原則 (OCP: Open-Closed Principle)
- リスコフの置換原則 (LSP: Liskov Substitution Principle)
- インターフェイス分離の原則 (ISP Interface Segregation Principle)
- 依存関係逆転の原則 (DIP: Dependency Inversion Principle)
- コンポーネントの原則
- 再利用・リリース等価の原則 (REP)
- 閉鎖性共通の原則 (CCP)
- 全再利用の原則 (CRP)
- REP, CCP, CRPの関係
- 非循環依存関係の原則 (ADP)
- 安定依存の原則 (SDP)
- 安定度・抽象度等価の原則 (SAP)
- まとめ
動機
業務にて設計に携わることがあり、何度か聞いていたクリーンアーキテクチャというものを調べてみた。 ブログ記事や解説記事を漁っているうちに、これは原本を呼んだ方が早そうだと思い書籍を購入した。 (後から気づいたが、本人のブログ記事が原点だったが説明が最小限なので流石にあれだけでは理解が難しい。)
Clean Architecture 達人に学ぶソフトウェアの構造と設計
崩壊したコードを書くほうがクリーンなコードを書くよりも常に遅い
素晴らしい本だったのだが、幾分情報量が多いのでここにまとめることで頭を整理しようと思う。 崩壊した脳内で設計を行うよりもクリーンにした頭で設計をする方が常に早いのだ。
設計の原則
プログラミングパラダイム
構造化プログラミング
goto文の使用がプログラミングにおいて有害であることが示された。 あらゆるプログラムは「順次」「選択」「反復」の3つの構造で構築できることが特定され、それぞれが数学的に証明された。 全てのモジュールは証明可能な単位に再帰的に分割することが可能であるとしたのが構造化プログラミングであり、 機能によりモジュールは分割できる。 全てのモジュールに対して数学的に正しいことを証明することはできないが、科学的にそれをテストで補うことが可能である。 言い換えればテスト可能な単位までモジュールを分割することが構造化プログラミングのパラダイムにおいてのベストプラクティスである。
オブジェクト指向プログラミング
「カプセル化」「継承」「ポリモーフィズム」に代表されるパラダイムである。 ただし、カプセル化と継承についてはパラダイムシフトが行われる前のC言語から普通に使用されていた概念である。 ここで重要になるのがポリモーフィズムとなる。 ポリモーフィズムの概念がC言語時代になかったわけではないが、安全に使用することはできなかった。 ポリモーフィズムを使用することで依存性を逆転させることが可能になり、ソースコードの依存関係を制御の流れから逆転させることができる。 依存関係逆転を用いることで処理の中核にある重要なビジネスルールをその他の依存関係から独立させることが可能になる。 まとめると、オブジェクト指向プログラミングの登場によって、ポリモーフィズムを用いることで、 システムにある全てのソースコードの依存関係を絶対的に制御する能力を人類は手に入れた。
関数型プログラミング
関数型プログラミングでは再代入をすることができない。 全ての変数は不変であることで、競合状態、デッドロック状態、並行更新の問題が発生しなくなる。
パラダイムのまとめ
- 構造化プログラミングは、直接的な制御の移行に規律を課すものである。
- オブジェクト指向プログラミングは、間接的な制御の移行に規律を課すものである。
- 関数型プログラミングは、代入に規律を課すものである。
SOLID原則
関数やクラスの設計原則をまとめたSOLID原則を紹介する。
単一責任の原則 (SRP: Single Responsibility Principle)
システムに対しある変更を望むグループのことを アクター と呼ぶこととする。 単一責任の原則とは対象のアクターが複数あるコードは分割するべきであるという原則である。 分割することで増えてしまう関数やクラスはFacadeパターンを使用することでインターフェイスを統一することができる。
オープン・クローズドの原則 (OCP: Open-Closed Principle)
ソフトウェアの構成要素は拡張に対しては開いていて、修正に対して閉じていなければならない 『アジャイルソフトウェア開発の奥義 第2版』 (SBクリエイティブ) より引用
依存性をコントロールし情報を隠蔽することで、各々の変更の影響を最小限に機能追加については容易にするという原則である。
リスコフの置換原則 (LSP: Liskov Substitution Principle)
リスコフの置換原則とは派生型に対する定義である。 T型がS型の派生型となるにはS型で定義されたプログラムPで、 S型のオブジェクトの代わりにT型のオブジェクトを使用してもPの振る舞いが変わらない必要がある。 これはアーキテクチャのレベルでも適用され、派生型と捉えられるあらゆる設計は厳密に置き換え可能でなければならない。
インターフェイス分離の原則 (ISP Interface Segregation Principle)
別のモジュール、コンテキスト、技術を使用する際は分離されたインターフェイスを使用すること。 直接依存してしまうと、密結合が生まれてしまいコンパイルの時間が伸び、障害に対してロバストでなくなる。
依存関係逆転の原則 (DIP: Dependency Inversion Principle)
コードの変化のしやすさを考えると、 具象クラス > 抽象クラス > インタフェース
となる、なるべきである。
そのため、具象クラスへの依存は可能な限り減らす、参照、継承、オーバーライドどれも御法度である。
具象インスタンスの作成をする際は Abstract Factoryパターン を利用することで、直接具象を参照しなくてもよくなる。
このように具象への参照を忌避していくと、最終的には全ての具象を扱う関数が唯一存在することとなり、
これはmainコンポーネントと呼ばれることが多い。
コンポーネントの原則
再利用・リリース等価の原則 (REP)
再利用のためのグループ化を行う。 ひとつのコンポーネントを形成するクラスやモジュールは、まとめてリリース可能でなければならない。 同じバージョン番号を共有し、同じリリースプロセスを経て、同じリリースドキュメントを持っている状態は合理的であり、あるべき姿である。
閉鎖性共通の原則 (CCP)
保守性のためのグループ化を行う。 単一責任の原則をコンポーネントのために言い換えたもの。 コンポーネントを変更する理由が複数あるべきではない。
全再利用の原則 (CRP)
不要なリリース作業を減らすための分割を行う。 一緒に用いられることが多いクラスやモジュールはひとまとめにすること。 密結合なクラスやモジュールも同様である。 逆に、同時に使用されることがほとんどないクラスは分離されるべきである。
REP, CCP, CRPの関係
REPとCCPは包含関係にあり、どちらもコンポーネントを大きくする方向に働くものである。 一方、CRPはコンポーネントを小さくする方向に働く原則であり、これら3つの原則のバランスをうまくとる必要がある。 REPとCCPのみを守るとリリース頻度が増加する。 CCPとCRPのみを守ると再利用性が低下する。 CPRとREPのみを守ると変更すべきコンポーネントが増大する。 プロジェクト初期はREPを軽視する傾向にあり、プロジェクトの成熟に連れて、徐々にREPへ重心を寄せていくことが多い。
非循環依存関係の原則 (ADP)
コンポーネント間の依存は循環してはならないという名前通りの原則。
安定依存の原則 (SDP)
変更しやすいコンポーネントは変更しにくいコンポーネントから依存されてはならない。
安定度・抽象度等価の原則 (SAP)
コンポーネントの抽象度はその安定度と同程度んでなければならない。
アーキテクチャ
アーキテクチャとはシステムに与えられた形状である。 アーキテクチャの目的はシステムの開発、デプロイ、運用、保守を容易にすることである。
それらを容易にするための戦略は、できるだけ長い期間、できるだけ多く選択肢を残すことである。
システム
開発
マイクロサービスアーキテクチャでも読んだ通り、1チームが1コンポーネントを担当するべきであるため、 形状は自ずとチームに寄り添う形で決まっていくはずである。
デプロイ
デプロイの文脈でアーキテクチャが目指すべきことは単一のアクションでデプロイが行えることである。 単に自動デプロイ環境が整っていることを指すのではなく、対象のコンポーネントが独立してデプロイできることを指す。
運用
アーキテクチャの運用における仕事はシステムへの理解の手助けとなることだ。 形状がシステムを端的に表せているならば、システムへの理解は容易となる。
保守
保守のコストとは既存のコードを掘り起こし、新しい機能の追加や欠陥の修正に最適な場所や戦略を見つけるコストである。 正しいアーキテクチャは上記コストを大幅に削減でき、人的リソースの増大を防ぐ。
アーキテクトの思考
選択肢
システムは大きく「方針」と「詳細」の二つに分割できる。 この時、アーキテクトは方針とは無関係に詳細を決めながら重要でない詳細については選択肢を残してアーキテクチャを構築していく。
切り離し
- 水平方向の分割としてレイヤー分割
- 垂直方向の分割としてユースケース分割
上記の分割を行う規模として
- ソースレベル
- デプロイ(バイナリ)レベル
- サービス(実行単位)レベル
などが考えられる。
境界線
1つのクラス、モジュール、コンポーネントにとって重要なものと重要でないものの間には境界線を引く。
インターフェイスと継承
インターフェイスとその継承の間には境界線が発生する。 対象の単位内でインターフェイスが定義さえされていれば、インターフェイスの実装部分については無関心でいられるためである。
デプロイコンポーネント
動的リンクライブラリはデプロイレベルの切り離しとして理解でき、それぞれの動的リンクライブラリには境界がある。
スレッド
モノリスでも動的リンクライブラリでもスレッドでの分割も行うことができる。
ローカルプロセス
スレッドよりも明確な境界線としてローカルプロセスもある。
サービス
現状最も強い境界線はサービスである。 同じプロセッサやマルチコアの環境で同時に動作させることも可能だが、そのような物理的な制約は特に問題にならない。 あらゆる通信はネットワークを経由して行われることが前提となる。
アーキテクチャ
方針とレベル
プログラムは入力を出力に変換する 方針 を詳細に記述したものである。 この方針は細分化され、入力の検証や、出力のフォーマット設定の記述をしている方針がある。 これらの方針には レベル が存在する。
「レベル」の定義は「入力と出力からの距離」である。 上述の入力の検証や、出力のフォーマット設定などは入出力に限りなく近いため下位のレベルに位置する。 逆に上位のレベルとしては受け取った入力に対して行う変換作業などが挙げられる。
ソースコードの依存性はデータフローから切り離し、レベルと結びつけるべきである。 どのような場合も、下位レベルのコンポーネントが上位レベルのコンポーネントに依存するように設計する。
方針のグルーピングについてはSRPやCCPに基づいて行うが、 その際に重要なのは上位レベルの方針は下位レベルの方針よりも変更の頻度が低く、変更の理由が重要である。
ビジネスルール
エンティティ
最重要ビジネスルールのことをエンティティと呼ぶ。 エンティティは最上位のレベルに位置することとなる。
エリック・エヴァンスのドメイン駆動設計をまとめる にて扱ったエンティティとは別の概念であり、ドメイン駆動設計でいうところのコアドメインにあたる。
ユースケース
ユースケースはアプリケーション固有のビジネスルールを記述する。 アプリケーション固有のビジネスルールとはエンティティをいつ・どこで呼び出すかを規定することである。
ユースケースでは実際の入出力の詳細を知ることはなく、エンティティはユースケースを知ることがない。 ここからユースケースがエンティティと入出力の詳細の中間のレベルに位置していることがわかる。
エリック・エヴァンスのドメイン駆動設計をまとめる でいうところのドメイン層のサービス、特に粒度を大きくとるために使用するサービスに近いものであると考えている。
リクエストとレスポンスのモデル
ユースケースは入力データを期待し、出力データを生成するが具体的な入出力の詳細に触れてはいけない。 そのため、入力データ、出力データは双方入出力の詳細に依存していないデータである必要がある。 ここで、SRPから、これらのデータ構造にエンティティへの参照を含めてはならない。
フレームワークとアーキテクチャ
はっきりさせておく必要があるのは、フレームワークはアーキテクトによって選択肢が残されるべき詳細であるということである。 フレームワークによってアーキテクチャが規定されることはなく、あくまでツールに過ぎないことを認識する必要がある。 また、テストについてもフレームワークに依存することなくユニットテストが実行できることが望ましい。
クリーンアーキテクチャ
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html より引用
優れたアーキテクチャの特性
- フレームワーク非依存: フレームワークをツールとして扱える
- テスト可能: ビジネスルールはUI、データベース、ウェブサーバー、その他の外部要素がなくてもテストできる
- UI非依存: UIはシステムの他の部分の変更と独立して変更できる
- データベース非依存: ビジネスルールの変更なしにデータベースを自由に変更できる
- 外部エージェント非依存: ビジネスルールは外部のインターフェイスについて何も知らない
依存性のルール
円の中心に近づくほどレベルが上がっていることを表している。 つまり、 ソースコードの依存性は、内側岳に向かっていなければならない。
境界線の越え方
ユースケースからプレゼンターを呼び出したい時など、上記のルールからユースケースがプレゼンターを直接呼び出し、 プレゼンターに依存することは許されていない。 内側から外側の層を使用する必要がある場合はDIPを利用する。
境界線を越えるデータ
- 構造体
- データ転送オブジェクト
- 関数呼び出しの引数
など選択肢はいくつもあるが、独立した単純なデータ構造であることが重要である。 エンティティオブジェクトやデータベースの行をそのまま渡すようなことをしてはいけない。
インターフェイスアダプター
Humble Object
Humble Objectパターンは振る舞いをユニットテストがしやすいように二つに分割するパターンである。 一つのモジュールは控えめ(Humble)で、ここにはテストが難しい振る舞いのみが記述される。 例としてはGUIが挙げられる。GUIにおいて画面に表示されているものの確認の自動化は極めて難しいが、 その他の振る舞いの大部分は簡単にテストできる。
PresenterとView
上述のHubmle Objectパターンで登場したGUIにおける表示とその他の部分を指す。 表示の部分をView, その他の部分をPresenterと言う。 Viewはテストが難しいため、表示だけに専念して、データの移動や取得などのロジックは含まないようにする。 代わりにテスト可能な部分の全てはPresenterに含まれる。 Presenterで全てのデータを取得、処理した後にViewが解釈でき、 そのまま表示できる ViewModelと呼ばれるようなデータ構造体として受け渡す。 ViewがやるべきことはViewModelからデータを読み込み画面に表示する以外なく、Humbleに保たれる。
データベースとゲートウェイ
ユースケースインタラクターとデータベースの間にあるのがゲートウェイである。 ゲートウェイはアプリケーションのビジネスルールとは格別され、データのCRUDの実際の操作にのみ焦点を当てる。 ゲートウェイはデータベースと指定されたやりとりをしているだけなのでHumble Objectと言える。 また、データベースとのやりとりの部分、例えばSQL文の記述になると思われるが、ゲートウェイに隔離しておくことで、 ユースケースインタラクターが独立してテスト可能になる。
さて、ここでデータマッパーに代表されるマッピング処理はどこに含まれるかだが、 当然ユースケースインタラクターではなく、ゲートウェイに含まれる。 ゲートウェイの中でもデータベースとゲートウェイの境界に位置する技術がデータマッパーである。
サービスリスナー
アプリケーションが他のサービスと通信する必要がある場合も、Hubmle Objectパターンを意識する。 境界を越える際はシンプルなデータ構造を持ってアプリケーションに渡すことや、詳細の情報をアプリケーション層が扱わないことを意識する。
境界
You Aren't Going to Need It.
通称YAGNIから必要に迫られる前の抽象化は悪だとされてきた。 しかし、アーキテクトは時に変化を予測してYAGNIを違反する必要がある。
部分的な境界を利用することで、完全な境界は形成せずに境界を作成する準備だけすることもできるが、 そのような境界は不完全なため、境界の維持は実装者のモラルに頼らざるを得ない。
アーキテクトは実装する境界と無視する境界を決定する必要があり、 それはプロジェクトの初期から運用に至るまでで何度も考察されるべきである。
メインコンポーネント
Mainコンポーネントは、究極的な詳細(最下位レベルの方針)である。
全てのシステムでは少なくとも一つ、その他のコンポーネントを作成・調整・監督するコンポーネントが必要になる。 そのようなコンポーネントをメインコンポーネントと呼ぶ。 依存関係の注入や、設定ファイルのパス指定などコードの本体で把握したくない文字列を読み込む。
Mainは、初期状態や構成を設定して、外部リソースを集め、アプリケーションの上位レベルの方針に制御を渡すプラグインである。 例えば、開発用、テスト用、本番用のMainを用意することもできる。
詳細
- データベース
- ウェブ
- フレームワーク
これらは全て決定を最後まで遅らせるべき詳細にあたる。 どれもビジネスルールと密結合させるべきではない。
設計手順
アクターとユースケースの洗い出し
システムの最初のアーキテクチャを決めるための第一歩は、アクターとユースケースを見つけることだ。
システムに変更を要求する可能性のあるアクターを探しだす。 単一責任の原則からあるアクターへの変更が他のアクターに影響を与えないようにしたい。
次に、アクターごとに考えられるユースケースを洗い出す。 ユースケースにもレベルが存在し、上位の概念ほど抽象化が高い。 例えば、家具のカタログを閲覧すると言うユースケースはアクターが複数いる場合には抽象ユースケースとなる。 管理者としてカタログを閲覧するのか、購入者としてカタログを購入するのかは本質的に異なるが、上位のレベルでは同じものであるため、 カタログを閲覧すると言う抽象ユースケースを継承したユースケースと考えることができる。
コンポーネントアーキテクチャ
コンポーネントアーキテクチャについてはここまでで議論されてきたように、詳細になるに従って具象化させていくように切り分けていく。 切り分け方としては伝統的なビュー、プレゼンター、インタラクター、コントローラー、ユーティリティーなどが考えられるが、 こここそがアーキテクトの腕の見せ所であると考えられるので、それぞれのプロジェクトごとに考えていくしかない。
依存性管理
依存性についても先述の通り、制御の流れと逆向きに、抽象化の向きに沿って、管理していく。 ここで一つ重要なのが、アクターごとに独立に変更や機能追加ができるようにしておくことである。
パッケージング
レイヤーによるパッケージング
- web
- service
- data
などのレイヤーの名称によってパッケージを分割する方法。 特に考えることなくパッケージングができるため簡単ではあるが、 プロジェクトが大きくなるとこれらの分割だけでは可読性が大きく損なわれる。 また、アーキテクチャがドメイン知識について何か提示してくれることがない。
機能によるパッケージング
レイヤーによるパッケージングを水平方向での分割とすると、機能による分割は垂直方向の分割である。 ドメイン駆動設計の用語で言うところの集約ルートに基づいて分割する。 こちらの方がアーキテクチャがドメイン知識についてのヒントとなるだろう。
ポートとアダプターによるパッケージング
クリーンアーキテクチャのレイヤー図のうちビジネスルールを表す、「Entities」「Use Cases」はドメイン層にあたる。 その外側はインフラストラクチャ層であり、この二つは確実に分割されている必要がある。 基本的には上記のことを踏まえたレイヤーによるパッケージングになる。
コンポーネントによるパッケージング
関連する機能をよくできたクリーンなインターフェイスの向こう側に閉じ込めて、アプリケーションなどの実行環境の内側に置いたもの
とコンポーネントを定義する。 コンポーネントに従ってパッケージを分割していくことになるが、 上記の考え方と違うのは永続化のコードがドメインのコードと同様のパッケージに属することだ。 当然、パッケージ内で依存性のコントロールは行われるため、分割の箇所が変わるだけで原則に違反するわけではない。 こうすることで、コントローラ層から直接インフラストラクチャ層を呼び出すようなヤンチャができなくなる。 また、コンポーネントと言う単位で分割することによってパッケージの責務がわかりやすくなるので、 コード利用者も管理者もコードの把握が楽になると言う利点もある。
まとめ
自分の解釈では下記を満たしていればクリーンアーキテクチャである。
- 安定度が大きくなるほど抽象度を上げる
- 依存性逆転の法則を利用して下位のコンポーネントを上位のコンポーネントに依存させない
- コアドメインのビジネスルールは独立させる
- アプリケーション固有のビジネスルールはコアドメインのビジネスルールにのみ依存する
- 変更の多い箇所はHumble Objectパターンで分離する
- テストが難しい箇所もHumble Objectパターンで分離する
- 境界を越えるデータは単純なデータ構造を使用する (エンティティオブジェクトを渡したりしない)
- ビジネスルールはWeb, UI, DB, フレームワークなどの外部要素と独立してテストができる
上記事項をきれいに守るためのテクニックは多岐にわたる。 クリーンアーキテクチャの図として出回っている、かの有名な同心の図も一つの具体例に過ぎない。 パッケージングの方法についても同心円図の命名規則に合わせる必要はなく、 最後に言及があるようにドメイン駆動設計を念頭に分割していく方が理にかなっていると思う (し、これはクリーンアーキテクチャに違反しているわけでもない)。
上記を満たすための設計の切り口があることと切り口にしてはいけないものがあることも学べた。 切り口は
-
アクターとユースケース (垂直方向)
- アクターごとに独立して変更できるよう
- ユースケースがドメインオブジェクトで表現できるよう
- レイヤーレベル (水平方向)
であり、切り口にしてはいけないものは詳細であるフレームワークやデータベースである。
クリーンアーキテクチャはドメイン駆動設計を念頭においたアーキテクチャであると私は考えているが、 用いられる用語はそれぞれで幾分か違ってしまっている。 お前らがModelと呼ぶアレをなんと呼ぶべきか。近辺の用語(EntityとかVOとかDTOとか)について整理しつつ考える にてかなり詳しくまとめてくれているので参照されたい。
最後に、クリーンアーキテクチャを学んだところで設計の「正解」が判明するわけではないとわかった。 ただし、書籍に大量に記述されている原則と照らし合わせることで何が「間違えている」かの知識を多く得ることができたのではないかと感じている。 クリーンアーキテクチャを採用した設計という言葉を使っていたがこれからは避けよう。 あぁ、アーキテクチャなんもわからん。