分层架构

一次代码审查的争论

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+ 方法
}

问题

  1. 代码行数爆炸(单类 5000+ 行)
  2. 测试困难(类太大,mock 复杂)
  3. 代码冲突(多人修改同一文件)

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 核心追问

  1. "三层架构的每一层职责是什么?" —— 基本概念题
  2. "胖 Service 问题怎么解决?" —— 按领域拆分、CQRS
  3. "分层架构和六边形架构的区别?" —— 依赖方向、核心隔离
  4. "什么时候不用分层架构?" —— 微服务拆分后、简单 CRUD 系统

7.2 级别差异

级别期望回答
P5能说清三层架构的职责划分
P6能分析胖 Service 的问题,知道 CQRS 思想
P7能设计四层/六边形架构,能从团队协作角度分析分层