この記事は全てAIで書かれています。
とあるCLIツールの紹介記事です。CleanArchitectureに興味がある人、興味はあるが守るのが大変に感じる人は見ていただけると幸いです。
TL;DR
生成AIに「クリーンアーキテクチャで書いて」とお願いしても、依存関係の違反は普通に起きる。usecase から infrastructure を直接 import したり、domain 層に sqlx が紛れ込んだり。自然言語での「お願い」には限界がある。
そこで、レイヤードアーキテクチャの依存ルールを TOML 1つで宣言的に定義し、CI で自動検証する CLIツール mille を作った。Rust 製、10言語対応。
生成AI時代のアーキテクチャ課題
ChatGPT や Claude にコードを書いてもらう機会が増えた。「クリーンアーキテクチャで」「レイヤー分離して」とプロンプトに書けば、それらしい構造のコードが生成される。
しかし、実際に生成されたコードをよく見ると、こんなことが起きている。
usecase に infrastructure が漏れる
// src/usecase/create_user.rs
use crate::domain::user::User;
use crate::infrastructure::postgres_repo::PostgresUserRepository; // NG!
pub fn create_user(name: &str) -> User {
let repo = PostgresUserRepository::new(); // 具体実装に直接依存
repo.save(User::new(name))
}
usecase 層は domain 層のインターフェースだけに依存すべきなのに、PostgresUserRepository という infrastructure の具体実装を直接参照してしまっている。
生成AIはコンテキストウィンドウの中で「動くコード」を最短距離で生成しようとする。依存関係逆転の原則(DIP)のような暗黙の制約は、明示的に指示しない限り無視されやすい。
外部ライブラリが想定外のレイヤーに侵入する
レイヤー間の依存だけでなく、外部ライブラリの利用範囲も問題になる。
# domain/user.py — domain 層なのに DB ライブラリを直接 import
from sqlalchemy import Column, Integer, String # NG!
class User:
id: int
name: str
domain 層は純粋なビジネスロジックだけを持つべきで、sqlalchemy のような永続化ライブラリに依存すべきではない。しかし、生成AIに「User エンティティを作って」と頼むと、ORM のアノテーションがついた状態で生成されることがよくある。
さらに細かいレベルでは、こんなケースもある。
// src/main.rs — DI の組み立てだけをすべきエントリポイント
let repo = UserRepositoryImpl::new(); // OK: インスタンス生成
repo.find_user(1); // NG: ビジネスロジックの直接呼び出し
main 層では infrastructure のファクトリメソッド(new, build など)だけを呼んで DI コンテナを組み立てるべきで、find_user のようなビジネスロジックを直接呼ぶのは設計違反だ。しかし、「呼んでいいメソッド」と「呼んではいけないメソッド」の区別は自然言語では伝えにくい。
ハーネスエンジニアリングという発想
これらの問題に対して、プロンプトを工夫して対処しようとするアプローチがある。しかし、いくらプロンプトを磨いても、生成AIは確率的に動作する以上、違反はゼロにはならない。
発想を変える必要がある。
自然言語での「お願い」ではなく、機械的に検証可能なルールで縛る。 これを巷では「ハーネスエンジニアリング」と呼んでいる。テストハーネスが実装の正しさを保証するように、アーキテクチャのルールもハーネスとして機械化する。
- 生成AIが書いたコードも、人間が書いたコードも、同じ基準でチェック
- CI に組み込めば、違反は merge 前に自動検出
- ルールは TOML ファイルに宣言的に記述 — 曖昧さゼロ
mille — TOML 1つでレイヤー依存を宣言的に定義
mille は、レイヤードアーキテクチャの依存ルールを静的解析する CLI ツール。
レイヤー間依存の制御
[[layers]]
name = "domain"
paths = ["src/domain/**"]
dependency_mode = "opt-in"
allow = [] # 何にも依存しない
[[layers]]
name = "usecase"
paths = ["src/usecase/**"]
dependency_mode = "opt-in"
allow = ["domain"] # domain のみ参照可
dependency_mode = "opt-in" はホワイトリスト方式。allow に書いたレイヤーだけ参照できる。逆に opt-out はブラックリスト方式で、deny に書いたレイヤーだけ禁止する。レイヤーの性質に合わせて使い分けられる。
外部ライブラリの制御
[[layers]]
name = "domain"
external_mode = "opt-in"
external_allow = [] # 外部ライブラリは一切使わない
[[layers]]
name = "usecase"
external_mode = "opt-in"
external_allow = ["serde", "uuid", "chrono"] # この 3 つだけ許可
[[layers]]
name = "infrastructure"
external_mode = "opt-out"
external_deny = [] # 何でも使ってOK
domain 層で sqlalchemy や sqlx を import したら即座にエラー。usecase 層では serde、uuid、chrono だけ使える。infrastructure 層は制限なし。
レイヤーごとに「何を使ってよいか」を厳密に定義できるのが mille の特徴。
メソッド呼び出しの制限
[[layers]]
name = "main"
paths = ["src/main.rs"]
dependency_mode = "opt-in"
allow = ["infrastructure", "usecase", "presentation"]
[[layers.allow_call_patterns]]
callee_layer = "infrastructure"
allow_methods = ["new", "build", "create", "init", "setup"]
main 層から infrastructure のメソッドを呼ぶとき、new、build などのファクトリメソッドだけを許可する。find_user や save のようなビジネスロジックを直接呼んだらエラーになる。
命名規則の制御
[[layers]]
name = "usecase"
name_deny = ["gcp", "aws", "azure", "mysql", "postgres"]
usecase 層のファイル名・関数名・変数名に aws や postgres のようなインフラ固有のキーワードが含まれていたらエラー。大文字小文字を無視した部分一致で検出する。
10言語対応
Rust, Go, TypeScript, JavaScript, Python, Java, Kotlin, PHP, C, YAML に対応。tree-sitter で AST ベースの解析を行うため、正規表現ベースのツールより正確。
多言語プロジェクトも 1つの mille.toml で統一管理できる。
mille があるとどう嬉しいか
1. AIが書いたコードを CI で自動検証
# .github/workflows/ci.yml
- name: Architecture check
run: mille check
たった 1 行で、生成AIが書いたコードのアーキテクチャ違反を merge 前に自動検出できる。レビュアーが依存関係を目視確認する必要がなくなる。
2. アーキテクチャの知識が属人化しない
「この層からこの層は参照禁止」「domain では ORM を使わない」といったルールが mille.toml に明文化される。チームメンバーの入れ替わりがあっても、ルールはコードと一緒にリポジトリに残る。
3. 段階的に導入できる
[severity]
dependency_violation = "warning" # まずは warning で始める
external_violation = "warning"
最初は severity を warning にして現状を可視化し、チームの合意が取れたら error に切り替える。既存プロジェクトへの導入もスムーズ。
4. 可視化から始められる
mille analyze --format svg > graph.svg # 依存グラフを SVG で出力
mille report external # 外部ライブラリの利用状況を一覧
ルール適用の前に、まず現状の依存関係を可視化できる。「実は domain が infrastructure に依存していた」という発見から始めるワークフローをサポートしている。
まとめ
生成AI時代において、アーキテクチャの品質を保つには「お願い」ではなく「仕組み」が必要。
mille は、レイヤー間依存・外部ライブラリ依存・メソッド呼び出し・命名規則を TOML 1つで宣言的に定義し、CI で機械的に検証するツール。
ルールは人間が定義し、検証は機械に任せる。それが、生成AIと共存しながらアーキテクチャを守るための現実的なアプローチだと考えている。
リンク
- GitHub: https://github.com/makinzm/mille
- ドキュメント: https://makinzm.github.io/mille
- インストール:
cargo install mille/npm install -g @makinzm/mille/pip install mille