第3章 技术维度边界划分:层次与模块的实战解剖

技术维度的边界划分是架构落地中最具操作性的活动。它不依赖于业务需求的风向,而是基于软件运行的固有规律:依赖方向、执行频率、变化速率、安全信任。本章将沿着“分层 → 模块化 → 插件化”这条从刚性到柔性的路径,剖析每一层级的边界标准、通信规则与权衡陷阱。

3.1 核心概念与边界驱动因素

技术边界只有三种基本范式:分层、模块化、插件化。它们并非互斥,而是可以共存于同一系统:外层使用分层,中层使用模块化,内层使用插件化。

边界范式 定义 驱动因素 信任模型 交付方式
分层架构 按处理抽象程度划分垂直堆叠的层级 标准化、可替换性、职责分离 上层信任下层,下层不信任上层 单体包 / 进程内库
模块化 按功能或领域水平划分独立单元 独立编译、并行开发、隔离失效 通过接口契约信任 JAR / DLL / 共享库
插件化 通过扩展点动态加载的外部功能单元 运行时扩展、第三方集成、热替换 沙箱隔离、版本校验 独立进程 / 容器 / 动态库

3.2 分层架构:四层模型的职责与信任链路

我们要关注的是四层架构:表现层、应用层、领域层、基础设施层。每一层都有严格的依赖方向——外层依赖内层,且依赖必须通过接口抽象穿过层级边界,而不是直接构造实现。这是依赖倒置原则(DIP)在分层中的落地。

3.2.1 层间通信规则与CQS策略

控制流方向与数据流方向可以分离。在严格分层中,控制流是上一级调用下一级;但在使用了《依赖倒置》的场景中,控制流与数据流会呈现“反转”现象。

  • 表现层 → 应用层:DTO或简单VO传参,不携带领域行为。
  • 应用层 → 领域层:传递领域对象(Entity/ValueObject)或领域ID。应用层协调事务、安全、日志,但不实现业务规则。
  • 领域层 → 基础设施层:通过Repository接口、UnitOfWork接口依赖。领域层永远不直接引用ORM注解或数据库驱动。
  • 基础设施层:实现所有接口,无向上依赖。
// 领域层定义接口(不依赖基础设施)
public interface OrderRepository {
    Order findById(OrderId id);
    void save(Order order);
}

// 基础设施层实现接口(使用JPA)
public class JpaOrderRepository implements OrderRepository {
    @PersistenceContext
    private EntityManager em;

    @Override
    public Order findById(OrderId id) {
        return em.find(Order.class, id.getValue());
    }
}
边界验证准则:在代码评审中,检查组是否有“上层直接new下层具体类”的行为。如果表现层出现了 new JpaOrderRepository(),则属于信任链路断裂,必须重构。

3.3 模块化边界:共享内核与OSGI风格

模块化解决了“单层内部膨胀”的问题。在DDD战术设计中,每个限界上下文自然就是一个模块。模块之间的通信必须经过共享内核(Shared Kernel)或防腐层(ACL),而不是直接交换内部实体。

3.3.1 共享内核契约清单

设计模块边界时,以下三类内容跨模块共享时必须达成版本一致:

  • 事件结构(Event Schema):订单支付成功事件、用户注册事件。
  • 值对象(Value Objects):Money、Address、PhoneNumber。
  • 领域服务接口(Domain Service Interface):如DiscountPolicy。

而不应共享的内容包括:DAO接口、JPA实体注解(@Table、@Entity)、Dubbo/Thrift生成的桩代码。这些跨模块共享会带来耦合爆炸。

// 模块A: 用户上下文 — 共享内核中的事件定义
public record UserRegisteredEvent(UserId userId, String email, Instant occurredAt) {}

// 模块B: 通知上下文 — 通过消息总线消费该事件
public class WelcomeMailConsumer {
    @EventHandler
    public void on(UserRegisteredEvent event) {
        mailService.sendWelcome(event.email());
    }
}

