架构风格全景:分层、管道、微内核与事件驱动

架构风格(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)来管理——每个事件记录:发布方、订阅方、幂等键、失败重试策略、可观测点。没有目录的事件驱动,半年后没人说得清全链路。

风格选择顺序

第一步 流程是否线性、数据能否逐步变形?   → 是:管道-过滤器
第二步 变化是否集中于同构变体?           → 是:微内核
第三步 是否存在多个互不关心的下游?       → 是:事件驱动
第四步 都不满足,且以业务用例为中心?     → 分层(最常见、最安全的默认值)
第五步 单一风格不够时,按边界组合,而不是全局套用一种

组合是常态:一个交易系统通常在顶层用分层组织业务用例,在支付渠道处用微内核,在风控与通知处用事件驱动,在报文处理处用管道-过滤器。风格是局部选择,不必全局统一。

常见误区

  • 风格当帽子戴:为"显得先进"全局上事件驱动,结果是没人能回答"这个请求走完了没有"。
  • 分层教条化:禁止跨层调用却允许层内随意互调,等于没有边界。
  • 微内核只学了插件注册:没定义扩展点的兼容与版本策略,插件一多就互相踩。
  • 事件驱动当同步调用用:发事件后立刻等待结果,既丢了解耦又添了复杂度,不如直接调用。
  • 忽略团队能力:风格背后是调试、运维、测试方式的整体改变,团队没准备好就会退化回同步调用。

小结:分层管好关注点与依赖方向,管道-过滤器管好可组合的处理步骤,微内核管好稳定核心与可变能力,事件驱动管好多下游解耦。选择依据是"变化集中在哪里、下游有几个",而不是流行程度;真实系统通常按边界组合多种风格。

笔记加载中…