1. 为什么我们需要分层架构
第一次接手一个没有分层的单体项目时,我花了整整三天时间才理清一个简单接口的业务逻辑。所有的代码都堆在同一个Service类里,数据库操作、业务规则、参数校验、甚至日志打印全都混杂在一起。那次经历让我深刻理解了分层架构的价值。
分层架构的本质是将复杂的系统拆解为多个职责单一的层次,就像建造一栋大楼需要地基、结构、管道、装修等不同工种协同工作一样。在软件开发中,这种分层带来的好处主要体现在三个方面:
-
可维护性:当需要修改某个功能时,你只需要关注特定层次的代码。比如修改数据库访问方式时,只需改动Repository层,不会影响业务逻辑。
-
可测试性:每个层次都可以独立测试。你可以单独测试Domain层的业务规则,而不需要启动整个Web容器。
-
可扩展性:当系统需要增加新功能时,清晰的层次划分让你知道应该在哪个层面进行扩展,而不会破坏现有结构。
提示:分层不是目的而是手段,过度分层反而会增加系统复杂度。通常5-7层是大多数系统的合理范围。
2. 典型后端分层架构详解
2.1 Controller层:系统的门面
Controller层是系统对外的接口,就像公司的前台接待员。它的核心职责是:
- 协议转换:将HTTP/RPC/MQ等不同协议的请求转换为内部统一的调用方式
- 参数校验:使用注解如@Valid进行基础校验
- 结果组装:将内部对象转换为适合外部的DTO/VO
java复制@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderApplicationService orderService;
@PostMapping
public Result<OrderVO> createOrder(@Valid @RequestBody CreateOrderCommand command) {
Order order = orderService.createOrder(command);
return Result.success(OrderVO.from(order));
}
}
常见误区:
- 在Controller中直接操作数据库
- 在Controller中编写复杂业务逻辑
- 在Controller中管理事务
这些做法都会破坏分层原则,导致后续难以维护。
2.2 Application/Service层:业务流程的指挥家
Application层负责协调多个Domain对象完成一个完整的业务用例,就像乐团的指挥协调不同乐器演奏一首曲子。
核心特征:
- 关注业务流程而非业务规则
- 通常一个方法对应一个用户用例
- 负责事务管理
- 可能调用多个Domain Service
java复制@Service
public class OrderApplicationServiceImpl implements OrderApplicationService {
@Autowired
private OrderDomainService orderDomainService;
@Autowired
private PaymentDomainService paymentDomainService;
@Transactional
@Override
public Order createOrder(CreateOrderCommand command) {
// 校验库存
orderDomainService.checkInventory(command.getItems());
// 创建订单
Order order = orderDomainService.createOrder(command);
// 发起支付
paymentDomainService.createPayment(order);
return order;
}
}
注意:Application层应该很"薄",如果发现一个Application方法超过100行,很可能需要重构。
2.3 Domain层:业务规则的核心
Domain层是系统的核心,包含了所有的业务规则和领域知识。好的Domain层应该与技术实现完全解耦,即使换掉整个技术栈,Domain层代码也不需要修改。
Domain层主要组成:
- 实体(Entity):有唯一标识的对象,如Order、User
- 值对象(Value Object):没有唯一标识的对象,如Address、Money
- 领域服务(Domain Service):不适合放在实体中的业务逻辑
- 领域事件(Domain Event):业务过程中产生的重要事件
java复制public class Order {
private Long id;
private List<OrderItem> items;
private OrderStatus status;
public void cancel() {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("只有新建订单可以取消");
}
this.status = OrderStatus.CANCELLED;
this.addDomainEvent(new OrderCancelledEvent(this.id));
}
// 其他业务方法...
}
设计原则:
- 实体应该尽可能封装自己的状态和行为
- 避免贫血模型(只有getter/setter)
- 领域服务应该是无状态的
2.4 Repository层:数据的守门人
Repository层负责数据的持久化和检索,但它不是简单的DAO,而是面向领域模型的。
关键区别:
- DAO通常面向数据库表结构
- Repository面向领域模型
java复制public interface OrderRepository {
Order findById(OrderId orderId);
List<Order> findByCustomer(CustomerId customerId);
void save(Order order);
void delete(Order order);
}
实现上,Repository通常会使用JPA、MyBatis等技术,但这些细节应该完全隐藏在Repository实现中,上层不需要知道。
2.5 Infrastructure层:技术细节的实现者
Infrastructure层包含所有的技术实现细节:
- 数据库访问实现(JPA、MyBatis)
- 消息队列客户端
- 缓存客户端
- 文件存储
- 外部服务调用
java复制@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Autowired
private OrderJpaRepository jpaRepository;
@Override
public Order findById(OrderId orderId) {
return jpaRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
// 其他方法实现...
}
依赖方向:Infrastructure层依赖Domain层,而不是相反。所有技术细节的接口定义应该在Domain层,实现在Infrastructure层。
3. 分层架构的优势与价值
3.1 可维护性提升的实际案例
在我参与的一个电商项目中,最初没有明确分层,所有代码都写在Service中。当需要从MySQL迁移到MongoDB时,我们不得不修改数百个Service方法。后来重构为分层架构后,再次需要更换数据库时,只需要修改Repository实现,其他层完全不受影响。
变更影响分析:
| 变更类型 | 未分层架构影响范围 | 分层架构影响范围 |
|---|---|---|
| 数据库变更 | 所有Service方法 | 仅Repository实现 |
| API协议变更 | 所有Controller | 仅Controller层 |
| 业务规则变更 | 分散在各Service | 集中在Domain层 |
3.2 测试便利性的提升
分层后,各层的测试可以完全独立:
- Domain层:纯单元测试,不依赖任何框架
- Repository层:集成测试,测试数据库交互
- Application层:Mock Domain层的单元测试
- Controller层:Mock Application层的单元测试
java复制// Domain层测试示例
class OrderTest {
@Test
void should_allow_cancel_for_created_order() {
Order order = new Order();
order.cancel();
assertEquals(OrderStatus.CANCELLED, order.getStatus());
}
@Test
void should_reject_cancel_for_shipped_order() {
Order order = new Order();
order.ship();
assertThrows(IllegalStateException.class, order::cancel);
}
}
这种细粒度的测试使得问题定位更加容易,测试运行速度也更快。
4. 常见反模式及解决方案
4.1 贫血模型:业务逻辑的荒漠
贫血模型是指领域对象只有属性和getter/setter,所有业务逻辑都放在Service中的反模式。
问题:
- 业务逻辑分散在各Service中
- 领域对象只是数据容器
- 难以维护业务不变量
解决方案:
- 识别核心业务概念,建立丰富的领域模型
- 将业务逻辑移入领域对象
- 使用领域服务处理跨多个领域对象的逻辑
java复制// 反模式:贫血模型
public class Order {
// 只有属性和getter/setter
}
public class OrderService {
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId);
if (order.getStatus() != OrderStatus.CREATED) {
throw new IllegalStateException("只有新建订单可以取消");
}
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
}
// 正确模式:充血模型
public class Order {
private OrderStatus status;
public void cancel() {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("只有新建订单可以取消");
}
this.status = OrderStatus.CANCELLED;
}
}
4.2 过厚的Service层
当Service层方法过长(如超过100行),通常意味着:
- 包含了本应在Domain层的业务逻辑
- 一个方法做了太多事情
- 缺乏适当的抽象
重构策略:
- 提取Domain Service
- 使用策略模式处理复杂分支
- 引入领域事件解耦逻辑
java复制// 重构前
public class OrderService {
public void processOrder(Long orderId) {
// 50行校验逻辑
// 30行业务逻辑
// 20行通知逻辑
}
}
// 重构后
public class OrderService {
public void processOrder(Long orderId) {
Order order = orderRepository.findById(orderId);
orderValidator.validate(order);
orderProcessor.process(order);
orderNotifier.notify(order);
}
}
4.3 Controller直接访问Repository
这种反模式会导致:
- 事务难以管理
- 业务逻辑泄漏到Controller
- 系统难以维护
解决方案:
- 严格遵循分层调用顺序
- 通过代码审查确保没有绕过Service层
- 使用架构守护工具(如ArchUnit)自动检测
java复制// 错误示例
@RestController
public class OrderController {
@Autowired
private OrderRepository orderRepository;
@PostMapping("/orders/cancel")
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId);
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
}
// 正确示例
@RestController
public class OrderController {
@Autowired
private OrderApplicationService orderService;
@PostMapping("/orders/cancel")
public void cancelOrder(Long orderId) {
orderService.cancelOrder(orderId);
}
}
5. 分层架构的实践心得
在实际项目中应用分层架构时,我总结了以下几点经验:
-
明确层间边界:制定团队规范,明确每层的职责和禁止事项。可以使用ArchUnit等工具自动验证架构约束。
-
依赖方向控制:严格遵循"上层依赖下层"的原则,Domain层应该是技术无关的最稳定层。
-
包结构反映架构:通过包名明确体现分层,如com.example.order.application、com.example.order.domain等。
-
适度灵活:对于简单CRUD应用,可以适当简化分层,避免过度设计。
-
持续重构:随着业务复杂度的增加,及时调整分层结构,保持架构的适应性。
-
文档和示例:为新成员提供分层架构的文档和示例代码,加速团队理解。
分层架构不是银弹,但它为复杂业务系统提供了清晰的结构和边界。当项目规模增长到一定程度时,良好的分层设计带来的维护优势会越来越明显。关键在于平衡设计的严谨性和开发的效率,找到适合团队和项目的分层粒度。