7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SOLIDやLayered Architectureは何を守っているのか?――アーキテクチャ零曲率定理から見る設計原則と不変量

7
Posted at

AI にコードを書いてもらうと、動くものはかなり速く出てきます。

でも、あとから差分を見て「あ、これ依存の向きがまずい」「境界を越えて直接呼んでいる」「この変更、障害が横に広がりそう」と気づくことがあります。

そのとき本当に知りたいのは、コードが動くかだけではありません。その変更で何のリスクが増減したのかです。循環依存は消えたのか。境界違反は減ったのか。障害は広がりにくくなったのか。

だから、AI が出した差分を「設計として大丈夫か」と診断するための物差しが必要になります。

この研究で中心に置く定理の名前は、アーキテクチャ零曲率定理です。

少し正確に書くと、次の形を目指しています。

Lawful(A, L)
  <-> NoRequiredObstruction(A, L)
  <-> RequiredAxesAvailableAndZero(Signature(A), L)

ここで A は対象のアーキテクチャ、L は採用した設計ルールのまとまりです。

  • Lawful(A, L) は、AL に対して健全であること
  • 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 を「OrderControllerOrderService に依存している」という意味で使います。

よくある 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 には注意が必要です。循環があるグラフで同じ辺を何度も通れる経路を許すと、同じ循環を何度でも回れてしまうため、深さは有限の値として扱えません。

そのため初期版では、有限な測定対象上で探索の上限を決めて測ります。循環そのものの有無や大きさは hasCyclesccMaxSize で見て、依存をどの程度深くたどるかは 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 の現在の狙いです。

関連リンク

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?