# Architecture Design

> Use when 设计或重构项目架构、定分层与依赖方向时。从变化频率推导层，而非套模板。

- Skill: `profyangshengxu/architecture-design` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add profyangshengxu/architecture-design`
- Raw SKILL.md: https://api.skillmd.com/api/skills/profyangshengxu/architecture-design/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: ProfYangShengXu (https://skillmd.com/u/profyangshengxu)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/profyangshengxu/architecture-design

---


# 架构设计：从「什么最容易变」推导分层

**不要套模板。** 四层（api/service/domain/db）只是这套原则在 Web 后端的一个投影。
换场景层名和数量都变，**不变的只有依赖方向**。

## 何时用

- 用户要给新项目定架构（"这个怎么分层"）
- **丢进来一坨能跑但难维护的代码，要重构**（最高频）
- 评审现有项目：依赖方向错在哪
- 用户问"Agent 系统 / 数据管线 / CLI 该怎么分层"

---

## 一、三条铁律（不可商量）

全部来自 Robert C. Martin 的组件耦合原则，与语言、框架无关。

### ① SDP — 依赖指向稳定

**易变的必须依赖不易变的，不能反过来。**

### ② SAP — 稳定者抽象

**被很多人依赖的组件，必须是抽象的**（接口/协议），否则它一改就全崩 —— 这叫「**痛苦区**」。
反过来，不稳定的组件应该是具体实现。

**SDP + SAP 合起来 = 依赖倒置架构。**

### ③ ADP — 依赖图无环

出现环就必须打破。**打破环的标准手法是引入抽象**（把环里某个依赖反转掉），
不是"加个中间层"也不是"把公共部分抽出来"。

---

## 二、核心操作只有两步

（此二步出自 Oliver Zihler，Alistair Cockburn 本人引用为标准路径）

### Step 1｜反转依赖

把「高层调用低层」改成「低层实现高层定义的接口」。

### Step 2｜确保端口定义在内部

**接口的定义必须住在被依赖的稳定侧，不是实现侧。**

### ⭐ 判据：什么叫「做对了」

Cockburn 给了强弱两档，**要往强里做**：

| 档位 | 表现 |
|---|---|
| **弱** | 知道要用 SQL，虽然不绑具体库，但接口仍用 SQL 词汇表达 |
| **强** ⭐ | **内部完全不知道外部技术存在** —— 连「外面有个数据库」都不知道 |

**自检问句**：把接口拿给一个不懂这个技术栈的人看，他能看懂吗？
看不懂 = 技术词汇漏进去了 = 弱实现。

---

## 三、怎么推导分层（不套模板的做法）

### 第 1 步：列「变化频率」

拿一张纸，把项目里所有东西按**多久变一次**排序：

```
变最快 ────────────────────────────► 最稳定
UI框架  外部API  数据库  ←→  业务规则  领域概念
```

**判断依据**：
- **业务 vs 技术**：业务规则（"名额满了不能报名"）十年不变；技术选型（FastAPI→Flask）常变
- **这条规则是公司特有还是行业通用？** 公司特有 → 易变；行业通用 → 稳定

### 第 2 步：按「变化频率」切层

**同一变化频率的东西放一层。** 这就是 CCP（共同闭包）—— 一起变的放一起。

**验证方法**：如果改一个需求，你发现要同时改 A 和 B 两个文件，那 A 和 B 可能该在同一层。
如果改 A 从不需要改 B，那它们该在不同层。

### 第 3 步：画依赖箭头，全部指向稳定侧

**所有箭头必须是单向的、指向更稳定的一侧。**

### 第 4 步：找出唯一装配点

Cockburn 叫它 **Configurator**，也有地方叫 **Composition Root**。

> **全项目只有这一处 new 对象。** 它知道所有实现，把实现注入给抽象。

测试时换掉它 = 换掉整个运行环境。

---

## 四、量化体检（对现有项目用）

三个指标，能手算也能脚本化：

```
I（不稳定度）= 出度 / (入度 + 出度)
   入度 = 多少人依赖我；出度 = 我依赖多少人
   I=0 最稳定（众人依赖我，我不依赖人）；I=1 最不稳定

