架构风格全景:分层、管道、微内核与事件驱动
架构风格(architectural style)是一组可复用的结构约定:它规定了部件怎么切、怎么连、数据往哪流。选风格不是选审美,而是选"哪种耦合能接受、哪种演进能承受"。下面四种是后端最常用的基本型,看懂它们的机制差异,比记住名字重要得多。
四种风格的机制对比
| 维度 | 分层(layered) | 管道-过滤器(pipes and filters) | 微内核(microkernel) | 事件驱动(event-driven) |
|---|---|---|---|---|
| 部件单位 | 层(layer) | 过滤器(filter) | 核心系统 + 插件(plugin) | 事件生产者 / 消费者 |
| 连接方式 | 相邻层调用 | 管道(pipe)传递数据 | 插件注册到扩展点 | 事件通道(broker / mediator) |
| 控制流 | 请求自上而下 | 数据推过一串过滤器 | 核心按扩展点回调插件 | 由事件触发,无固定顺序 |
| 主要收益 | 关注点分离、可替换实现 | 步骤可组合、可并行、易测试 | 核心稳定、能力可插拔 | 极致解耦、易加订阅方 |
| 主要代价 | 层数膨胀、可能逐层透传 | 端到端延迟叠加、共享状态难 | 扩展点设计难、调试跨插件 | 时序与一致性推理困难 |
| 典型场景 | 业务增删改查应用 | 数据清洗、ETL、编解码链、报文校验 | IDE、规则引擎、网关插件、可配置产品 | 订单域事件、通知分发、审计与积分 |
分层(layered)
最常见的起点:接入层 → 应用层 → 领域层 → 基础设施层。要点是相邻层调用 + 依赖单向。
接入层(handler) 解析协议、鉴权、组装响应
应用层(app) 编排用例、事务边界
领域层(domain) 业务规则与不变量
基础设施层(infra) 数据库、缓存、外部接口的实现
分层最大的陷阱是透传层(pass-through layer):应用层只有一行 return svc.Do(req),没有编排逻辑。这时它不产生价值,只增加阅读成本,应当合并。分层不是越多越好,而是每一层都必须承担一项别人不做的职责。
管道-过滤器(pipes and filters)
把处理拆成独立步骤,数据单向流过:
type Filter func(ctx context.Context, in []byte) ([]byte, error)
// 装饰顺序即执行顺序:解压 → 校验签名 → 解密 → 反序列化
func Chain(fs ...Filter) Filter {
return func(ctx context.Context, in []byte) ([]byte, error) {
var err error
for _, f := range fs {
if in, err = f(ctx, in); err != nil {
return nil, err
}
}
return in, nil
}
}
适合数据处理与协议解析,因为每个过滤器都能单独测试,也能按需重排、并行(如多路分支)。不适合需要全局共享状态或频繁回退的流程;端到端延迟是各步之和,步骤一多要警惕"管道越来越长、没人敢动中间那节"。
微内核(microkernel)
核心只保留稳定的最小能力 + 一组扩展点(extension point),可变部分做成插件:
type Payload struct {
OrderID string
Amount int64
Method string
}
// 扩展点:核心定义接口,插件实现
type PayChannel interface {
Name() string
Pay(ctx context.Context, p Payload) (string, error)
}
// 核心不变,新渠道只新增一个实现并注册
type Registry struct{ channels map[string]PayChannel }
func (r *Registry) Register(c PayChannel) { r.channels[c.Name()] = c }
func (r *Registry) Dispatch(ctx context.Context, p Payload) (string, error) {
c, ok := r.channels[p.Method]
if !ok {
return "", fmt.Errorf("unsupported method: %s", p.Method)
}
return c.Pay(ctx, p)
}
判断是否适合微内核,看两件事:核心逻辑是否长期稳定,以及变化是否集中在若干同构的变体(渠道、规则、格式、算法)。若变化是零散的、跨领域的,硬造扩展点只会得到一堆空接口。
事件驱动(event-driven)
生产者发布事实,不关心谁消费。两种拓扑:
中间人拓扑(broker):生产者 → 通道 → 消费者A
→ 消费者B
事件调解者(mediator):生产者 → 调解者(知道流程与顺序)→ 消费者
要点:拓扑越靠近 broker,解耦越强、流程越不可见;
越靠近 mediator,流程越清晰、调解者越容易变成新的上帝对象。
事件驱动的代价不在消息中间件本身(投递语义、顺序、幂等属于 middleware-interview 相关章节),而在架构层面的推理成本:一次下单会触发多少个消费者、它们各自的失败会不会让业务看起来"做了一半",这些必须靠事件目录(event catalog)来管理——每个事件记录:发布方、订阅方、幂等键、失败重试策略、可观测点。没有目录的事件驱动,半年后没人说得清全链路。
风格选择顺序
第一步 流程是否线性、数据能否逐步变形? → 是:管道-过滤器
第二步 变化是否集中于同构变体? → 是:微内核
第三步 是否存在多个互不关心的下游? → 是:事件驱动
第四步 都不满足,且以业务用例为中心? → 分层(最常见、最安全的默认值)
第五步 单一风格不够时,按边界组合,而不是全局套用一种
组合是常态:一个交易系统通常在顶层用分层组织业务用例,在支付渠道处用微内核,在风控与通知处用事件驱动,在报文处理处用管道-过滤器。风格是局部选择,不必全局统一。
常见误区
- 风格当帽子戴:为"显得先进"全局上事件驱动,结果是没人能回答"这个请求走完了没有"。
- 分层教条化:禁止跨层调用却允许层内随意互调,等于没有边界。
- 微内核只学了插件注册:没定义扩展点的兼容与版本策略,插件一多就互相踩。
- 事件驱动当同步调用用:发事件后立刻等待结果,既丢了解耦又添了复杂度,不如直接调用。
- 忽略团队能力:风格背后是调试、运维、测试方式的整体改变,团队没准备好就会退化回同步调用。
小结:分层管好关注点与依赖方向,管道-过滤器管好可组合的处理步骤,微内核管好稳定核心与可变能力,事件驱动管好多下游解耦。选择依据是"变化集中在哪里、下游有几个",而不是流行程度;真实系统通常按边界组合多种风格。