クリーンアーキテクチャを読んだ感想
一部 イントロダクション
- 「リリースしたあとにコードをクリーンにすることはない。リリースした後も市場からの圧はなくならないから。」納得感しかない
- 変更できないプログラムは存在しないが、変更のメリットをデメリットが上回り変更できなくなってしまったプログラムは存在する。
当たり前のことだけで改めてそうだなと思うことが多かった。非エンジニアのマネージャーからの要望にできるかできないかだけの回答は不十分で、その変更がプロダクトコードにどのような影響があるのかをちゃんと伝える必要があって、できるけど工数がかなりかかるとか、なんで工数がかかるのかとか、その場凌ぎの修正はできるけどそれは負債としてインパクトはでかいよとか、エンジニアから伝えていかないとリリースできても気づいたら負債だらけのコードが山積みになって結果的に積む。
二部 プログラミングパラダイム
「構造化プログラミング」「オブジェクト指向プログラミング」「関数型プログラミング」がもたらすパラダイムの話。歴史的な話としておもしろかった。
これらのプログラミング手法は何かをもたらすのではなく何をすべきでないかの制約であるというのはなるほどなと思った。上記3つのパラダイムはそれぞれgoto文、関数ポインタ、代入を奪っている。
三部設計の原則
SOILID原則の話
単一責任の原則(SRP: Single Responsibility Principal)
- どのモジュールもたった一つのことを行うべきというのは誤解。
- 正しくは**「モジュールを変更する理由はたった一つであるべきである」**だそうです。
- もっと言うとソフトウェアに手を加えるのはユーザーやステークホルダーでありそれらがモジュールを変更する理由となる。
- そして、変更を希望するユーザーたちは複数となるためそれらをアクターというグループにすると
- **「モジュールはたった一つのアクターにたいして責務を負うべきである」**が正しい理解であるそうです。
オープン・クローズドの原則(OCP: Open-Closed Principal)
- ソフトウェアの振る舞いは既存の成果物を変更せず拡張できるようにすべきである
- ちょっとした拡張のために大量の書き換えが必要なのであればそれは設計が破綻している可能性がある
- この原則が一番言いたいことは変更の影響を受けずに拡張しやすくすることである。
単一責任の原則は関数やクラスに対しての原則だが、OCPはそれらの塊であるコンポーネントに対しての原則。適切に分割されたコンポーネントの階層はOCPに従っており、下位のコンポーネントが修正されても上位のコンポーネントは影響を受けない。
一番重要で保護したいのはビジネスロジックであり、そのコンポーネントはあらゆる下位コンポーネントの変更を受けないように中心にあり、それ以外はビジネスロジックの関心ごとにすぎない、、みたいなのがクリーンアーキテクチャのあの円につながってくのかな
リスコフの置換原則(LSP: Liskov Substituation Principal)
有名な正方形・長方形の例があるそう
正方形は長方形の適切な派生型ではないのでリスコフの置換原則に当てはまらないよ
RESTfulなAPIにせよ、Javaのクラス設計にせよ将来的に置換可能な設計にしていないと複雑で特殊な実装を追加するはめになる。(Javaなどであればインターフェイスを適切に使おうねという話)
インターフェイス分離の原則(ISP: Interface Segregation Principal)
使わない機能を含んだ似たようなインターフェイスを使いまわさず、適切に分離しようねという話。余計な機能は結局依存関係を増やし、あとあと辛くなるから。
依存関係逆転の原則(DIP: Dependency Inversion Principal)
- 依存関係は具象ではなく抽象に依存しようねという教え。
- インターフェイスは具象実装よりも壊れにくい。
- 具象実装を変化させてもお抽象のインターフェイスは何も影響を受けない。
- 変化しやすい具用クラスを参照しないためにAbstract Factoryパターンが使える。
- 上記のデザインパターンを使うのは具象型をnewするときにどうしても具象型への依存ができてしまうから。
四部 コンポーネントの原則
SOLID原則がレンガを組み合わせて壁や部屋を作る方法を伝える原則だとすると、コンポーネントの原則は部屋を組み合わせて建物を作る方法を伝える原則である。
再利用・リリース等価の原則
一つのクラスやモジュールをまとめたコンポーネントはリリース可能でなければならない。また、コンポーネントは一貫したテーマをもったクラスやモジュールの集まりである。
閉鎖性共通の原則
これは単一責任の原則をコンポーネント向けに言い換えたもので、コンポーネントを変更する理由が複数あってはならないという原則。
全再利用の原則
これはインターフェイス分離の法則を一般化したもので、どちらも余計なものには依存するな的な原則。
上記3つの原則は三角形の関係にあり、ちょうどいい感じに真ん中に開発しているプロジェクトがなるように近づけていくことが大事。全て完璧にはならない。
非循環依存関係の原則
循環依存は良くないよと言う話。
循環依存を解決するには依存性逆転の法則を使い、抽象クラスを間に挟むようにすればいい。
安定依存の原則
安定度の高い方向に依存しようねという原則。
安定したコンポーネントとは変更しづらいコンポーネントのことであり、これが不安定なコンポーネントに依存してしまうとコンポーネントの変更がしづらくなってしまう。
安定度について
安定したコンポーネントとは何か?安定したコンポーネントとは多数のコンポーネントから依存されているコンポーネントのことである。なぜなら、少しの変更でも依存されている多数のコンポーネントの調整が必要だからである。容易に変更できないから安定してるよねと言うこと。
不安定なコンポーネントは依存しているだけでどこからも依存されていないコンポーネントのことである。どこからも依存されていないということはどこにも責任を負っていないということである。
安定度の指標を計算するにはそのコンポーネントの依存しているコンポーネントの数と依存されているコンポーネントの数を使用して計算する。
全てのコンポーネントが安定している必要はない。
変更されやすいコンポーネントと変更が少ないであろう安定したコンポーネントが通常混在している。
全てが安定したコンポーネントだと常に変化する現実のビジネスロジックを反映できないだろう。
抽象コンポーネント
抽象コンポーネントは実装のないコンポーネントなので非常に安定したコンポーネントなので安定度の低いコンポーネントが依存する対象としては理想的。
プログラムを書く時に何かとインターフェイスに依存するようにするのはこういった理由があるから。
安定度・抽象度等価の原則
コンポーネントの抽象度はその安定度と同等でなければならない。
安定度の高さがコンポーネントの拡張性を妨げないように安定度の高いコンポーネントは抽象度も高くあるべきだ。逆に、安定度の低いコンポーネントは具象クラスであるべきで、容易にコードを修正できるようにしておくことが大事。
苦痛ゾーン
安定度マックスの具象コンポーネント。抽象度が低いため拡張することができず、安定度が高いため容易にコードを修正することもできない。基本的にはこのゾーンにあるクラスは辛いだけなのでこのゾーンを抜けるように修正すべき。
ただし、例外としてデータベーススキーマとユーティリティークラスがある。データベーススキーマは抽象度が極めて低く、変更がされやすく、他のコンポーネントに依存されまくっている。そのため、データベーススキーマの変更作業はだいたい辛くなる。ユーティリティーは様々なところで使われる具体的な実装クラスであることが多いが、変更されることは少ない。
無駄ゾーン
これはどこからも使われていない抽象クラスのことである。修正を繰り返すとだいたいこのようなクラスが混じってるもの。即刻削除するべき。
第五部 アーキテクチャ
優れたアーキテクチャは時間の経過とともに変化していくことまで想定して適切に設計する。
適切に設計されたシステムは、モノリスからマイクロサービスへの変化も、マイクロサービスからモノリスへまた戻ることも容易にできる。ちゃんと各コンポーネントが独立していて切り離しが容易にできるようになってるから。
結局、モノリスがだめとかマイクロサービスがいいとかの話ではなく適切なコンポーネントの構成を保つことが重要。
優れたアーキテクチャはユースケースを中心にしているため、フレームワークやデータベース、ツールなどに依存しない。
アーキテクチャが優れていれば中心のユースケースを変更することなくフレームワークやデータベースを容易に変更することができる。
クリーンアーキテクチャにおいて変更に強い設計にするために抽象クラスやインターフェイスを予測して設計することは大事だけどやりすぎるとYAGUNIに違反するかもしれない。ここらへんはちゃんと予測して、無駄のないように必要で適切な設計をするしかない。
テストはシステムの最も外側であると考えられるがシステムの一部である。テストはそのテスト対象のクラスやコンポーネントに強く依存しているため少しのコードの変更で多数のテストが壊れてしまうといったことがよくある。
まじでよくある。この副作用として開発者が変更を嫌がり、開発が停滞するという本当によくない現象が起こる。こうならないためにも、変更が多いものへの依存を極力減らし、テストに関しても設計をする必要がある。
境界線
データベースはビジネスルールに関係ないので境界線を引く
この部分は異議を唱える人が多いだろうとあるけど、実際私の業務ではデータベースのスキーマをビジネスレイヤーの人が決めてたくらいなので密接だと思う人は多そうだし、私も密接だと思ってた。
クリーンアーキテクチャ
やっと出てきた同心円
- エンティティ、つまりビジネスルールが中心にあり、これが依存しているものはない。逆にエンティティ以外のレイヤーは全てエンティティに依存していると言える。そのため、円の外側にあるUIやDB、フレームワークなどのレイヤーがエンティティに影響を与えることはない。ビジネスルールは現実のビジネスの課題を表現したものであり、当然最も変更され、インパクトの大きい部分である。そのため、エンティティは変更されやすいように抽象化されていたほうがいいし、他からの影響を受けにくいよう必然的にレイヤーの中心に位置している。
- ユースケースにはアプリケーション固有のビジネスルールが含まれている。
- インターフェースアダプター。GUIのMVCアーキテクチャを保持するのはこのレイヤー。このレイヤーではユースケース、エンティティに便利な形式からデータベースに便利な形式にデータを変換する。円の内側であるユースケースやエンティティはデータベースについて何も知らない。データベースがSQLデータベースであれば全てのSQLはこのレイヤーに限定する必要がある。
- 一番外側の円はフレームワークやデータベースといった詳細が詰まっている。被害が抑えられるようにこれらは一番外側に位置している。
- こっちのが重要と言われている右下のやつ。これは境界線を越えるためのフローを表していて、インターフェイスアダプター層のコントローラーからユースケースの呼び出しに関しては抽象度の高いインターフェイスを使用していて、ユースケースからプレゼンターの呼び出しがしたい場合はインターフェイスアダプター層のインターフェイスを経由することで依存性逆転の法則を使い、依存の矢印が常に内側に向くようにしている。
- 境界を越えるデータはDTOを使用したり、構造体を使ったり、なんでもいいけど内側が外側を知ることのないよう変換して受け渡しをするべき。
Humble Objectパターン
アーキテクチャの境界の近くにはHumble Objectパターンが潜んでいる。Humble Objectパターンとはテストが容易な部分とテストが比較的難しい部分を分離するパターンのことである。
あんまり意識してなかったけど、この考え方は大事だなと思って、テストがしやすい部分を分離するのは関数やクラスのテストを書くにあたって大事な考え方だなと思う。
第六部 詳細
- データベースはシステムの詳細でビジネスルールとは一番離れた概念です
- webも詳細です
- フレームワークも詳細です
オニオンアーキテクチャ
Jeffrey Palermoが自身のブログで提唱。2008年くらい
With traditionally layered architecture, a layer can only call the layer directly beneath it. This is one of the key points that makes Onion Architecture different from traditional layered architecture.
従来のレイヤードアーキテクチャは直下のレイヤーしか呼び出すことはできなかったがオニオンアーキテクチャは内側のレイヤーを直接呼び出すことができる。
-
DBやUIが一番外側にあり、内側に向かっての一方向のみ依存関係があるのはクリーンアーキテクチャの概念と一緒
-
オニオンが強調しているのがレイヤーの直下のみでなく内側であれば直下のレイヤーを飛び越して呼び出せるということ。クリーンでは直下のみか?明言されてたか確認
-
内側のレイヤーは、インターフェースを定義する。 外側のレイヤーはインターフェースを実装する。これも、クリーンの考え方的には依存関係逆転の法則を使用し依存関係を内側への一方向にしようとするとそうするしかないんだけど、オニオンほど明言されてない、気がする。
Key tenets of Onion Architecture:
- The application is built around an independent object model
- Inner layers define interfaces. Outer layers implement interfaces
- Direction of coupling is toward the center
- All application core code can be compiled and run separate from infrastructure
- オニオンアーキテクチャーはDDDと親和性が高いがDDDでなくとも機能する。
- 外側のinfrastructureをいつでも剥がせるよという主張が目立った。
なにか具体的なものがあるのを期待したが結局オニオンも概念的なものなのでこれがオニオンアーキテクチャですみたいなものはない。とりあえず、クリーンもオニオンもSpringみたいなDIフレームワークならまだしもGoとかでやる場合、Registoryみたいなことを考えなきゃいけないけど、だいぶ具体的な実装の話なので書いてない。
クリーンアーキテクチャとの比較
どちらも関心ごとの分離を目的とした依存関係を意識したレイヤードアーキテクチャであり、本質は同じという前提。
- クリーンアーキテクチャのレイヤー名称が分かりづらい。(個人的感想)
クリーン
- フレームワークとドライバ
- インターフェイスアダプター
- アプリケーションのビジネスルール
- ビジネスルール
オニオン
- インフラストラクチャ
- アプリケーションサービス
- ドメインサービス
- ドメインモデル
オニオンの方が言いたいことがシンプルでわかりやすく感じる。
- アプリケーションのコアはビジネスでルールである。
- フレームワークやDBはアプリケーションのコアではなく、一番外側にあるべき。
- 依存関係が外から内への一方向で、それを保つために依存性逆転の法則を使用しているのは同じなのだけどオニオンはそこをあまり詳しく書いてないので簡単そうに見える。
具体的な実装について
個人の感想です。
オニオン
上記の例ではドメインをオブジェクトに言い換えている。
オニオンが提唱されているブログの例ではASP.NET MVC Frameworkが使用されていてControllerはフレームワークと密接であり、切り離すことはできないとしてインフラストラクチャに置かれている。
まあ、なんとなくわかりやすいけどいまいちわかんない。
- 中心がアプリケーションのコアになるオブジェクト。
- その外側でオブジェクトの永続化や検索などをするのでRepositoryのインターフェイスはここにくる
- アプリケーションサービス層はRepositoryインターフェイスに依存するUserSessionが置かれる
- 一番外側のインフラストラクチャ層のUIの部分にコントローラーとUserSessionの実装が置かれる。
この例で言うUserSessionがサービス的な役割だと思ってるのだけどだとしたら実装とインターフェイス両方ともアプリケーションサービス層でいいんじゃないのと思うんだけどなんでこうなるのかがわからない。UserSessionクラスがどういう役割のクラスなのかはわからないけどCookieみたいなセッションデータを表現しているからアプリケーションロジックとは関係ないよねで一番外側なの?
結論
クリーンがわからないからといってオニオンが全てを解決してくれるわけではない。わからないものはわからない。クリーンもオニオンもアーキテクチャであり、概念的なものなので。
と思ったけど
この記事がめっちゃわかりやすかった。
infrastructure, application, presentation, domainという名称はDDDからきてたのか。
DDDでこのレイヤー構造で関心ごとの分離はできたけどdomainがinfrastructureに依存しているのを依存関係逆転の法則で解決したのがクリーンやオニオンアーキテクチャなのか。レイヤーの名称としてはDDDがわかりやすいのでDDDの名称でディレクトリ分けしたクリーン(オニオン)アーキテクチャみたいなのが一番しっくりくるかも。
参考
大変充実した考察が書かれている
DDD + オニオン