大多数软件的失败,并不是因为它无法被构建出来,而是因为一两年后,每次修改都比上一次更费时,小小的修复会弄坏不相关的功能,没人敢再碰核心代码。整洁架构和 SOLID 原则正是为防止这种情况而生的一组理念:把系统中因不同原因而变化的部分彼此隔离开。
代码库为何会腐化
腐化几乎总是源于耦合:业务规则知道 HTTP 请求、数据库行和 UI 组件的存在。当定价逻辑写在一个既解析 JSON 又拼写 SQL 的控制器里时,不启动 Web 服务器和数据库就无法测试定价;更换数据库就必须改动定价逻辑;而想在后台任务中复用定价逻辑更是无从谈起。
好的架构就是划清边界,让最有价值的代码,也就是业务规则,依赖尽可能少的东西。
依赖规则
按照 Robert C. Martin 的描述,整洁架构把代码组织为若干同心圆层:
- 实体(Entities)位于中心:核心业务对象和规则,例如订单及其总价的计算方式。
- 用例(Use cases)环绕其外:应用特有的业务流程,例如下单或退款。
- 接口适配器:控制器、展示器和仓储,负责在用例与外部世界之间转换数据。
- 框架与驱动位于最外层:Web 框架、数据库、UI、第三方服务。
让这一切成立的只有一条规则:源代码的依赖只能指向内层。内层对外层一无所知。用例从不导入数据库客户端,而是声明一个描述自身需求的接口,即端口(port),由外层提供具体实现,即适配器(adapter)。这也是这种风格又被称为端口与适配器或六边形架构的原因。
一个能清楚体现分层的目录结构可能如下所示:
src/
domain/ entities and value objects; no framework imports
order.ts
application/ use cases and the ports they need
ports.ts
place-order.ts
infrastructure/ adapters: database, email, payment provider
postgres-order-repository.ts
email-notifier.ts
interface/ HTTP handlers, CLI commands, UI
http/orders-controller.ts
main.ts composition root: wires adapters into use cases一个完整的 TypeScript 示例
从实体开始。它只包含纯粹的业务规则,不导入任何框架代码:
// domain/order.ts
export type OrderLine = { sku: string; quantity: number; unitPriceCents: number }
export class Order {
private constructor(
readonly id: string,
readonly customerId: string,
readonly lines: readonly OrderLine[]
) {}
static create(id: string, customerId: string, lines: OrderLine[]): Order {
if (lines.length === 0) throw new Error('An order needs at least one line')
if (lines.some((line) => line.quantity <= 0)) throw new Error('Quantities must be positive')
return new Order(id, customerId, lines)
}
get totalCents(): number {
return this.lines.reduce((sum, line) => sum + line.quantity * line.unitPriceCents, 0)
}
}用例描述业务流程,并声明它所依赖的端口。注意,这些接口归应用层所有,而不是归数据库代码所有:
// application/ports.ts
import type { Order } from '../domain/order'
export interface OrderRepository {
save(order: Order): Promise<void>
}
export interface Notifier {
orderPlaced(order: Order): Promise<void>
}
// application/place-order.ts
import { Order, type OrderLine } from '../domain/order'
import type { Notifier, OrderRepository } from './ports'
export class PlaceOrder {
constructor(
private readonly orders: OrderRepository,
private readonly notifier: Notifier
) {}
async execute(input: { customerId: string; lines: OrderLine[] }) {
const order = Order.create(crypto.randomUUID(), input.customerId, input.lines)
await this.orders.save(order)
await this.notifier.orderPlaced(order)
return { orderId: order.id, totalCents: order.totalCents }
}
}外层的适配器用真实技术实现端口,组合根(composition root)在启动时把一切连接起来:
// infrastructure/postgres-order-repository.ts
import type { Pool } from 'pg'
import type { OrderRepository } from '../application/ports'
import type { Order } from '../domain/order'
export class PostgresOrderRepository implements OrderRepository {
constructor(private readonly db: Pool) {}
async save(order: Order) {
await this.db.query('INSERT INTO orders (id, customer_id, total_cents) VALUES ($1, $2, $3)', [order.id, order.customerId, order.totalCents])
}
}
// main.ts
const placeOrder = new PlaceOrder(new PostgresOrderRepository(pool), new EmailNotifier(mailer))回报体现在测试上。用例只需简单的内存假对象就能在几毫秒内完成测试,不需要数据库、不需要邮件服务器,也不需要 mock 库:
import { expect, test } from 'vitest'
import { PlaceOrder } from './place-order'
import type { Order } from '../domain/order'
test('places an order and notifies the customer', async () => {
const saved: Order[] = []
const notified: Order[] = []
const placeOrder = new PlaceOrder({ save: async (order) => void saved.push(order) }, { orderPlaced: async (order) => void notified.push(order) })
const result = await placeOrder.execute({
customerId: 'customer-1',
lines: [{ sku: 'TS-1042', quantity: 2, unitPriceCents: 1500 }],
})
expect(result.totalCents).toBe(3000)
expect(saved).toHaveLength(1)
expect(notified).toHaveLength(1)
})SOLID 原则逐条解读
SOLID 是五条面向对象设计原则,它们在单个类和模块的层面上服务于同一个目标。
S:单一职责原则
一个模块应该只有一个变化的理由,也就是说它只对一类人或一个关注点负责。一个既计算税费、又生成 PDF、还发送邮件的发票类,每当财务、设计或 IT 部门改变想法时都要修改。应把它拆分为计算器、渲染器和发送器。
O:开闭原则
代码应当对扩展开放,对修改关闭:通过新增代码来增加行为,而不是修改已经测试过的代码。常用的技巧是策略模式:
interface DiscountRule {
appliesTo(order: Order): boolean
amountCents(order: Order): number
}
const rules: DiscountRule[] = [new LoyaltyDiscount(), new BulkOrderDiscount()]
export const discountCents = (order: Order) => rules.filter((rule) => rule.appliesTo(order)).reduce((sum, rule) => sum + rule.amountCents(order), 0)
// A new promotion is a new class in the list, not another branch in a growing if/elseL:里氏替换原则
接口的任何实现都必须能在任何需要该接口的地方使用,而不会让调用方感到意外。一个实现了 OrderRepository 却在调用 save() 时抛出异常的 ReadOnlyOrderRepository 就违反了这一原则,依赖该契约的调用方会在运行时出错。如果某个实现无法履行完整的契约,它就需要一个不同的、更小的接口。
I:接口隔离原则
客户端不应依赖它用不到的方法。与其使用一个包含二十个方法的大接口,不如使用多个小而专注的接口,例如 OrderReader 和 OrderWriter。小接口更容易实现、更容易在测试中伪造,也更容易修改。
D:依赖倒置原则
高层策略应当依赖抽象,而不是依赖具体细节。上面的 PlaceOrder 用例依赖的是 OrderRepository 接口,而不是 PostgreSQL,因此更换数据库时业务逻辑完全不受影响。这条原则正是依赖规则的驱动力。
值得了解的设计模式
- 仓储(Repository):把实体的存储方式隐藏在一个类似集合的接口后面。
- 策略(Strategy):在同一个接口后面替换可互换的算法,例如定价、运费或折扣规则。
- 适配器(Adapter):用你的应用定义的接口包装第三方 API。
- 工厂(Factory):集中处理复杂对象的创建和校验,就像
Order.create那样。 - 观察者与领域事件:让系统的其他部分响应“订单已创建”,而订单本身无需知道谁在监听。
- 装饰器(Decorator):在现有实现外层添加缓存、日志或重试,而不修改它本身。
何时不该过度设计
整洁架构是一种工具,而不是一种信仰。一个业务逻辑很少的小型 CRUD 应用并不需要四层结构和一堆接口,这些繁文缛节的成本会超过它带来的收益。请根据代码的复杂度和预期生命周期,适度地运用这些理念:
- 无论项目大小,始终把业务规则放在控制器和 UI 组件之外。这几乎没有成本。
- 在有充分理由的地方引入端口和适配器:可能被替换的外部服务、需要快速测试的逻辑,或者 API、后台任务、CLI 等多个入口。
- 不要创建只有一个实现、也没有测试替身的接口。等到第二个需求出现时再加(you aren’t gonna need it,即 YAGNI)。
- 以修改真正令人痛苦的地方为指引,一次一个模块,逐步向这种架构重构。
要点回顾
- 可维护性来自对依赖的管理:让业务规则独立于框架和基础设施。
- 依赖指向内层。用例声明端口,外层提供适配器。
- SOLID 把同样的思路应用到类和模块上:单一职责、扩展而非修改、诚实的契约、小接口,以及依赖抽象。
- 让架构的复杂程度与问题本身的复杂度相匹配。
总结
归根结底,整洁架构是为了保护你最有价值的代码,也就是让业务运转的那些规则,使其免受框架、数据库和交付方式频繁变化的冲击。让依赖指向内层,让模块保持专注,并在不依赖基础设施的情况下测试业务逻辑。这样,你的代码库在第五年依然能像第一年一样快速响应变化。
延伸阅读
- #Architecture
- #Clean Architecture
- #Design Patterns
- #SOLID
- #TypeScript