3.4 插件化边界:SPI与扩展点

插件化是最高灵活度的技术边界,适用于功能热插拔、三方支付对接、自定义处理链等场景。其核心要素只有一个:接口契约稳定,实现可运行时替换。

  • 插件生命周期:加载(初始化)→ 激活(装载配置)→ 执行(服务请求)→ 挂起(更新)→ 卸载。
  • 信任链路模型:宿主应用只信任插件声明的版本号与API签名。插件调用宿主服务需要通过ServiceRegistry,而不是直接持有宿主内部Bean。
// 插件接口定义(宿主包内声明)
public interface PaymentPlugin {
    String getProviderName();
    PaymentResult charge(PaymentRequest request);
    void onLoad(PluginContext context);
    void onUnload();
}

// 插件实现(第三方独立JAR)
public class WechatPayPlugin implements PaymentPlugin {
    @Override
    public PaymentResult charge(PaymentRequest request) {
        // 调用微信支付SDK
        return PaymentResult.success(request.transactionId());
    }
}
常见陷阱:插件接口中使用泛型、Lambda、Stream时,宿主与插件可能因JDK版本不同导致序列化失败。解决方案:插件接口只接受基本类型 + 简单POJO,避免函数式回调。

3.5 选型权衡:分层 vs 模块化 vs 插件化

无法用一个维度衡量三者谁更优,应从以下角度权衡:

维度 分层 模块化 插件化
开发复杂度 低(固定结构) 中(需定义共享内核) 高(需管理插件生命周期与版本)
测试隔离性 中(Mock下层) 高(独立测试模块) 高(加载模拟插件)
热部署能力 不支持 类加载器级别 运行时完全支持
安全性 高(信任方向明确) 中(跨模块调用需校验) 低(需沙箱)
适用场景 CRUD系统、标准领域 大型单体、多上下文DDD 三方集成、引擎、平台

3.6 风险点与落地步骤

以下是实施技术边界划分时的关键风险及应对:

  • 过度抽象:在分层架构中为每一层都定义接口,导致代码量膨胀300%且无实际收益。应对:只在“可能被替换”的边界定义接口,例如Repository、外部API封装。
  • 共享内核膨胀:模块化初期放入共享内核的内容过多,每个模块都依赖整个共享内核。应对:采用事件风暴明确核心概念,共享内核体积控制在总代码的5%以内。
  • 插件版本失控:宿主更新接口后,旧插件无法加载,导致线上故障。应对:强制插件接口保留major.minor.patch版本号,宿主加载时校验major版本完全一致。
落地第3章的三步法
1. 切割:按“表现层/应用层/领域层/基础设施层”拆分当前模块的包结构,确保依赖方向从外向内。
2. 抽象:为基础设施层依赖的接口(Repository、MessageBus、Cache)创建独立接口定义。
3. 验证:使用 jdeps 或 ArchUnit 编写测试,断言各层依赖方向正确,无循环依赖。

3.7 验收标准

  1. 所有包依赖方向从外指向内,不存在“基础设施层引用表现层类”的情况。
  2. 表现层、应用层、领域层均不包含 @Entity、@Table、SQL语句、HTTP客户端等基础设施框架注解或调用。
  3. 模块间通信全部通过共享内核的事件或接口完成,无直接对象引用。
  4. 插件工程独立构建,不依赖宿主代码的编译产物;插件加载后能正常注册到 ServiceRegistry 并能处理至少 1 个请求。
  5. 单元测试中,领域层核心逻辑可使用内存实现完成测试,无需启动数据库或网络。
今日思考:如果你现在的系统全是“扁平包结构”,所有类混在一起;尝试找出最内层的3个类(最稳定、变化最慢、无外部依赖),它们应该成为领域层的起点。把这三个类划为一个包,去除对框架的依赖——这就是技术边界划分的第一步。