全栈手账 订阅 RSS

软件架构模式

架构设计的分层与取舍指南:从分层架构、六边形架构讲到 DDD 的核心概念与适用边界,分析单体与微服务的真实成本,给出模块化单体的演进路径,并附常用设计模式的实战写法、依赖方向原则与架构决策记录方法。

读了 24 分钟 更新于 11 月 3 日

一、架构解决的是什么问题

架构不是「用什么框架」,而是在需求持续变化的前提下,控制改动的传播范围。

没有架构 有合理架构
改一个功能要动十几个文件 改动集中在一层/一个模块
业务逻辑散落在 Controller 里 业务逻辑集中在领域层
换数据库要重写半个项目 只换数据访问实现
无法单独测试业务逻辑 业务逻辑可脱离框架测试

判断架构好坏的一条实用标准:

当需求变化时,你能不能只改一处?改一处之后,是否不影响其他部分?


二、分层架构

2.1 经典四层

文本
Controller(接口层)     处理 HTTP/参数校验/响应组装      ↓Service(应用层)        编排业务流程、事务边界      ↓Domain(领域层)         核心业务规则、不依赖框架      ↓Repository(数据访问层) 数据库读写、外部服务调用

2.2 各层职责与禁止事项

层 该做 不该做
Controller 参数校验、调用 Service、组装响应 写业务判断、直接操作数据库
Service 编排流程、控制事务、组合领域对象 处理 HTTP 细节(header/cookie)
Domain 业务规则、不变量校验 依赖框架、直接查数据库
Repository 数据读写、查询优化 业务判断

2.3 代码示例

javascript
// Controller:只做接口适配router.post('/orders', async (ctx) => {  const dto = validateCreateOrder(ctx.request.body)     // 参数校验  const order = await orderService.create(dto, ctx.state.user.id)  ctx.body = { code: 0, data: { orderNo: order.orderNo } }})// Service:编排流程 + 事务class OrderService {  async create(dto, userId) {    const product = await productRepo.findById(dto.productId)    if (!product) throw new BusinessError('商品不存在')    const order = Order.create({ userId, product, quantity: dto.quantity })  // 领域逻辑在实体里    await transaction(async (trx) => {      await stockRepo.decrease(product.id, dto.quantity, trx)   // 条件更新防超卖      await orderRepo.save(order, trx)    })    await eventBus.emit('order.created', { orderNo: order.orderNo })  // 后续动作异步    return order  }}// Domain:业务规则内聚在实体中,可独立单测class Order {  static create({ userId, product, quantity }) {    if (quantity <= 0) throw new BusinessError('数量必须大于 0')    if (quantity > 99) throw new BusinessError('单次最多购买 99 件')    return new Order({      orderNo: generateOrderNo(),      userId,      productId: product.id,      quantity,      unitPrice: product.price,        // 价格以服务端商品数据为准      totalAmount: product.price * quantity,      status: 'pending',    })  }  markPaid(transactionId) {    if (this.status !== 'pending') throw new BusinessError('订单状态不允许支付')    this.status = 'paid'    this.paidAt = new Date()    this.transactionId = transactionId  }}

关键收益:Order 的业务规则不依赖数据库、框架,可以纯单元测试。

2.4 分层的常见误用

误用 后果
Controller 里写满 if/else 业务判断 逻辑无法复用、无法测试
Service 直接返回数据库实体 上层耦合数据结构,字段变更影响面大
Domain 层引入 ORM 注解 领域对象无法脱离框架测试
层与层之间相互调用(Controller 直接调 Repository) 绕过业务规则,产生不一致

三、六边形架构(端口与适配器)

3.1 核心理念

业务核心在中心,外部世界通过「端口」接入,由「适配器」实现具体技术。

code
         ┌─────────────┐  HTTP ──┤  Controller  │──┐         └─────────────┘  │         ┌─────────────┐  │      ┌──────────────────┐  CLI  ──┤   Command    │──┼─────►│   领域核心        │         └─────────────┘  │      │  (业务规则)      │         ┌─────────────┐  │      └──────────────────┘  MQ   ──┤   Consumer   │──┘              │         └─────────────┘                 │                                 ┌───────┴────────┐                                 │  端口(接口)    │                                 └───────┬────────┘                          ┌──────────────┼──────────────┐                     ┌────▼────┐   ┌─────▼─────┐  ┌─────▼─────┐                     │ MySQL   │   │  Redis    │  │ 第三方 API │                     │ 适配器   │   │  适配器    │  │  适配器    │                     └─────────┘   └───────────┘  └───────────┘

