clean-architecture について
Robert C. Martin の Clean Architecture 原則に基づき、コードレビューと設計を支援するスキルです。レイヤー間の境界や依存関係を確認したい開発者向けに、検索ツールを使った検証方法も示します。
主な機能
- 01レイヤー分離と依存方向の確認
- 02ポート、アダプター、DTO境界の点検
- 03SOLID原則の評価
- 04フェイク実装の置き換え方の提示
- 05検索ツールによるインポート検証
活用シーン
- 01依存ルール違反のコードレビュー
- 02ドメイン層とプレゼンテーション層にまたがる機能設計
- 03フェイクバックエンドの置き換え可能性の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ nathankim0/ clean-architecture-skills --skill clean-architectureインストール後、Claude Code で/clean-architectureを入力して読み込まれたか確認します。
詳細
クリーンアーキテクチャのレビュー用スキル
概要
このスキルでは、Robert C. Martin(Uncle Bob)のクリーンアーキテクチャの原則に基づいて、コードを分析・レビューします。
使用するタイミング
- ユーザーから「クリーンアーキテクチャ」のレビューを依頼されたとき
- 「アーキテクチャレビュー」「依存関係の確認」「レイヤー分離」について尋ねられたとき
- クリーンアーキテクチャに基づいて新機能を設計するとき
- 機能をドメイン、データ、プレゼンテーション、依存性注入のモジュールに分割するとき、またはフェイクを実際のバックエンドに置き換えるとき
手順
1. プロジェクト構造の分析
まず、プロジェクトのディレクトリ構造を把握します。
- レイヤーが分離されているか確認する
- モジュール/パッケージ構造を調べる
- 依存関係の方向を特定する
- 既にプロジェクトのレイヤー構造に沿っている機能を見つけ、独自の形式を考案するのではなく、その規約(ファイル名、ポートの形、実装の注入方法)を再利用する
レビューではなく設計を行う場合は、コードを書く前に以下を決めます。
- このアプリケーションが担うルールと、外部の権威(サーバー、決済プロバイダー、広告ネットワーク)が決定するルールはそれぞれ何か?
- 内側のレイヤーに必要なポートは何か。また、それぞれのポートにはどの操作を持たせるか?
- 実装を選択するコンポジションルートはどこか?
- 今必要なフェイクまたはモックアダプターは何か。また、後で何に置き換えるのか?
2. 依存関係のルール
基本原則:ソースコードの依存関係は、内側(より高レベルの方針)に向かうものだけでなければなりません。
┌─────────────────────────────────────────────────────────┐
│ Frameworks & Drivers │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Interface Adapters │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Use Cases │ │ │
│ │ │ ┌─────────────────────────────────┐ │ │ │
│ │ │ │ Entities │ │ │ │
│ │ │ └─────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
← Dependencies flow inward
チェックリスト:
- 内側のレイヤーが外側のレイヤーを参照していませんか?
- エンティティはユースケース、アダプター、インフラストラクチャを認識しないようになっていますか?
- ユースケースはアダプターやインフラストラクチャを直接参照しないようになっていますか?
- 境界を越える際にDIPが適用されていますか?
- プレゼンター、ビューモデル、コントローラー、UIコンポーネントは、コンポジションルートやDIコンテナーをインポートしないようになっていますか?
2-0. コンポジションルートは最外層
具体的な実装を選択するモジュール(DIコンテナー、サービスロケーター、provide*ファクトリー、アプリのエントリーポイント)はすべてに依存するため、その内部にあるものから依存してはなりません。
- アプリケーションのエントリー、ルートコンポーネント、
mainなど、境界の1か所で実装を接続し、コンストラクター、パラメーター、コンテキスト/プロバイダーを介して内側へ渡す - 内側のモジュールがDIモジュールを呼び出して依存関係を取得すると、具体的な依存関係を関数呼び出しの背後に隠したことになります。具体的なクラスをインポートしていなくても、これは依存関係のルールに違反します
- フェイクを実際の実装に置き換えるとき、変更が必要なのはコンポジションルートだけにする
// Bad: the view model reaches out to the container
class InventoryViewModel {
private repository = AppContainer.inventoryRepository();
}
// Good: the composition root decides and injects
class InventoryViewModel {
constructor(private repository: InventoryRepository) {}
}
const viewModel = new InventoryViewModel(createInventoryRepository(config));
2-1. 境界を越える
制御の流れとソースコードの依存関係は、反対方向を向く場合があります。
Controller → Use Case → Presenter
↓ ↓ ↑
[dependency] [dependency] [inverted!]
例:ユースケースがプレゼンターを呼び出す必要がある場合
- ユースケースからプレゼンターを直接参照すると、依存関係のルールに違反します
- 解決策:ユースケース内で出力ポート(インターフェース)を定義し、プレゼンターがそれを実装する
// Use Case Layer (inner)
interface OrderOutputPort {
presentOrder(order: OrderData): void;
}
class CreateOrderUseCase {
constructor(private outputPort: OrderOutputPort) {}
execute() {
// ... business logic
this.outputPort.presentOrder(orderData);
}
}
// Adapter Layer (outer) - depends on Use Case
class OrderPresenter implements OrderOutputPort {
presentOrder(order: OrderData): void {
// Transform for UI
}
}
2-2. 境界を越えるデータ
原則:境界を越えるデータは、単純なデータ構造(DTO)にする必要があります。
// Bad: Passing Entity directly
class GetUserUseCase {
constructor(private userRepository: UserRepository) {}
execute(id: string): Promise<User> { // Returning Entity directly
return this.userRepository.findById(id);
}
}
// Good: Transform to DTO
class GetUserUseCase {
constructor(private userRepository: UserRepository) {}
async execute(id: string): Promise<UserResponseDTO> {
const user = await this.userRepository.findById(id);
return { id: user.id, name: user.name };
}
}
チェックリスト:
- エンティティやデータベースの行を直接、境界の外へ渡していませんか?
- 外部フレームワークのデータ形式が内側のレイヤーに入り込んでいませんか?
- 境界で使うデータ構造は、すべてのレイヤーに冗長なDTOを追加することなく、内側のレイヤーに必要な情報を提供していますか?
3. レイヤー構造のレビュー
エンティティ(ドメインレイヤー)
- 中核となるビジネスルールを含む
- 外側のレイヤーのフレームワーク、永続化、UIから独立している
- 変更される可能性が最も低い
チェックリスト:
- エンティティはフレームワークに依存しないようになっていますか?
- 純粋なビジネスロジックだけを含んでいますか?
- インポートするライブラリーは、インフラストラクチャ上の関心事を持ち込むのではなく、ドメインの独立性を保っていますか?
- ドメインは、外部の権威が担うルールを再実装していませんか?(3-1を参照)
- 表示のしきい値、文言、色、書式など、プレゼンテーションにのみ関係するルールを含んでいませんか?
3-1. ポリシーの所有者は誰ですか?
クォータ、適格性、価格、報酬額、有効期限などのルールを外部の権威が決定する場合、クライアントはそのルールを自らのドメインに重複して持つべきではありません。ルールが重複すると内容にずれが生じます。権威側がルールを変更しても、次のリリースまでクライアントには古い数値が表示されます。
- クライアントが計算に必要とする未加工の入力値ではなく、ポートから判断結果そのもの(
remainingToday、nextGrantAmount、expiresAt)を返す - アプリケーションが真に所有するルールだけをドメインに残す
- 権威がまだ存在しない場合は、ドメインではなく、その権威の代わりとなるフェイクアダプターに模擬ポリシーを置く
3-2. プレゼンテーションルールはプレゼンテーション層に置く
純粋関数だからといって、自動的にドメインルールになるわけではありません。「アイテムの有効期限が3日以内に切れる場合に警告を表示する」は表示上の判断です。3を2に変えても、ビジネス上の結果は変わりません。このようなしきい値、ラベル、カウントダウンの書式設定はプレゼンターやビューモデルに置き、通知ジョブなどのビジネスプロセスで同じルールが必要になった場合にのみ、ドメインへ移してください。
ユースケース(アプリケーション層)
- アプリケーション固有のビジネスルール
- 機能を実装するためにエンティティをオーケストレーションする
- 外部システム向けのインターフェースを定義する
チェックリスト:
- 各ユースケースの責務は単一ですか?
- 入出力ポートは明確に定義されていますか?
- 実装ではなく、リポジトリーインターフェースを使用していますか?
インターフェースアダプター
- データ変換を処理する
- コントローラー、プレゼンター、ゲートウェイを含む
- MVC、MVVMなどのパターンを適用できる
チェックリスト:
- DTOとエンティティ間の変換はここで行われていますか?
- 外部形式と内部形式が明確に分離されていますか?
- UIコンポーネントは薄く保たれていますか?また、状態遷移、ロック、キャンセル、コピーの選択は、UIなしでも読み取れるプレゼンター、コントローラー、またはビューモデルに移されていますか?
フレームワークとドライバー(インフラストラクチャ層)
- DB、Webフレームワーク、外部サービス
- 主にグルーコード
- 最も変更される可能性が高い
チェックリスト:
- フレームワークやデータベースの詳細は、エンティティやユースケースの外に保たれていますか?
- リポジトリーの実装は、アダプター層とインフラストラクチャ層のどちらに配置されていても、内側のポートに依存していますか?
- 永続化形式やワイヤー形式(シリアライザー、スキーマから生成された型)はエンティティと分離され、アダプター内で明示的にマッピングされていますか?
3-3. フェイクとモックはアダプターです
フェイクバックエンド、インメモリストア、モックSDKは、同じポートを実装する外側の層です。
- 実際のアダプターの隣に配置し、ドメインやUIの中には決して置かない
- コンポジションルートでのみ選択し、内側のコードに触れずに実際の実装へ置き換えられるようにする
- 冪等性や失敗時の結果も含め、実際の実装と同じ契約を持たせ、切り替えても呼び出し元を変更せずに済むようにする
3-4. デバッグ操作を本番用ポートから切り離す
開発環境やステージング環境でのみ使うリセット、シード、時間移動の操作は、本番用ポートに含めるべきではありません。含めると、すべての実際の実装にそれらの実装が必要になります(ISP)。これらは別個の任意の機能として公開し、該当環境でのみコンポジションルートから提供してください。
3-5. 共有状態の所有者を1つにする
同じポートのデータを複数の画面やコンポーネントで表示する場合、1つの所有者(アダプター層のプロバイダー、ストア、キャッシュなど)がリポジトリーのインスタンスとその状態を保持し、他の画面やコンポーネントはそれを購読します。
所有者がいない場合の兆候:
- 各コンシューマーがそれぞれ独自のリポジトリーインスタンス、ローダー、タイマーを作成する
- 同じリクエストがコンシューマーごとに1回ずつ送信される
- ある画面で変更を加えた後、別の画面では再読み込みするまで古い値が表示される
フォーカス、アプリの再開、スケジュールの境界、変更後などの更新トリガーを所有者に集約し、同時に発生した読み込みを1つのリクエストにまとめます。カウントダウンの刻々とした更新など、表示だけに使うタイマーは、その表示を行うコンポーネント内に置いてください。そうすれば、共有状態によってすべての購読者が再レンダリングされることはありません。
3-6. アダプターの契約を推測しない
実際のバックエンド契約(API 仕様、GraphQL スキーマ、SDK コールバック)がまだ存在しない場合、作り上げた契約に基づいて「実際の」アダプターを実装したり、コンパイルを通すために生成された型を回避したりスキーマを手編集したりしないでください。
- ポートとフェイクアダプターは維持します。ポートの形状は、推測したサーバーのレスポンスではなく、内側のレイヤーが必要とするものから決まります
- バックエンドチーム向けに、期待する契約(操作、フィールド、エラーケース、冪等性キー)を書き残します
- 契約が公開されたら実際のアダプターを追加し、コンポジションルートで差し替えます
4. SOLID 原則のレビュー
クリーンアーキテクチャの実装で使用されるオブジェクト指向設計の原則です。DIP は境界の越え方に不可欠です。
| 原則 | 説明 | クリーンアーキテクチャとの関連 |
|---|---|---|
| SRP | クラスが変更される理由は 1 つだけであるべきです | レイヤーごとの責任の分離 |
| OCP | 拡張に開かれ、変更に閉じている | インターフェースによる拡張 |
| LSP | サブタイプは基底型と置き換え可能でなければなりません | 実装の置き換え可能性 |
| ISP | 使用しないインターフェースに依存してはいけません | ポートインターフェースの分離 |
| DIP | 具象ではなく抽象に依存します | 境界を越える際の中核 |
5. 機械的に検証する
フォルダー名だけを当てにしないでください。たとえば、プロジェクトの検索ツールを使って各レイヤーが実際にインポートしているものを一覧にします。
# Inner layers must not reference implementations, frameworks, or the composition root.
# Port interfaces such as `UserRepository` are expected in the domain, so search for
# implementation names and wiring instead of the word "Repository".
rg -n "Impl\b|Container|Module\.provide" src/domain
rg -n "Impl\b|AppContainer|Module\.provide" src/presentation src/ui
# Fakes must be referenced only by adapters and the composition root
rg -n "Fake|Mock" src --glob '!src/data/**' --glob '!src/di/**'
モジュール名はプロジェクトの規約に合わせて調整し、該当箇所をそれぞれ読みます。一致箇所は手がかりであり、結論ではありません。
違反ごとに、ファイルと行、内側のモジュール、そこから到達する外側のモジュール、そしてそれを修正する境界を報告します。
出力形式
レビュー結果は次の形式で提示します。
# Clean Architecture Review Report
## Summary
- Overall Assessment: [Good/Needs Improvement/Critical]
- Key findings summary
## Dependency Rule Analysis
- List of violations
- Improvement suggestions
## Layer Structure Review
### Entities: [Good/Needs Improvement]
### Use Cases: [Good/Needs Improvement]
### Interface Adapters: [Good/Needs Improvement]
### Infrastructure: [Good/Needs Improvement]
## Recommendations
1. High priority improvements
2. Medium priority improvements
3. Long-term suggestions
重要な注意事項
クリーンアーキテクチャは万能ではありません
- すべてのプロジェクトで、厳密に 4 つのレイヤーが必要なわけではありません
- 依存関係のルールに従っている限り、レイヤーの数は柔軟に決められます
- プロジェクトの規模と複雑さに応じて適用してください
避けるべき一般的な落とし穴
- レイヤーへの過度なこだわり:レイヤーの分離そのものが目的ではありません
- 過度な抽象化:有用な境界を設けずにインターフェースを追加することです。ポートによってビジネスルールをインフラストラクチャから分離できる場合は、実装が 1 つあれば十分です
- 表面的な導入:実際の依存関係のルールを無視し、IDE のリファクタリング機能に頼ることです
- 将来を見越した過剰設計:起こりそうにない変更に備えて複雑さを加えることです
重要な質問
レビュー中は、常に次の質問を検討してください。
- 「この分離は実際にメリットをもたらしますか?」
- 「依存関係のルールは本当に守られていますか?」
- 「変更時の影響範囲は最小限に抑えられていますか?」
実践的な検証
クリーンアーキテクチャから本当にメリットを得られているか、自分に問いかけてください。
- 役に立っているのはレイヤーの分離ですか、それとも IDE の自動リファクタリングだけですか?
- この Repository インターフェースは、実装の数にかかわらず、ビジネスルールをインフラストラクチャから分離できていますか?
- 将来変更が起きる可能性が 0.001% しかないのに、過剰に抽象化していませんか?
- 外側のモジュールのクラス名を変更しても、内側のモジュールに影響しませんか?
言語別の例
TypeScript/JavaScript
// Good: Dependency Inversion
interface UserRepository {
findById(id: string): Promise<User>;
}
class GetUserUseCase {
constructor(private userRepository: UserRepository) {}
}
// Bad: Depending on concrete implementation
class GetUserUseCase {
constructor(private userRepository: PostgresUserRepository) {}
}
関数型言語(OCaml、ReScript、F#、Elm 風レコード)
ポートにクラスは必要ありません。関数のレコードもポートであり、コンポジションルートがそれを構築します。
(* Domain: port as a record of functions *)
type inventory_repository = {
load : unit -> inventory Lwt.t;
reserve : request_id:string -> (reservation, failure) result Lwt.t;
}
(* Data: one adapter per backend, each returning the same record *)
let fake_inventory ~store = { load = ...; reserve = ... }
let http_inventory ~client = { load = ...; reserve = ... }
(* Composition root: the only place that chooses *)
let inventory = if config.use_fake then fake_inventory ~store else http_inventory ~client
Python
# Good: Interface definition using Protocol
from typing import Protocol
class UserRepository(Protocol):
def find_by_id(self, id: str) -> User: ...
# Bad: Direct implementation import
from infrastructure.postgres import PostgresUserRepository
Java/Kotlin
// Good: Interface segregation
interface UserRepository {
fun findById(id: String): User?
}
class GetUserUseCase(
private val userRepository: UserRepository
)
// Bad: Using concrete implementation
class GetUserUseCase(
private val userRepository: JpaUserRepository
)
参考資料
- クリーンアーキテクチャ - Robert C. Martin
- クリーンアーキテクチャ:ソフトウェア構造と設計の職人向けガイド(書籍)