A（抽象度）= 抽象类/接口数 / 总类数

D（距主序列距离）= |A + I - 1|
```

| D 值 | 含义 | 处方 |
|---|---|---|
| **≈ 0** | 健康 | 保持 |
| **> 0.3** | **无用处区** —— 太抽象，没人用 | 删掉或合并抽象 |
| **> 0.5** | **痛苦区** —— 太具体，却被大量依赖 | **立刻抽接口** |

**用法**：用户说"我这架构有问题但说不清"，算一遍 D，**痛苦区的那个组件就是病根**。

---

## 五、跨场景映射（层名自己推，别抄）

以下是**同一套原则在不同场景的投影**，仅供对照，不要直接套：

| 场景 | 稳定核心 | 易变外壳 | 端口（稳定侧定义的接口）|
|---|---|---|---|
| **Web 后端** | domain 业务规则 | api / db | Repository 接口、UseCase 接口 |
| **CLI 工具** | core 核心逻辑 | cli 参数解析 / io | 输入输出抽象 |
| **Agent 系统** | **agent-loop 决策逻辑** | harness / tools / model client | Tool 接口、Model 接口 |
| **数据管线** | transform 变换规则 | source / sink | Reader / Writer 接口 |
| **前端** | state 状态机 | view / api-client | API 客户端接口 |
| **ML 训练** | model 定义 / 损失 | data loader / trainer / 日志 | Dataset 接口、Logger 接口 |
| **游戏** | game-logic 规则 | render / input / assets | InputProvider 接口 |
| **嵌入式** | app-logic | drivers / hal | HAL 接口（James Grenning 叫它 Service Abstraction Layer）|

**⭐ Agent 场景特别注意**（用户近期主攻方向）：
判据 === 「谁是决策逻辑，谁是对接环境」。**agent-loop 是稳定的**（怎么想），
**harness/tools 是易变的**（对接什么环境）。所以应当是 **harness 依赖 agent-loop 定义的接口**，
不是 agent-loop import 具体工具。

---

## 六、输出物（交付什么）

重构/设计任务收尾，给这四样：

1. **职责表**：每层干什么、什么情况归它、边界在哪
2. **依赖图**：箭头方向（文字版 mermaid 即可）
3. **接口归属清单**：每个抽象接口归哪层、由谁实现
4. **唯一装配点**：哪条命令/哪个 main 里 new 对象

代码骨架按用户的技术栈给（Python/FastAPI 是高频栈）。

---

## 七、反模式（遇到就报）

| 症状 | 诊断 | 处方 |
|---|---|---|
| 一个函数里既有 SQL 又有 if 业务判断又有 HTTP 异常 | 三层糊在一起 | 拆 domain / service / db / api |
| domain 里 `import` 了 ORM 或 web 框架 | **依赖方向反了** | 反转：domain 定义接口，外部实现 |
| 接口定义在实现侧（db 定义 IRepository）| **端口位置错了** | 把接口搬到 service/domain |
| 依赖图有环 | 违反 ADP | 用抽象打断环 |
| 组件被 20 处依赖但全是具体类 | **在痛苦区** | 抽接口 |
| 到处 `new Database()` | 缺装配点 | 收敛到 Composition Root |
| 为了"解耦"抽了 20 个没用过的接口 | **在无用处区** | 删 |

---

## 八、⚠️ 别过度设计

**原教程自己承认**：

> 用例本身很小，重构后代码更多、文件更多，看起来反而更复杂；**在真实项目中值得做**。

**判据**：
- 项目活不过三个月 / 只有你一个人用 → **别拆**
- 要被面试官看 / 要被别人接手 / 技术栈会换 → **拆**

拆到「**为了解耦而解耦**」就是纯亏损。宁可写具体的重复代码，也别抽用不上的抽象。

---

## 参考

- `references/theory-sources.md` — 六条组件原则原文要点、I/A/D 公式出处、
  Hexagonal 与 Clean/Onion 的关系、从分层到端口的官方两步法

