4. 分层架构中的边界策略:展示层、应用层、领域层、基础设施层的职责与依赖方向
分层架构(Layered Architecture)是软件系统中最经典、最广泛应用的架构模式之一。在功能边界划分的实践中,分层架构提供了一套严格的“物理隔离”规范:每一层有明确的职责边界,依赖方向必须是单向的。任何跨越边界的调用都必须遵守“上层依赖下层”的铁律,禁止下层反向依赖上层或同层之间的横向穿透。
本章将深入剖析展示层、应用层、领域层、基础设施层的职责边界,以及它们之间的依赖方向、信任链路和数据流动方式。理解这些策略是后续所有边界识别工作的基础。
核心原则:依赖倒置原则(DIP)是分层边界正确的基石。高层模块(领域层)不应依赖低层模块(基础设施层),两者都应依赖抽象接口。
4.1 常见误区与破局
许多团队在实施分层架构时,往往陷入“伪分层”的困境,导致边界模糊、耦合严重。下表列出了三种最常见的错误分层实践及其后果:
| 误区类型 | 错误表现 | 后果 |
|---|---|---|
| 贫血模型 | 领域层只包含数据(Entity/POJO),所有业务逻辑放在应用层或服务层 | 领域层退化,业务规则散落,变更影响大 |
| 基础设施泄漏 | 领域层直接使用ORM注解、数据库连接、HttpRequest对象 | 领域层无法独立测试,更换数据库或框架代价高 |
| 跨层穿透 | 展示层直接调用基础设施层的Repository或DAO | 跳过业务规则,安全与一致性失控 |
诊断方法:如果在代码审查中发现 @Entity 注解出现在领域实体上,或者领域服务方法中出现了 Connection 参数,说明边界已经被打破。
4.2 四层职责与依赖方向定义
我们采用标准的四层架构模型,每层的职责边界如下:
- 展示层(Presentation): 用户界面、API端点、请求/响应处理。职责仅限于接收输入、格式化输出、路由分发。不包含任何业务逻辑。
- 应用层(Application): 用例编排、事务控制、权限验证、事件发布。作为领域层和展示层之间的“薄胶水”,不包含领域规则。
- 领域层(Domain): 核心业务逻辑、实体、值对象、领域服务、聚合根。是整个系统的核心,零依赖基础设施。
- 基础设施层(Infrastructure): 数据库访问、消息队列、文件系统、外部API调用。提供具体的技术实现。
依赖方向为单向:展示层 → 应用层 → 领域层 ← 基础设施层(通过接口反向依赖)。
4.3 数据流与控制流边界
数据流和控制流在分层边界中具有不同的穿透规则:
- 控制流: 总是从上层发起,逐层向下传递。展示层调用应用层,应用层调用领域层。领域层的回调通过接口反向传递给基础设施层(例如依赖注入框架控制对象创建)。
- 数据流: 跨层传递时,必须使用边界对象(DTO、VO)。禁止将领域实体直接暴露给展示层,避免遗漏聚合边界或触发懒加载异常。
高风险操作: 将领域实体(Entity)直接作为 JSON 响应序列化返回给前端。这会导致:1) 聚合边界被破坏;2) 序列化触发未加载的关联数据(N+1问题);3) 内部状态暴露给外部。必须使用 DTO 进行数据裁剪。
4.4 信任链路与安全边界
分层架构中的信任是递减的:领域层是最信任的核心,而展示层和基础设施层是“不可信”的边缘。
| 信任层级 | 安全策略 | 举例 |
|---|---|---|
| 展示层 | 输入校验(格式、长度、类型) | JSR-303 Bean Validation |
| 应用层 | 权限校验、会话验证、防重放 | Spring Security @PreAuthorize |
| 领域层 | 业务规则不变性、聚合内一致性 | 实体方法中校验状态约束 |
| 基础设施层 | SQL注入防护、连接池管理、限流 | MyBatis参数绑定、Redis连接池 |
4.5 选型权衡与风险点
分层架构并非银弹,在以下场景中需要权衡:
- 过度分层导致“垃圾代码”: 如果业务逻辑非常简单(如纯 CRUD),四层架构会产生大量的样板代码和参数传递。此时应退化为“三层架构”或“扁平化”。
- 依赖注入陷阱: 过度使用框架注入会导致对象创建和生命周期控制权完全交给容器,边界变得不可见。需要对注入点进行审计,确保只有应用层和基础设施层之间允许使用注入。
- 性能开销: 每次跨层调用都会产生对象复制和上下文切换。在极高性能要求的场景下(如网关、实时计算),需要用
struct或Span这类低开销的边界对象。
4.6 落地步骤
启用分层架构边界策略的推荐步骤:
- 定义模块结构: 按照层创建 Maven/Gradle 模块(
presentation、application、domain、infrastructure),在build.gradle中严格配置编译依赖方向。 - 编写依赖规则脚本: 使用 ArchUnit 或自定义 Checkstyle 规则,在单元测试阶段强制校验层间依赖。
- 实现依赖倒置: 在领域层定义 Repository 接口,在基础设施层实现。应用层使用构造函数注入接口。
- 建立 DTO 映射层: 在应用层或展示层定义
xxxDTO,使用 MapStruct 或手工转换,确保领域实体不直接暴露。
// 领域层:定义边界接口(无外部依赖)
public interface AccountRepository {
Account findById(String accountId);
void save(Account account);
}
// 基础设施层:实现接口(依赖Spring Data JPA)
@Service
public class AccountRepositoryImpl implements AccountRepository {
@Autowired
private AccountJpaRepository jpaRepository;
@Override
public Account findById(String accountId) {
AccountEntity entity = jpaRepository.findById(accountId)
.orElseThrow(() -> new AccountNotFoundException(accountId));
return entity.toDomain(); // 基础设施层内部转换
}
}
// 应用层:使用接口,不关心实现
@Service
public class TransferService {
private final AccountRepository accountRepository;
public TransferService(AccountRepository accountRepository) {
this.accountRepository = accountRepository; // 依赖注入
}
public void transfer(String fromId, String toId, Money amount) {
Account from = accountRepository.findById(fromId);
Account to = accountRepository.findById(toId);
from.withdraw(amount);
to.deposit(amount);
accountRepository.save(from);
accountRepository.save(to);
}
}
4.7 验收标准
完成本章的边界策略实施后,应通过以下验收检查:
- 编译期隔离: 尝试在领域层的 Java 文件中导入
javax.persistence、org.springframework.web、java.sql.Connection等基础设施类时,编译报错。 - 单元测试可模拟: 领域层所有核心用例可以完全通过 Mock 基础设施接口进行单元测试,不需要启动数据库或 Web 容器。
- 依赖方向验证: ArchUnit 测试通过,确保不存在应用层直接调用基础设施层(除非经过接口间接调用)的情况。
- DTO 隔离: 在展示层和领域层之间没有直接的类型引用(如展示层不 import 领域层 Entity)。