#分层架构
#一次代码审查的争论
2024年,我们团队 code review 时爆发了一场争论:
一位同学把业务校验逻辑写在了 Controller 层:
@Controller
class OrderController {
@PostMapping("/orders")
public Result<Order> create(@RequestBody OrderDTO dto) {
// 业务校验写在 Controller
if (dto.getAmount() <= 0) {
return Result.error("金额必须大于0");
}
if (dto.getAmount() > 100000) {
return Result.error("单笔金额不能超过10万");
}
if (!userService.exists(dto.getUserId())) {
return Result.error("用户不存在");
}
// 调用服务
return orderService.create(dto);
}
}高级开发说:"业务逻辑应该放在 Service 层。"
初级开发反驳:"校验逻辑放在 Controller 不也能用吗?测试也方便。"
这场争论的本质是:分层架构中每一层的职责边界是什么?
#一、分层架构的本质
分层架构的核心思想:让每一层只关注自己该关注的事情。
┌─────────────────────────────────────┐
│ Presentation Layer │ 展示层:接收请求,返回响应
├─────────────────────────────────────┤
│ Business Layer │ 业务层:核心业务逻辑
├─────────────────────────────────────┤
│ Persistence Layer │ 持久层:数据读写
├─────────────────────────────────────┤
│ Database │ 数据库:数据存储
└─────────────────────────────────────┘
上层依赖下层,下层不依赖上层#二、传统三层架构🔴
#2.1 基本结构
// 表现层(Controller)
class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public UserDTO getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
}
// 业务层(Service)
class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public UserDTO getUserById(Long id) {
User user = userRepository.findById(id);
return convertToDTO(user);
}
}
// 持久层(Repository)
interface UserRepository extends JpaRepository<User, Long> {
}
// 数据层(Entity)
@Entity
class User {
@Id
private Long id;
private String name;
private String email;
}#2.2 三层架构的职责定义
| 层级 | 职责 | 不该做的 |
|---|---|---|
| Controller | 接收请求、参数校验、调用 Service、返回响应 | 业务逻辑 |
| Service | 事务管理、业务逻辑、组合多个 Repository 调用 | 直接操作数据库 |
| Repository | 单表 CRUD、数据库操作 | 业务逻辑 |
| Entity | 数据结构定义 | 业务逻辑 |
#2.3 依赖方向原则
Controller → Service → Repository → Database
↑ ↑ ↑
└──────────┴─────────┘
所有依赖都指向数据库方向
上层可以依赖下层,下层不能依赖上层依赖倒置原则:Service 依赖的是 Repository 接口,而不是具体实现。这使得 Service 不需要关心数据库的实现细节。
// ✅ 依赖接口
interface UserRepository {
User findById(Long id);
}
class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
// ❌ 依赖具体实现
class UserService {
private final JpaUserRepository userRepository; // 直接依赖实现类
}#三、分层的问题与演进🟡
#3.1 胖 Service 问题
传统三层架构最大的问题是"胖 Service":
// 一个类积累了 100 个方法
class OrderService {
public OrderDTO create(OrderDTO dto) { ... }
public void cancel(Long orderId) { ... }
public void refund(Long orderId) { ... }
public void pay(Long orderId) { ... }
public void deliver(Long orderId) { ... }
public void receive(Long orderId) { ... }
public OrderDetailDTO getDetail(Long orderId) { ... }
public List<OrderDTO> list(Long userId) { ... }
public Page<OrderDTO> page(PageRequest req) { ... }
// ... 一共 100+ 方法
}问题:
- 代码行数爆炸(单类 5000+ 行)
- 测试困难(类太大,mock 复杂)
- 代码冲突(多人修改同一文件)
#3.2 演进方案一:按领域拆分
// 订单领域服务
class OrderDomainService {
// 聚合根操作
public Order create(OrderDTO dto) { ... }
public void cancel(Long orderId) { ... }
public void pay(Long orderId) { ... }
}
// 订单查询服务(读优化)
class OrderQueryService {
public OrderDTO getDetail(Long orderId) { ... }
public Page<OrderDTO> list(Long userId) { ... }
}这就是 CQRS(命令查询职责分离) 的雏形——写操作和读操作分离。
#3.3 演进方案二:防腐层(ACL)
当你的系统需要调用外部系统时,加一层防腐层:
// ❌ 直接调用外部系统(污染核心逻辑)
class OrderService {
public void create(Order order) {
// 业务逻辑
// 直接调用外部系统 —— 如果外部 API 变了?
externalUserService.getUser(order.getUserId());
}
}
// ✅ 防腐层隔离
class OrderService {
private final UserFacade userFacade; // 内部接口
public void create(Order order) {
// 业务逻辑
userFacade.getUser(order.getUserId()); // 通过门面
}
}
// 防腐层实现
class UserFacadeImpl implements UserFacade {
private final ExternalUserAdapter externalAdapter; // 外部适配器
@Override
public UserDTO getUser(Long userId) {
try {
return externalAdapter.getUser(userId);
} catch (ExternalSystemException e) {
// 降级处理
return fallbackGetUser(userId);
}
}
}#四、四层架构与六边形架构🔴
#4.1 四层架构
┌──────────────────────────────────────┐
│ Controllers (API) │ 接口层:HTTP/RPC
├──────────────────────────────────────┤
│ Application (Use Case) │ 应用层:编排业务用例
├──────────────────────────────────────┤
│ Domain │ 领域层:核心业务逻辑
├──────────────────────────────────────┤
│ Infrastructure │ 基础设施层:持久化、消息、外部服务
└──────────────────────────────────────┘四层 vs 三层的区别:把业务逻辑再拆成"应用层"和"领域层"。
// 应用层:编排用例,不包含业务逻辑
class CreateOrderUseCase {
private final OrderDomainService orderDomain;
private final UserFacade userFacade;
private final EventPublisher eventPublisher;
public Order execute(CreateOrderCommand cmd) {
// 1. 校验用户
User user = userFacade.getUser(cmd.getUserId());
if (!user.isActive()) {
throw new UserNotActiveException();
}
// 2. 创建订单
Order order = orderDomain.create(cmd);
// 3. 发布领域事件
eventPublisher.publish(new OrderCreatedEvent(order));
return order;
}
}
// 领域层:核心业务逻辑
class OrderDomainService {
public Order create(CreateOrderCommand cmd) {
Order order = new Order();
order.setUserId(cmd.getUserId());
order.setAmount(cmd.getAmount());
// 领域规则:金额校验
if (order.getAmount() <= 0) {
throw new InvalidOrderAmountException();
}
return order;
}
}#4.2 六边形架构(端口与适配器)
六边形架构把系统想象成一个六边形,核心业务在中间,通过端口与外部交互:
┌─────────────────┐
│ │
┌────────▶│ Application │◀────────┐
│ │ Core │ │
│ │ │ │
┌─────────┐ └────────┬────────┘ ┌─────────┐
│ HTTP │─────────────────┼─────────────────│ MQ │
│ Adapter │ │ │ Adapter │
└─────────┘ │ └─────────┘
│
┌────────┴────────┐
│ │
│ Ports │
│ (输入/输出端口) │
└─────────────────┘核心特点:
- 领域核心独立:不依赖任何外部框架
- 端口定义接口:定义输入(用例)和输出(持久化、外部调用)
- 适配器实现端口:HTTP、数据库、消息队列都是适配器
【架构权衡】 六边形架构的代价是前期设计成本高(需要识别端口和适配器),收益是核心业务完全可测试、可替换。适合业务复杂、核心逻辑需要长期维护的系统。
#五、生产避坑清单
#5.1 跨层调用
// ❌ 错误:Controller 直接操作 Repository(绕过 Service)
class UserController {
@Autowired
private UserRepository userRepository; // 绕过了 Service
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id); // 绕过了业务逻辑和事务
}
}
// ✅ 正确:严格分层
class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
}#5.2 贫血 vs 充血模型
// ❌ 贫血模型:Service 承担所有业务逻辑
class OrderService {
public void pay(Order order, double amount) {
if (amount < order.getAmount()) { // 业务逻辑在 Service
throw new Exception("金额不足");
}
order.setStatus("PAID"); // Entity 只是数据容器
}
}
// ✅ 充血模型:业务逻辑在 Entity 中
class Order {
private OrderStatus status;
public void pay(double amount) {
if (amount < this.amount) { // 业务逻辑内聚在 Entity
throw new Exception("金额不足");
}
this.status = OrderStatus.PAID;
}
}
class OrderService {
public void pay(Long orderId, double amount) {
Order order = repository.findById(orderId);
order.pay(amount); // 调用 Entity 的方法
}
}💡
DDD 推荐充血模型,因为业务逻辑和数据内聚在一起,更容易维护。但充血模型要求 Entity 不能被框架直接持久化(需要通过仓储),实施成本较高。
#5.3 依赖倒置的陷阱
// ❌ 错误:Service 依赖具体实现
class OrderService {
@Autowired
private JpaOrderRepository repository; // 直接依赖实现类
}
// ✅ 正确:依赖接口
interface OrderRepository {
Order findById(Long id);
void save(Order order);
}
class OrderService {
@Autowired
private OrderRepository repository; // 依赖接口
}#六、团队协作视角
分层架构不只是技术问题,更是团队协作问题。
| 分层 | 团队边界 | 变更频率 | 质量要求 |
|---|---|---|---|
| Presentation | 前端/客户端 | 高 | 中 |
| Application | 业务/用例 | 高 | 高 |
| Domain | 核心规则 | 低 | 最高 |
| Infrastructure | 技术实现 | 中 | 中 |
康威定律:系统的架构反映了组织的沟通结构。如果两个团队分别负责用户域和订单域,就不应该让这两个域的代码混合在一个分层架构中。
#七、面试总结
#7.1 核心追问
- "三层架构的每一层职责是什么?" —— 基本概念题
- "胖 Service 问题怎么解决?" —— 按领域拆分、CQRS
- "分层架构和六边形架构的区别?" —— 依赖方向、核心隔离
- "什么时候不用分层架构?" —— 微服务拆分后、简单 CRUD 系统
#7.2 级别差异
| 级别 | 期望回答 |
|---|---|
| P5 | 能说清三层架构的职责划分 |
| P6 | 能分析胖 Service 的问题,知道 CQRS 思想 |
| P7 | 能设计四层/六边形架构,能从团队协作角度分析分层 |