3.2 端口与适配器

javascript
// 端口:领域层定义的接口(不关心实现)// domain/ports/OrderRepository.jsclass OrderRepository {  async findById(id) { throw new Error('not implemented') }  async save(order) { throw new Error('not implemented') }}// 适配器:基础设施层实现// infrastructure/persistence/MysqlOrderRepository.jsclass MysqlOrderRepository extends OrderRepository {  async findById(id) {    const row = await db.query('SELECT * FROM orders WHERE id = ?', [id])    return row ? toOrderEntity(row) : null  }  async save(order) {    await db.query('INSERT INTO orders ... ON DUPLICATE KEY UPDATE ...', [...])  }}// 测试用内存适配器:无需数据库即可测业务class InMemoryOrderRepository extends OrderRepository {  constructor() { super(); this.store = new Map() }  async findById(id) { return this.store.get(id) || null }  async save(order) { this.store.set(order.id, order) }}

最大收益:业务逻辑的测试不再需要数据库;换存储只需新写一个适配器。

3.3 何时值得引入

情况 建议
业务规则复杂、需要大量单测 ✅ 值得
需要支持多种存储/多种接入方式 ✅ 值得
简单 CRUD 后台 ❌ 过度设计,分层架构足够

四、DDD 入门

4.1 核心概念

概念 含义 示例
限界上下文 业务边界,同一术语在不同上下文含义不同 「商品」在销售上下文有价格,在仓储上下文有库位
实体(Entity) 有唯一标识、生命周期内可变 订单、用户
值对象(Value Object) 无标识、不可变、按值比较 金额、地址、时间区间
聚合(Aggregate) 一组相关对象的一致性边界 订单 + 订单项
聚合根 聚合的唯一入口,外部只能通过它访问 订单是订单项的聚合根
领域事件 领域中发生的事实 订单已支付、库存已扣减
仓储(Repository) 聚合的持久化抽象 OrderRepository

4.2 值对象示例

javascript
// ✅ 值对象:把规则内聚,避免散落各处的判断class Money {  constructor(amountInCents, currency = 'CNY') {    if (!Number.isInteger(amountInCents)) throw new Error('金额必须为整数分')    if (amountInCents < 0) throw new Error('金额不能为负')    this.amount = amountInCents    this.currency = currency    Object.freeze(this)  }  add(other) {    if (other.currency !== this.currency) throw new Error('币种不一致')    return new Money(this.amount + other.amount, this.currency)  }  multiply(n) { return new Money(Math.round(this.amount * n), this.currency) }  toString() { return (this.amount / 100).toFixed(2) }}

收益:金额的「非负、整数、币种一致」规则只写一次,不会在每个 Service 里重复判断。

4.3 聚合的边界设计

原则:

  1. 一个事务只修改一个聚合——跨聚合用领域事件最终一致。
  2. 聚合宜小不宜大——聚合越大,并发冲突越多、加载越慢。
  3. 外部通过聚合根访问内部对象,不直接操作子对象。
javascript
// ✅ 订单项只通过订单修改class Order {  addItem(product, quantity) {    if (this.status !== 'pending') throw new Error('已提交的订单不能修改')    this.items.push(new OrderItem(product, quantity))    this.recalculateTotal()  }}// ❌ 绕过聚合根直接改子对象,规则被跳过order.items.push(new OrderItem(...))

4.4 何时用 DDD、何时不用

适合 不适合
业务规则复杂、术语多、多方角色 纯 CRUD 数据维护
长期迭代、规则频繁变化的核心域 简单表单、报表查询
团队需要统一语言沟通 一次性项目、原型验证

实用建议:不必全项目套 DDD。只对「核心域」用(如交易、计价、风控),其余部分用简单分层即可。强行给 CRUD 上聚合根,是最常见的过度设计。


五、单体 vs 微服务

5.1 客观对比

维度 模块化单体 微服务
部署 一个进程 N 个服务独立部署
开发效率(初期) ✅ 高 ❌ 低(需处理服务间通信)
事务一致性 ✅ 简单(本地事务) ❌ 复杂(分布式事务/Saga)
故障隔离 ❌ 一处崩溃全站崩 ✅ 单服务故障可降级
独立扩容 ❌ 只能整体扩 ✅ 热点服务单独扩
技术异构 ❌ 单一技术栈 ✅ 各服务自选
运维复杂度 ✅ 低 ❌ 高(注册发现、链路追踪、配置中心)
团队规模要求 小团队更合适 需要独立团队负责各服务

