著書
これを読んだ人
- エンジニア三年目
- Javaは半年ほど業務で使用したことがある
- フレームワークはSpring Bootを使った
- あのときは特有のアノテーションに苦しんだ記憶
- 受託開発の会社でエンジニアしています
- 今はFull TypeScriptの案件に携わることが多い
- クリーンアーキテクチャを実践したことはない
GitHub
自己紹介サイト
この記事を書いた目的
本を読むことで知識を得ることがすごく好きなのですが、以前から読んでも目が滑って内容をあんまり理解できておらず、実践できないみたいなことが自分の中でよくあり、これなら本を読んでいる意味がないので、まずは読んだ際の自分の解釈を文章化してアウトプットを行うことで知識の定着、活用がしやすいのではないかと感じたので書いてます。
もし内容的に書いてはまずいものがあれば教えて下さい。
全体的な感想
- アーキテクチャ図がめちゃくちゃ分かりやすい
- 依存関係が保守不可能な複雑性を生む
- 保守可能なコードを書くことで顧客、開発者ともに満足度が上がる
- 書籍の難易度は高め
- プログラミング初心者が見てもクリーンアーキテクチャやDIが必要な理由をなかなか理解できない気がする
- 業務で複雑なプロジェクトを味わい、より良い方法を模索しているエンジニア歴3~5年くらいの人にはかなり向いていると思われる
- サンプルコードがJavaで書かれているので、ある程度Javaの知識がないと意味を理解できないところが多々存在する
- これを実際に業務のコードに落とし込んでクリーンアーキテクチャを実践するにはかなりの技術力が必要だと感じる
- メンバー全員が技術に長けており、かつ作成するアプリケーションに対する豊富なドメイン知識がないと難しそう
- 最も重要な考えとして、ドメインに対するコードと処理のためのコードを分離することが大事なのかなと感じた(個人的な解釈なので間違っている可能性もあり)
- イメージとしてはあくまで手段としてのコードと、目的に対するドメインロジックのコードを分離し、アプリケーションの根幹をなすコードがどこにあるかすぐに確認、検証、修正可能にするためにクリーンアーキテクチャがあるのかなと
- プログラミングって究極的に言ってしまえばユーザーからのinputに対してoutputを出力するためだけのもので、手段や道具として永続化のためのDBやフロントエンドの画面があるという認識
- ならば重要なのはドメインロジック(inputからoutputをするためのコード)であって、それ以外の部分は依存をなくし、代替可能なコードにしておくべきかなと
以下は心に残った内容と自分なりの解釈を記載
気になった方はぜひとも本を購入し、手元に持っておくことをおすすめします。
はじめに
- ソフトウェア開発者の楽園とは
- ドメインに関するロジックのテストが作りやすい
- インフラや特定の技術をモックに置き換えることが容易
- 使用している技術を別の技術に置き換えることが簡単
- そのようなソフトウェアであれば、顧客が必要とする面倒な新機能をどのコードに実装すべきか迷うことがない
- クリーンアーキテクチャの基盤をなす考えの名称
- ヘキサゴナルアーキテクチャ
- ポート&アダプタ
- オニオンアーキテクチャなど
- 基本となる考え方
- ソフトウェア内のドメインに関するコードと技術的なコードをどのように分離するのか
- その際の依存の流れを内向きにする
- つまり、技術的なコードからドメインに関するコードに向かうようにする
- 期限に追われて短絡的なコードが増えてくると変更が難しくなる
保守容易性
- 保守しやすいプロジェクトだと顧客も開発者も満足度が上がる
- 機能追加時のパターン
- DRY原則を用いる
- テストのしやすさを向上するためにDIを用いる
- オブジェクトの生成をより簡単に行えるようにビルダーを導入する
- 実装に対して複数の選択肢がある場合には、常に将来発生するであろう変更に対して簡単にコードで対応できる手段を選択すれば良い
従来の多層アーキテクチャでは何が問題なのか
- 従来
- Web層
↓ - ドメイン層
↓ - 永続化層
- Web層
- Web層でHTTP経由でのリクエストを受け取り、ドメイン層のサービスに渡す
- サービスは何らかのビジネスロジックを実行するが、その際に永続化層にあるコンポーネントを呼び出してデータストアに格納されているエンティティのデータを取得する
- この従来のアーキテクチャでは変更に対して非常に脆弱になる
- 不適切な依存関係が確立されやすい
データベース中心の設計になってしまう問題
- この場合、Web層はドメイン層に依存し、ドメイン層は永続化層、つまりDBに依存する
- 従来のアーキテクチャではまずデータベース設計から考える
- なぜなら依存の流れに沿えるから
- そうではなく、何よりまずビジネスロジックから実装すべきである
- そうすれば開発者はビジネスロジックを正しく理解出来ているかどうかを技術的な要素を考えることなく判断できるようになる
- 本来であればビジネスロジックを正しく構築できたという確信を得てから、そのビジネスロジックの周りにある永続化層やWeb層の実装を始めるべき
- そうしないと永続化層とドメイン層が密結合になってしまう
問題のある短絡的な実装になってしまう問題
- 従来の多層アーキテクチャのルールではコンポーネントは同じ層かそれより下層のものに対してのみ呼び出し可能というルールしかない
- 一度短絡的な実装を許してしまうと割れ窓理論でどんどん実装に対する基準が下がってしまう
テストがしづらくなってしまう
- 多層アーキテクチャでは層を飛び越えたアクセスを行う実装が増えてしまう
- これにより、ビジネスロジックがWeb層でも実装されるようになってしまう
- また、Web層のテストをする際にドメイン層の依存だけではなく、永続化層の依存も制御する必要が出てくる
- このため、テストコードでは両方の層のモックが必要になり、テストコードが複雑になってしまう
- こうなるとテストコードの作成に時間がかかるようになり、テストコードを書かなくなってしまう
ユースケースが見つけづらくなる問題
- あたし異コードを書くことよりも、既存のコードを変更することの方に多くの時間が割かれる
- そのため、アーキテクチャは新たな機能を追加したり、既存の機能を変更したりする箇所がコードベースのどこにあるのかを速やかに把握できる構造になっていることが重要
- これは多層アーキテクチャでは行いにくい
- なぜならビジネスロジックをドメイン層ではなく、Web層に記述してしまうから
- また単一のサービスに複数のユースケースを扱ってしまい、肥大化したサービスになってしまうから
異なる作業を同時に行うことが難しくなる
依存関係の逆転
- SOLID原則のSとDにあたる単一責任の原則
- 依存関係逆転の原則
単一責任の原則
- コンポーネントを変更する理由は、ただ一つになるようにすべき
依存関係逆転の原則
-
コードベース内の依存関係は、いかなるものであっても、その方向をひっくり返すことができる
-
クリーンアーキテクチャでは全ての依存はビジネスロジックに向かう
-
エンティティ
↓ -
ユースケース
↓ -
コントローラー
↓ -
UI
ヘキサゴナルアーキテクチャを使用して、アプリケーション層の中にドメイン層を閉じ込める
ヘキサゴナルアーキテクチャ
- エンティティ
- ユースケース
- 受信ポート
- 送信ポート
- 受信アダプタ
- 送信アダプタ
パッケージ構成に関する戦略
層を意識したパッケージ構成
- web(web層)
- domain(ドメイン層)
- persistence(永続化層)
これではどの機能のコードがどこにあるのか分からない
そこで、機能を意識したパッケージ構成にする
- account
- SendMoneyController
- AccountRepository
- AccountRepositoryImpl
- SendMoneyService
同じパッケージ内でしか使われないクラスをパッケージプライベートにする
これをスクリーミングアーキテクチャと呼ぶ
ユースケースの実装
- ドメインモデルの実装
- ここはAccountなどのクラスを作る
- ユースケースの概要
- 入力値を受け取る
- ビジネスルールに関する妥当性確認を行う
- ドメインモデルの状態を変える
- 処理結果を返す
- つまりユースケース層ではサービスを作る
Webアダプタの実装
- ここではinterfaceやコントローラーを定義する
処理内容
- 送られてきたHTTPリクエストをプログラムで利用可能なオブジェクトに変換する
- 認証/認可の確認を行う
- 入力値の妥当性確認を行う
- 入力値をユースケースの入力モデルに変換する
- ユースケースを呼び出す
- ユースケースの処理結果をHTTPレスポンスに変換する
- HTTPレスポンスを返す
永続化アダプタの実装
- 従来の多層アーキテクチャでは最終的に全てのものが永続化層に依存してしまい、データベース中心の設計になってしまう
- この依存の向きを逆にして、永続化アダプタをアプリケーション層に差し込む
永続化アダプタの責務
永続化アダプタの処理手順
- 入力モデルを受け取る
- 受け取った入力モデルをデータベースに対して操作を行えるものに変換する
- その変換したものを使ってデータベースを操作する
- データベースから返ってきた結果をアプリケーションが扱える出力モデルに変換する
- その変換した出力モデルを返す
アプリケーションの核に対して行うデータベースへの操作を提供する送信ポート(インターフェース)をどのように分割するのか?
-
必要としないお荷物を抱えたものに依存していると、予期せぬトラブルのもとにつながる
-
この問題に対する答えがインターフェイス分離の原則
-
LoadAccountPort
-
UpdateAccountStatePort
-
CreateAccountPort
みたいに各サービスは実際に必要とするメソッドだけに依存できるようにする
アーキテクチャの構成要素に対するテスト
テストピラミッド
- システムテスト
- 統合(integration)テスト
- 単体テスト
さらにシステムテストの上にE2Eテストがあったりする
- 単体テスト
単一のクラス依存がある場合はモックに置き換える
- 統合テスト
関係のある複数の単体をつなぎ合わせた1つのグループを構成し、そのグループの入口となるクラスを呼び出してテスト対象の処理を実行させることで、そのグループ外としたように機能するかを検証する
- システムテスト
テスト対象のユースケースがアプリケーションのすべての層を経由して意図したように機能するかを検証する
テストの戦略
- ドメインエンティティを作成するあいだに、そのドメインエンティティを検証する単体テストを用意する
- ユースケースを実現するサービスを作成する間にそのサービスを検証する単体テストを容易数r
- アダプタを作成する間にそのアダプタを検証する統合テストを用意する
- システムテストではテスト対象となるアプリケーションのユーザーがそのアプリケーションを介して行うであろう一連の操作に関連するシナリオの中で特に重要なシナリオを検証する
大切なことは機能の実装が終わったあとにテストするのではなく、機能を実装している間にテストを行う
テストを作成するときにモックを作るのが大変なのであればそれは何かが間違っているということ
境界を超える際のモデルの変換
- 異なる層と遣り取りをする差異、どのようにモデルを変換するのか
戦略
- モデルを変換しない戦略
- 双方向でのモデルの変換
- 徹底的なモデルの変換
- 一方向でのモデルの変換
どれを選ぶかは時と場合による
アプリケーションの組み立て
- 依存する方向を正しい向きにする
- 全ての依存関係は内側へ向かうようにすべき
- (ドメイン層に向かって依存すべき)
- ヘキサゴナルアーキテクチャではユースケースと永続化アダプタとの間に送信ポート(インターフェース)を配置することで、永続化アダプタのことを知らなくても済むようにする
- これを行うとテストしやすくなる
- なぜならモックを渡せるのでテスト対象のクラスをほかのクラスから隔離できる
- これを行うために依存するオブジェクトを生成する責務を担う構成コンポーネントを用意する
構成コンポーネント
- 一番外側の層に配置する
- Webアダプタのオブジェクトを生成すること
- HTTPリクエストが確実にWebアダプタに渡されるようにすること
- ユースケースのオブジェクトを生成すること
- Webアダプタにユースケースのオブジェクトを渡すこと
- 永続化アダプタのオブジェクトを生成すること
- ユースケースに永続化アダプタのオブジェクトを渡すこと
- 永続化アダプタがデータベースへ確実にアクセスできるようにすること
- これをJavaで全部書こうとすると大変なので、Springなどのフレームワークを用いる
短絡的な実装への意図した選択
-
割れ窓理論
- これは実際に開発しててまじまじと感じる
- 短絡的な実装をせざるを得ないときはADRに残す
-
ユースケース間でモデルを共有しない
-
ドメインエンティティを入力モデルや出力モデルとして使うことに決めたのであれば、プロダクトの成長とともにそのドメインエンティティを特定のユースケースでしか使わない入力モデルや出力モデルに置き換える必要が出てくる
- そうしないと初期段階ではユースケースが単純なCreateやUpdateしか扱わないときは問題ないが、ユースケースが成長してデータベースに格納しない情報も扱うようになり、より複雑なビジネスロジックを扱うことも要求されてくるとユースケースに対する専用の入力モデルや出力モデルが必要になってくるから
- この場合にユースケースの変更が原因でドメインエンティティまで変更する必要が出てくる
-
受信ポートは取り除くと受信アダプタとアプリケーション層との間にあった抽象化層がなくなり、シンプルにできる
- しかし、受信ポートはアプリケーションの核への入口がどこなのかを示しているため、目的の有ーすケースを実施させるためには開発者がアプリケーションの内部構造を理解して、度のサービスのどのメソッドを呼び出さなくてはならないのかを把握することが必須になる
- アプリの複雑度によって柔軟に設定する
-
サービスの省略
- 単純なCRUD操作しか行わないユースケースの場合、サービスはビジネスロジックを扱わず、永続化アダプタの呼び出ししか行わない
- そのため、ユースケースにサービスを経由させて永続化アダプタを呼び出させるのではなく、永続化アダプタにユースケースを直接実装させる選択がある
- 単純なCRUD操作しか行わないユースケースの場合、サービスはビジネスロジックを扱わず、永続化アダプタの呼び出ししか行わない
-
しかし、この場合は受信アダプタと送信アダプタは入力モデルや出力モデルとしてドメインエンティティを共有することになる
-
加えてアプリケーションの核にはユースケースを表現するものがなくなってしまう
- そうなるとCRUD操作しか行わない予定だったユースケースに対してあとから複雑なビジネスロジックを加えることが決まった場合、そのユースケースはすでに送信アダプタに実装されているため、新たなビジネスロジックも送信アダプタのコードに追加されてしまう
- こうなるとビジネスロジックがいたるところに散在してしまい、保守が難しくなる
アーキテクチャ内の境界の維持
- アクセス修飾子を用いるかパッケージプライベートを使う
- publicなクラスに対して依存ルールを定義するには適応度関数を使う
- CIの中で検知する
- ArchUnitというツールが有る
複数の境界づけられたコンテキストの管理
- 異なるドメインの間には境界がある
- 適切にドメインごとにコードを分離しないとドメイン同士が密結合になり、コードに変更を加えたら別ドメインの機能が機能しないということが起きてしまう
境界づけられたコンテキストごとに一つのアプリケーションの核を用いる場合
- 核同士の結合にはドメイン接続アダプタを用いる
- このアーキテクチャでは境界づけられたコンテキストの数が増えるにつれて依存の数が急激に増える
- このため、コンテキストが4つの場合、6つのドメイン接続アダプタが必要になる
- ヘキサゴナルアーキテクチャはアプリケーション全体をカプセル化することを目的としている
各境界づけられたコンテキストの分離
- 受信ポートと送信ポートを分離することで境界づけられたコンテキストを完全に分離することができる
境界づけられたコンテキスト同士の適切な連携
- 各コンテキスト内のドメインサービスがそれぞれ異なるデータモデルを維持できるように、各境界付けられたコンテキストは個別のポートを持つのが理想
- とはいえ場合によってはこの分離は必要ではないと判断しデータベースへの送信ポートを共有して同じデータモデルを共有することもあるかもしれない
- ただし、そのようなことを選択した場合はそれらの境界づけられたコンテキスト同士が密結合してしまうリスクをきちんと認識して置かなければならない
コンポーネント基盤のアーキテクチャ
- ソフトウェア開発が始まった時点では、開発者はエンドユーザーがそのソフトウェアに対して求めていることをすべて知ることはできません
- そして、エンドユーザーが実際に求めていることを開発者が理解できるようになるのは、そのエンドユーザーがその父祖とウェアを実際に触るようになった時になる
- そのため、開発者は開発時点では「仮定」しないといけない
- ヘキサゴナルアーキテクチャは保守の歯やすさを追求するアーキテクチャである
- それを実現するためにアプリケーションの核と外部の世界との間に境界を設けている
コンポーネントの抽出と抽出したコンポーネント同士の連携
- 保守容易性を促進させるものの一つはモジュール化のしやすさ
- モジュール分割が適切にされていなければ何をどこまで把握すればよいのかわからなくなり、コードのあらゆる箇所を見に行かなくてはならなくなる
アーキテクチャの決定
最初はシンプルに
- 長い目で見た場合、どのアーキテクチャが最も優れているかを知ることはできず、ソフトウェアの成長に従ってアーキテクチャを変えないといけない可能性は十分にある
- これに対応するためには、ソフトウェアは変更に対する柔軟性を持っていなくてはならない
- 依存関係があると切り離せなくなり、変更ができなくなる
- ヘキサゴナルアーキテクチャを採用しても結果として不必要な複雑さを持ちこんでいるだけになってしまう場合もある。
ドメインの進化
- 仮にCRUD操作しか持たないシンプルなアプリケーションとして始まったとしても時間が経つにつれ、多くのビジネスルールを扱う豊富なドメインを表現するコードで構成されたアプリケーションに変わっていくこともある
- 最も重要なことは、ドメインに関するコードをドメインとは関係ないことから束縛されずにソフトウェア開発が行えること
自身の経験への信頼
- 人間は今までのやり方を変えることは難しい
最後に
- TypeScript
- Next.js App Router
- Prisma
みたいな技術構成でクリーンアーキテクチャを実践している参考リポジトリみたいなのがありましたら、ぜひとも共有してもらえるとすごく助かります。
実践されているコードを見たい。