5A Philosophy of Software Design

分层与泄漏

抽象层

抽象层泄漏抽象层间通信跨越调用

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第5章:分层与泄漏(Layers and Leakage)

一、导读

1.1 本章学习目标

第5章讲解分层设计中的问题——抽象泄漏和层间通信。

本章的核心学习目标包括:

  • 理解抽象泄漏的概念
  • 掌握层间通信的规则
  • 学会避免跨越调用
  • 理解分层的实践策略

1.2 为什么本章如此重要

分层是信息隐藏的主要手段,但分层设计容易出现问题——下层细节泄漏到上层,或者上层绕过中间层直接调用下层。

1.3 核心问题引导

什么是抽象泄漏?为什么它有害?

层间通信应该遵循什么规则?

什么是跨越调用?如何避免?

分层应该分几层?


二、核心概念详解

2.1 抽象泄漏

抽象泄漏(Leaky Abstraction):
抽象的实现细节泄漏到了接口中

例:ORM 框架
// 理论上,ORM 隐藏了 SQL
User user = session.get(User.class, id);

// 但实际上,SQL 的细节泄漏了:
// - N+1 查询问题
// - 懒加载 vs 急加载
// - 缓存失效
// 用户仍然需要理解 SQL 才能高效使用

抽象泄漏不可避免,但可以最小化:
1. 选择好的抽象
2. 提供逃生舱口(escape hatch)
3. 文档化泄漏的部分

2.2 层间通信规则

规则1:只能调用相邻层
    应用层
      ↓
    服务层
      ↓
    数据层

    应用层不能直接调用数据层

规则2:上层调用下层,下层不直接调用上层
    上层 → 下层(正常调用)
    下层 → 上层(通过回调/事件)

规则3:每层只暴露必要的接口
    每层为上层提供清晰的接口
    隐藏内部实现

2.3 跨越调用

跨越调用(Pass-Through Method):
方法只是调用另一个方法,不做任何事

// 跨越调用
public class UserService {
    private UserRepository repo;
    
    public User findById(long id) {
        return repo.findById(id);    // 什么都没做
    }
}

跨越调用的问题:
1. 增加了一层但没有增加价值
2. 增加认知负荷
3. 增加维护成本

解决方案:
1. 删除跨越层
2. 或者让这一层做一些有意义的事

// 有意义的层
public class UserService {
    public User findById(long id) {
        User user = repo.findById(id);
        if (user == null) throw new NotFoundException();
        auditLogger.logAccess(user);    // 增加了审计功能
        return user;
    }
}

2.4 分层策略

分多少层?

太少:
- 一层做太多事
- 难以理解和修改

太多:
- 跨越调用
- 性能开销
- 复杂性增加

经验法则:
- 3-5 层通常足够
- 每层应该有明确的职责
- 每层应该提供有意义的抽象

常见分层:
1. 接口层 → 应用层 → 领域层 → 基础设施层(DDD)
2. 表示层 → 业务层 → 数据层(三层架构)
3. 控制器 → 服务 → 仓储 → 数据库

三、实战案例

3.1 案例一:修复分层问题

java
// 问题:跨越调用 + 抽象泄漏
public class OrderController {
    private JdbcTemplate jdbc;    // 泄漏了 JDBC
    
    @GetMapping("/orders/{id}")
    public Order getOrder(@PathVariable long id) {
        return jdbc.queryForObject(    // 跨越了服务层
            "SELECT * FROM orders WHERE id = ?",
            new Object[]{id},
            new OrderMapper()
        );
    }
}

// 修复:正确的分层
public class OrderController {
    private OrderService orderService;    // 只依赖服务层
    
    @GetMapping("/orders/{id}")
    public OrderDTO getOrder(@PathVariable long id) {
        Order order = orderService.findById(id);    // 调用服务层
        return OrderDTO.from(order);
    }
}

public class OrderService {
    private OrderRepository repository;    // 只依赖仓储接口
    
    public Order findById(OrderId id) {
        Order order = repository.findById(id);
        if (order == null) throw new NotFoundException();
        return order;
    }
}

public class OrderRepository {
    // 隐藏了 JDBC 实现细节
}

四、本章小结

抽象泄漏:实现细节泄漏到接口中,不可避免但可最小化。

层间通信:只调用相邻层,上层调用下层。

跨越调用:方法只转发调用,应删除或增加价值。

分层策略:3-5 层,每层有明确职责。


五、思考与练习

在你的代码中找出一个抽象泄漏的例子。

如何消除跨越调用?

你的项目分了几层?是否合理?


5/7分层与泄
0%