5.2 微服务的真实成本

成本项 说明
分布式事务 跨服务数据一致性需要 Saga/TCC/最终一致,复杂度陡增
网络故障 调用可能超时、重试、幂等,处处要考虑
调试困难 一次请求跨多个服务,必须上链路追踪
数据一致性 无法使用数据库外键与联表查询
运维负担 服务注册、配置管理、网关、日志聚合、监控告警
组织要求 康威定律:服务边界往往映射团队边界,团队不够就是负担

5.3 判断标准

不要上微服务,如果:

  • 团队规模小于 10 人
  • 单体部署时间 < 5 分钟、故障影响可接受
  • 没有独立的运维与监控能力
  • 业务边界还不清晰(此时拆分会拆错)

可以考虑微服务,如果:

  • 单体已大到「改动互相阻塞、发布排队」
  • 存在明显不同伸缩需求的模块(如报表服务吃 CPU、网关吃连接)
  • 有多个团队需要独立发布节奏
  • 已具备完善的 CI/CD、监控、链路追踪基础设施

5.4 推荐路径:模块化单体优先

文本
阶段一:分层单体  └─ Controller / Service / Repository,按技术分层阶段二:模块化单体(多数项目的最优解)  └─ 按业务模块切分,模块内分层,模块间只通过明确接口通信阶段三:仅在必要时抽取独立服务  └─ 某个模块需要独立扩容或有独立发布节奏 → 拆出去

模块化单体的目录结构:

code
src/├── modules/│   ├── order/│   │   ├── controller.js│   │   ├── service.js│   │   ├── domain/│   │   ├── repository.js│   │   └── index.js          ← 唯一对外出口│   ├── product/│   └── user/├── shared/                   ← 共享工具、通用组件└── app.js

约束:模块之间只能通过 index.js 暴露的接口调用,不允许直接引用对方内部文件。

javascript
// modules/order/service.jsconst productModule = require('../product')      // ✅ 通过模块出口// const productRepo = require('../product/repository')  // ❌ 禁止跨模块直接访问内部class OrderService {  async create(dto) {    const product = await productModule.getForSale(dto.productId)   // 走公开接口  }}

收益:模块间依赖清晰,将来拆微服务时边界已经现成。


六、常用设计模式实战

设计模式的价值不在「用得多」,而在命名一个反复出现的问题的解法。

6.1 策略模式(替换大量 if/else)

javascript
// ❌ 分支爆炸function calcFreight(type, weight) {  if (type === 'standard') return weight * 2  else if (type === 'express') return weight * 5 + 10  else if (type === 'free') return 0  // 每加一种就要改这个函数}// ✅ 策略表const FREIGHT_STRATEGIES = {  standard: (w) => w * 2,  express: (w) => w * 5 + 10,  free: () => 0,}function calcFreight(type, weight) {  const strategy = FREIGHT_STRATEGIES[type]  if (!strategy) throw new Error('未知运费类型')  return strategy(weight)}

识别信号:同一个 switch/if-else 因为新需求被反复修改。

6.2 工厂模式(统一构建复杂对象)

javascript
const PAYMENT_PROVIDERS = {  alipay: () => new AlipayProvider(cfg.alipay),  wechat: () => new WechatProvider(cfg.wechat),  balance: () => new BalanceProvider(),}function createPaymentProvider(type) {  const factory = PAYMENT_PROVIDERS[type]  if (!factory) throw new Error('不支持的支付方式')  return factory()}

6.3 装饰器模式(横切关注点)

javascript
// 限流 + 缓存 + 日志,用装饰器叠加,不污染业务代码const getProducts = withCache(60)(withRateLimit(100)(withLog(baseGetProducts)))

6.4 观察者 / 事件模式(解耦后续动作)

javascript
// 下单后要发短信、加积分、写报表 —— 用事件解耦,而不是在 Service 里逐个调用eventBus.on('order.created', sendSmsHandler)eventBus.on('order.created', addPointsHandler)eventBus.on('order.created', writeReportHandler)

注意:事件是异步的,要接受最终一致;关键流程(如扣库存)不能只靠事件。

6.5 模式使用原则

原则 说明
先有问题,再找模式 为用模式而用模式是最常见的浪费
模式要能命名 团队能说「这里是策略模式」才算达成共识
简单优先 三行能解决的不要引入工厂 + 接口 + 注册表

七、依赖方向原则

7.1 依赖倒置

