AI にコードを書いてもらうと、動くものはかなり速く出てきます。
でも、あとから差分を見て「あ、これ依存の向きがまずい」「境界を越えて直接呼んでいる」「この変更、障害が横に広がりそう」と気づくことがあります。
そのとき本当に知りたいのは、コードが動くかだけではありません。その変更で何のリスクが増減したのかです。循環依存は消えたのか。境界違反は減ったのか。障害は広がりにくくなったのか。
だから、AI が出した差分を「設計として大丈夫か」と診断するための物差しが必要になります。
この研究で中心に置く定理の名前は、アーキテクチャ零曲率定理です。
少し正確に書くと、次の形を目指しています。
Lawful(A, L)
<-> NoRequiredObstruction(A, L)
<-> RequiredAxesAvailableAndZero(Signature(A), L)
ここで A は対象のアーキテクチャ、L は採用した設計ルールのまとまりです。
-
Lawful(A, L)は、AがLに対して健全であること -
NoRequiredObstruction(A, L)は、要求されたルールを破る具体例がないこと -
RequiredAxesAvailableAndZero(Signature(A), L)は、必要な診断項目が測定済みで 0 であること
もちろん、これは「必要な違反候補を測れている」「診断項目が対応する違反を正しく表している」といった前提のもとでの主張です。
砕いて言うと、次の見方です。
「良い設計」を設計原則の名前だけで判定しない。
採用したルールごとに、循環依存、境界越え、抽象化漏れのような破れがどこに残っているかを見る。
ArchitectureSignature は、その破れを項目ごとに分けて出す診断表である。
ここでの「零曲率」は、単一スコアの比喩ではありません。
循環依存、抽象化違反、LSP 違反、補償失敗、履歴再生の不整合、境界違反などを、
それぞれ「どのルールを破る具体例か」として提示できる状態を目指します。
はじめに
ソフトウェア設計では、SOLID、Layered Architecture、Clean Architecture、Event Sourcing、Saga、Circuit Breaker など、名前のある設計原則やパターンをよく使います。
どれも大事な言葉です。ただ、現場では「それによって何が良くなるのか」が曖昧になることがあります。設計原則の名前は便利ですが、ときどき免罪符にもなります。
名前のある設計原則を使っていることと、アーキテクチャ上のリスクが下がっていることは同じではありません。
たとえば、循環依存を減らしたいのか、変更の波及を抑えたいのか、実装を差し替えやすくしたいのか、障害を広げにくくしたいのか。リスクの種類を分けて見る必要があります。
この研究では、設計原則を次のように見ています。
設計原則は、守りたいアーキテクチャ上の性質を保ったり、改善したりするための変更の方向を示すものです。
つまり、「SOLID は良い」「Layered Architecture は良い」とまとめて言うのではなく、「それぞれ何を守るための考え方なのか」を分けて見ます。
アーキテクチャで守りたい性質
ここでは、設計変更の前後で守りたい性質を「不変量」と呼びます。少し硬い言葉ですが、意味はシンプルです。
たとえば Layered Architecture や Clean Architecture では、依存の向きを制御することが重要になります。
ただし、この二つは同じ依存方向を意味するわけではありません。
依存関係をグラフとして見ると、コンポーネントを点、依存関係を矢印で表せます。この記事では OrderController -> OrderService を「OrderController が OrderService に依存している」という意味で使います。
よくある Layered Architecture では、あらかじめ決めた層の順序に沿って、上位層から下位層へ依存する形を取ることがあります。
Presentation -> Application -> Domain -> DataAccess
一方で、Clean Architecture や Ports and Adapters では、Domain や Application が Infrastructure の具体実装に依存しないようにします。
UI -> Application -> Domain
ApplicationService -> PaymentPort
SqlPaymentRepository -> PaymentPort
この場合、PaymentPort は Application 側が定義する抽象であり、Infrastructure 側の SqlPaymentRepository がそれを実装します。
重要なのは、どちらの形が常に正しいかではありません。採用したアーキテクチャの依存ポリシーに対して、実際の依存グラフが違反していないかです。
Lean 側では、まず「依存するたびに層が下がる」という強いモデルを使います。これは実務上の Layered Architecture をすべて表す定義ではなく、循環依存が起きない構造をはっきり扱うためのモデルです。
実際の設計では、同じ層の中の依存を許す場合もあります。その場合は、同一層内の依存ルールや循環の扱いを別に決める必要があります。
SOLID を守っていても、レイヤー違反は起きる
SOLID は、責務分離、抽象への依存、置換可能性などを考える上で役に立ちます。一方で、SOLID だけでシステム全体のレイヤー構造まで保証できるわけではありません。
上の Clean Architecture / Ports and Adapters 風の例で、問題になるのは次のような依存です。
ApplicationService -> SqlPaymentRepository
ApplicationService が具体的な SQL 実装を直接呼び始めています。ApplicationService 自体の責務がある程度整理されていて、インターフェースも使っているかもしれません。それでも、大域的に見ると「Application 層が Infrastructure の詳細に依存する」というレイヤー違反が起きています。
この 1 本の依存は、複数の指標に同時に影響します。
- Application 層から Infrastructure の詳細への依存なので、
boundaryViolationCountが増えます。 -
PaymentPortを通すべき場所で具体実装に依存しているなら、abstractionViolationCountが増えます。 -
ApplicationServiceの依存先が増えるので、fanoutRiskにも影響します。
つまり、1 つの設計違反をただ「悪い」とまとめるのではなく、どの性質に対する違反なのかを分解して読めます。
このように、SOLID が見ているものと Layered Architecture が見ているものは違います。
| 設計原則 | 主に見ていること |
|---|---|
| SOLID / DIP / LSP | 責務が分かれているか、抽象に依存しているか、差し替えても振る舞いが壊れないか |
| Layered / Clean Architecture | 依存の向きが守られているか、循環がないか、境界をまたぐ依存が増えていないか |
| Event Sourcing / Saga / CRUD 型の状態管理 | 状態遷移や補償処理をどう表すか、失敗時の流れをどう扱うか |
| Circuit Breaker / Replicated Log | 実行時の障害伝播をどう止めるか、分散状態をどうそろえるか |
設計原則を比べるときは、「どちらが偉いか」ではなく、「どのリスクを下げるためのものか」を見る方が実用的です。
零曲率定理で何が変わるか
アーキテクチャ零曲率定理が目指しているのは、設計原則を経験則の名前としてではなく、違反の具体例を消すための操作として読むことです。
考え方は単純です。
- 採用した設計ルールを決める
- そのルールを破っている具体例を探す
- 見つかった破れを診断項目として記録する
- 測っていない項目は「問題なし」と読まない
たとえば、循環依存なら「閉じた依存経路」、抽象化違反なら「抽象を通さない具体実装への依存」、LSP 違反なら「同じ抽象として扱いたい実装どうしの観測不一致」が、破れの具体例になります。
この見方にすると、設計原則やパターンは次のように読み替えられます。
| 設計の語彙 | 零曲率定理での読み方 |
|---|---|
| SOLID / DIP / LSP | 局所的な契約を見るルールです。責務、抽象への依存、置換可能性に関する違反の具体例を減らします。SOLID は万能原理ではなく、大域的な依存構造や分散実行の健全性を単独では保証しません。 |
| Layered / Clean Architecture | システム全体の構造を見るルールです。依存方向、境界違反、循環依存、抽象化違反に関する違反の具体例を減らします。 |
| Event Sourcing / Saga / CRUD | 状態の変化をどう扱うかに関する設計操作です。履歴再生、往復変換、補償、更新経路の整合性に関する違反の具体例を減らすために使います。 |
| Circuit Breaker / Replicated Log | 実行時の依存や分散状態を見る設計操作です。障害伝播、実行時 exposure、合意や収束の破れに関する違反の具体例を減らします。 |
つまり「Clean Architecture だから良い」「Event Sourcing だから良い」とは言いません。どの設計ルールを要求していて、どの破れが残っていて、どの診断項目が測定済みなのかを確認します。
Lean では、この対応のうち、定義がはっきりしていて数学的に扱える部分を証明します。一方で、違反数がレビューコストや障害率と関係するかは Lean の定理ではなく、実コードベースと運用データで検証する仮説です。
ArchitectureSignature: 複数の診断項目で見る
この研究では、アーキテクチャの状態を ArchitectureSignature として記録します。
これは、アーキテクチャ品質を 80 点、60 点のような単一スコアにするためのものではありません。複数の診断項目で、アーキテクチャの状態を読むためのものです。
初期版では、たとえば次のような項目を見ます。
Sig0(A) =
< hasCycle,
sccMaxSize,
maxDepth,
fanoutRisk,
boundaryViolationCount,
abstractionViolationCount >
それぞれの意味は次の通りです。
| 項目 | 見ていること |
|---|---|
hasCycle |
循環依存があるかどうかです。循環があると、変更の影響が回り込みやすくなります。 |
sccMaxSize |
強連結成分の最大サイズです。強連結成分とは、互いに到達できるコンポーネントの塊です。この塊が大きいほど、分離しにくい構造です。 |
maxDepth |
依存をたどったときの深さです。ただし循環を何度でも回れる経路の長さではなく、初期版では有限な測定対象上で上限を決めた深さとして扱います。 |
fanoutRisk |
依存先の多さです。初期版では測定対象内の依存辺の合計、つまり totalFanout として扱います。 |
boundaryViolationCount |
レイヤーやドメイン境界を越えた依存の数です。境界ルールがある場合に測ります。 |
abstractionViolationCount |
抽象を通すべき場所で、具体実装に直接依存している数です。DIP 的な違反を見ます。 |
maxDepth には注意が必要です。循環があるグラフで同じ辺を何度も通れる経路を許すと、同じ循環を何度でも回れてしまうため、深さは有限の値として扱えません。
そのため初期版では、有限な測定対象上で探索の上限を決めて測ります。循環そのものの有無や大きさは hasCycle や sccMaxSize で見て、依存をどの程度深くたどるかは maxDepth で見る、という分担にします。
fanoutRisk も、初期版では凝った重み付けをしません。測定対象内にある依存辺の合計を数えます。局所的に依存が集中しているかは、将来的には maxFanout のような別軸で見る方が自然です。
この 6 項目は完成形ではなく、最初に安定して測るための土台です。今後の ArchitectureSignature では、たとえば次のような観点を追加していきます。
| 追加したい観点 | 代表的な項目 |
|---|---|
| 循環や変更波及を細かく見る |
sccExcessSize, maxFanout, reachableConeSize
|
| 抽象化や置換可能性を見る |
projectionSoundnessViolation, lspViolationCount
|
| 状態遷移や実行時の障害伝播を見る |
relationComplexity, runtimePropagation
|
| 実データ上のコストとの関係を見る |
reviewCost, incidentRepairCost
|
ただし、これらを全部まとめて 1 つの点数にするわけではありません。測れる項目から追加し、まだ測れていない項目は未測定として扱います。
たとえば、あるリファクタリングの前後で次のように変わったとします。
before:
hasCycle = 1
sccMaxSize = 8
maxDepth = 9
fanoutRisk = 37
boundaryViolationCount = 12
after:
hasCycle = 0
sccMaxSize = 1
maxDepth = 6
fanoutRisk = 21
boundaryViolationCount = 5
このとき、「全体として良くなった」とだけ言うのではなく、次のように分けて見ます。
- 循環依存が消えました
- 大きな強連結成分が分解されました
- 依存の深さが下がりました
- 依存先の多さによるリスクが下がりました
- 境界違反が減りました
逆に、状態遷移の複雑さや実行時の障害伝播が悪化しているなら、それは別の軸として残します。単一スコアにまとめすぎると、どの問題が残っているのかが見えにくくなります。
未測定をリスク 0 と読まない
実コードから指標を取るとき、すべての項目を常に測れるわけではありません。
たとえば、境界ルールがまだ定義されていなければ、boundaryViolationCount は正しく測れません。このとき仮の値として 0 が入っていても、それは「境界違反がない」という意味ではありません。
そこで、値とは別に metricStatus を持ちます。
{
"signature": {
"boundaryViolationCount": 0
},
"metricStatus": {
"boundaryViolationCount": {
"measured": false,
"reason": "policy file not provided"
}
}
}
measured: false は「未測定」という意味です。リスク 0 ではありません。ここを分けないと、「測っていないだけ」のものを「問題なし」と誤読してしまいます。
Lean で証明すること、実データで確かめること
この研究では、Lean で証明する話と、実コードベースで確かめる話を分けます。
Lean で扱うのは、定義がはっきりしていて、数学的に証明できる構造です。たとえば、現時点では次のような事実を証明しています。
- レイヤー構造があれば循環依存は起きない
- レイヤー構造があれば、依存の伝播は有限段で止まる
- 循環がないことと、閉じた経路がないことは対応する
- 抽象への写し方が正しければ、測定対象内の抽象化違反の数は 0 になる
- 観測される振る舞いが抽象を通して保たれれば、測定対象内の LSP 違反の数は 0 になる
ただし、LSP 的な違反は依存グラフだけでは測れません。何を「観測される振る舞い」とみなすかを別に定義する必要があります。
一方で、次のような主張は Lean の定理ではありません。
- fanout リスクが高い変更はレビューコストが増える
- 強連結成分が大きい領域では障害修正コストが増える
- 境界違反が将来の変更波及と関係する
- 実行時の依存の広がりが障害影響範囲と関係する
これらは、実コードや運用データで確かめる仮説です。まずは抽出ルールと分析手順を固定し、Signature の各成分が変更範囲、レビューコメント数、レビュー往復回数、障害修正時間などと関係するかを小さく検証します。
Lean の証明と実データによる検証は役割が違います。Lean は「測っているものが、定義した性質に対応している」ことを支えます。現実の変更コストや障害コストとの関係は、別途データで確かめます。
何を目指しているか
ここまでの話をまとめると、設計原則は絶対的な善ではありません。特定の性質を守るための設計上の制約や、変更の方向として読めます。
最終的には、アーキテクチャレビューを「感想」から「診断」に近づけたいです。
たとえば、レビューで次のように言える状態を目指しています。
- この変更で循環依存は消えました
- ただし fanout リスクはまだ高いです
- 抽象化違反は減りましたが、実行時の伝播は未測定です
- Event Sourcing で履歴を再構成しやすくなりましたが、状態遷移の複雑さは増えました
- Circuit Breaker は実行時の障害伝播を抑えますが、静的な依存グラフのレイヤー構造を保証するものではありません
設計原則を「なんとなく良いもの」としてではなく、「どの性質を守るためのものか」として読みます。アーキテクチャ品質を単一スコアではなく、複数の診断項目として読みます。
この見方は、AI 駆動開発でも役に立つはずです。AI がコードを書く速度を上げるほど、人間側には「その変更はアーキテクチャとして大丈夫か」を確かめる仕組みが必要になります。
設計原則、形式的な不変量、実コード上の測定をつなげることが、代数的アーキテクチャ論 V2 の現在の狙いです。