文本
❌ 业务层依赖具体实现Domain ──► MySQL ──► 具体驱动✅ 业务层依赖抽象,实现反向注入Domain ──► 接口(端口)◄── MySQL 适配器
javascript
// ✅ 依赖注入:业务层接收抽象的仓储class OrderService {  constructor(orderRepo) { this.orderRepo = orderRepo }   // 只依赖接口}// 生产环境const service = new OrderService(new MysqlOrderRepository())// 测试环境const service = new OrderService(new InMemoryOrderRepository())

7.2 三条硬规则

规则 说明
内层不依赖外层 领域层不能 import 框架、ORM、HTTP 库
依赖抽象而非实现 Service 依赖 OrderRepository 接口,不依赖 MysqlOrderRepository
跨模块只走公开接口 不直接访问其他模块的内部文件

八、架构决策记录(ADR)

8.1 为什么需要

半年后没人记得「当初为什么不用 X 方案」。重新讨论一次的成本极高,还容易推翻已有合理设计。

8.2 简化模板(一页纸)

markdown
# ADR-003:订单模块采用模块内分层,暂不拆微服务日期:2026-01-01状态:已采纳## 背景订单模块业务规则复杂,但团队仅 5 人,且部署时间 < 3 分钟。## 决策采用模块化单体:订单作为独立模块,模块内分层(Controller/Service/Domain/Repository),模块间只通过 index.js 公开接口通信;暂不拆分独立服务。## 备选方案- 直接拆微服务:需要引入注册中心、链路追踪,运维成本超出团队承受范围。- 纯技术分层:模块边界不清晰,订单与商品逻辑相互渗透。## 后果- 正面:开发效率高、本地事务简单、部署简单。- 负面:订单高峰期无法单独扩容。- 触发重新评估的条件:订单模块 QPS 超过单机承载的 60%,或团队规模超过 15 人。

8.3 什么决策值得记录

值得记 不必记
技术选型(框架、数据库、中间件) 变量命名
架构分层与模块边界 代码格式
关键取舍(一致性 vs 可用性) 显而易见的实现细节
暂不做什么及原因 —

九、常见误区

误区 事实
「先设计完美架构再开发」 业务边界要靠实践验证,过度设计会阻碍调整
「小项目不需要分层」 小项目更需要简单结构,否则三个月后就是泥球
「微服务更先进」 微服务是组织与规模的解法,不是技术先进的证明
「DDD 就是分层 + 一堆名词」 没有统一语言与业务边界,DDD 只剩复杂度
「设计模式用得越多越好」 模式是给重复问题的命名,不是目标
「分层就是多写几个文件」 分层的价值在依赖方向,不是文件数量

十、检查清单

分层

  • Controller 无业务判断,只做参数校验与响应组装
  • 业务规则内聚在 Service/Domain,不在 Controller
  • 上层不直接访问 Repository 绕过业务规则
  • 领域逻辑可脱离框架与数据库单测

模块边界

  • 按业务模块(而非技术类型)组织目录
  • 模块间只通过公开接口(index.js)通信
  • 模块内部分层,外部不可见

依赖

  • 内层不依赖外层框架
  • 依赖抽象(接口)而非具体实现
  • 关键依赖通过构造注入,便于测试替换

选型

  • 单体/微服务选择基于团队规模与真实痛点,而非跟风
  • 若需要拆分,先做到模块化单体
  • 已评估微服务的隐藏成本(分布式事务、链路追踪、运维)

文档

  • 关键架构决策有 ADR(背景、决策、备选、后果)
  • ADR 写明了「何时重新评估」的触发条件

十一、总结

架构的核心只有三句话:

  1. 分层的价值在依赖方向——内层不依赖外层,业务逻辑能脱离框架测试。文件分几个目录不重要,依赖指向才重要。
  2. 模块边界的价值在改动收敛——按业务模块切分、只走公开接口,让「改一个需求只动一个模块」成为可能。这也是将来拆服务的现成边界。
  3. 微服务是组织规模的解法,不是技术先进的标志——团队小、边界不清时拆微服务,只会把复杂度乘以服务数。

落地顺序建议:

文本
先分层(Controller/Service/Domain/Repository)  ↓再模块化(按业务切分,模块间只走公开接口)  ↓核心域引入 DDD(值对象、聚合、领域事件)  ↓确有瓶颈才拆服务

最后一条实践建议:把架构规则写进 lint 或 CI(如禁止跨模块直接引用、禁止 Controller 引用 Repository)。只写在文档里的架构约束,通常半年后就会被绕过。


完

前端、后端、部署、客服都是我一个人。写这里的东西都是真踩过的坑,不是抄来的教程。