Skip to content

整洁架构

要理解整洁架构(Clean Architecture),我们需要先回到它的设计目标:让软件系统的「业务核心」与「外部依赖」彻底解耦,从而实现可维护、可扩展、可测试的终极目标。它由Robert C. Martin( Uncle Bob)在2012年提出,本质是对「依赖倒置原则(DIP)」「关注点分离(SoC)」等经典设计原则的系统性落地。

一、整洁架构的核心原则

整洁架构的所有规则都围绕一个黄金法则展开:

内层不依赖外层,所有依赖都指向内部(依赖方向只能从「具体实现」指向「抽象核心」)。

换句话说:

  • 业务核心(比如「用户必须有有效邮箱」的规则)不应该知道任何外部系统的存在(比如数据库是MySQL还是PostgreSQL,前端是React还是Vue);
  • 外部系统(比如框架、数据库、UI)必须依赖业务核心的抽象接口,而非具体实现。

二、整洁架构的四层结构

整洁架构用同心圆分层来划分系统职责,从内到外依次是:
实体(Entities)→ 用例(Use Cases)→ 接口适配器(Interface Adapters)→ 外部系统(Frameworks & Drivers)
每层都有明确的职责,且仅依赖内层(或同层的抽象)。

1. 最内层:实体(Entities)—— 业务的「原子规则」

职责:封装跨用例的核心业务规则(即「什么是对的」),是系统中最稳定、最不应该变化的部分。

  • 实体可以是领域模型(比如UserOrder),包含属性(如userIdemail)和业务行为(如User.validateEmail()验证邮箱格式、Order.calculateTotal()计算订单总价);
  • 实体不依赖任何外部代码(甚至不依赖框架注解,比如@Entity),它是「纯业务逻辑」的载体。

例子

java
// 实体:User(不依赖任何外部库)
public class User {
    private String userId;
    private String email;
    private String password;

    // 业务行为:验证邮箱格式(核心规则)
    public void setEmail(String email) {
        if (!email.matches("^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$")) {
            throw new IllegalArgumentException("无效的邮箱格式");
        }
        this.email = email;
    }

    // 业务行为:加密密码(核心规则)
    public void setPassword(String rawPassword) {
        this.password = hash(rawPassword); // 哈希逻辑是实体的一部分
    }
}

2. 第二层:用例(Use Cases)—— 业务的「流程规则」

职责:封装具体业务流程(即「如何做一件事」),是实体的「使用者」。

  • 用例对应「用户故事」或「业务场景」(比如「用户注册」「订单支付」「发送验证码」);
  • 用例依赖实体(调用实体的业务行为),但不依赖任何外部系统(比如数据库、API);
  • 用例通过输入端口(Input Port)接收外部请求,通过输出端口(Output Port)调用外部依赖(但输出端口是抽象接口,具体实现由外层提供)。

关键:用例只关心「业务流程的逻辑」,不关心「流程中的细节如何实现」(比如「保存用户」的操作由外层的数据库适配器实现,用例只调用UserRepository接口)。

例子

java
// 用例输入端口:定义用例需要的参数(注册请求)
public record RegisterUserRequest(String email, String password) {}

// 用例输出端口:定义用例需要的外部依赖(抽象接口)
public interface UserRepository {
    void save(User user); // 保存用户(具体实现由数据库适配器提供)
}

// 用例:注册用户(核心业务流程)
public class RegisterUserUseCase {
    private final UserRepository userRepository; // 依赖抽象接口

    public RegisterUserUseCase(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 执行用例
    public void execute(RegisterUserRequest request) {
        // 1. 校验参数(用例逻辑)
        if (request.email() == null || request.password() == null) {
            throw new IllegalArgumentException("邮箱或密码不能为空");
        }

        // 2. 创建实体(调用实体的业务行为)
        User user = new User();
        user.setEmail(request.email()); // 触发实体的邮箱验证
        user.setPassword(request.password()); // 触发实体的密码加密

        // 3. 保存用户(调用输出端口的抽象方法)
        userRepository.save(user);
    }
}

3. 第三层:接口适配器(Interface Adapters)—— 「翻译层」

职责:将内层的「业务模型」与外层的「外部系统模型」互相转换(即「翻译」),同时实现用例的输出端口

  • 适配器的作用是「隔离差异」:比如将User实体转换成数据库的user表结构(ORM适配器),或转换成API的JSON响应(REST适配器);
  • 适配器依赖用例实体,但不依赖外层的具体框架(比如不直接使用JdbcTemplate,而是通过接口封装);
  • 常见的适配器类型:
    • 数据库适配器:实现UserRepository接口,用JDBC/MyBatis/Hibernate操作数据库;
    • API适配器:将HTTP请求转换为用例的RegisterUserRequest(比如Spring的@RestController);
    • 缓存适配器:实现CacheRepository接口,用Redis存储缓存。

例子(数据库适配器):

java
// 数据库适配器:实现UserRepository接口(用MyBatis操作数据库)
@Repository // 框架注解仅存在于适配器层
public class MyBatisUserRepository implements UserRepository {
    private final UserMapper userMapper; // MyBatis的Mapper接口

    @Autowired
    public MyBatisUserRepository(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public void save(User user) {
        // 将User实体转换为MyBatis的UserDO(数据对象)
        UserDO userDO = new UserDO();
        userDO.setUserId(user.getUserId());
        userDO.setEmail(user.getEmail());
        userDO.setPassword(user.getPassword());

        // 调用MyBatis的Mapper保存
        userMapper.insert(userDO);
    }
}

例子(API适配器):

java
// API适配器:将HTTP请求转换为用例调用(Spring Controller)
@RestController
@RequestMapping("/api/users")
public class UserController {
    private final RegisterUserUseCase registerUserUseCase; // 依赖用例

    @Autowired
    public UserController(RegisterUserUseCase registerUserUseCase) {
        this.registerUserUseCase = registerUserUseCase;
    }

    @PostMapping("/register")
    public ResponseEntity<Void> register(@RequestBody RegisterUserRequest request) {
        // 调用用例执行注册流程
        registerUserUseCase.execute(request);
        return ResponseEntity.ok().build();
    }
}

4. 最外层:外部系统(Frameworks & Drivers)—— 「具体实现」

职责:包含所有具体的外部依赖,是系统中最容易变化的部分。

  • 内容:框架(Spring、React)、数据库(MySQL、MongoDB)、中间件(Redis、MQ)、UI(网页、App)等;
  • 特点:这些组件不定义任何业务逻辑,仅通过调用内层的适配器来工作;
  • 依赖方向:外部系统依赖接口适配器(比如Spring的@Autowired注入UserController,而UserController依赖用例)。

三、示意图

整洁架构示意图.png

四、整洁架构的实现细节:输入/输出端口的设计

用例层是整洁架构的「业务流程中枢」,它通过**输入端口(Input Port)接收外部请求,通过输出端口(Output Port)**调用外部依赖。这两个端口的设计直接决定了系统的解耦程度。

1. 输入端口:用例的「入口」

输入端口是用例的对外接口,定义了用例的「执行方式」(需要什么参数,返回什么结果)。它的作用是:

  • 隔离外部请求的格式(比如HTTP参数、CLI命令)与用例的核心逻辑;
  • 让用例可以被多种外部系统调用(比如同时支持REST API、RPC、定时任务)。

设计原则

  • 输入端口是抽象接口(或函数式接口),用例本身是接口的实现;
  • 输入参数用简单POJO/Record(比如RegisterUserRequest),避免依赖外部框架的对象(比如Spring的HttpServletRequest)。

例子(输入端口的标准设计):

java
// 输入端口:定义用例的执行契约
public interface RegisterUserInputPort {
    void execute(RegisterUserRequest request);
}

// 用例:实现输入端口(核心流程不变)
public class RegisterUserUseCase implements RegisterUserInputPort {
    private final UserRepository userRepository;

    public RegisterUserUseCase(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public void execute(RegisterUserRequest request) {
        // 业务流程(同前)
    }
}

// API适配器:调用输入端口(Spring Controller)
@RestController
public class UserController {
    private final RegisterUserInputPort registerUserInputPort; // 依赖抽象接口

    @Autowired
    public UserController(RegisterUserInputPort registerUserInputPort) {
        this.registerUserInputPort = registerUserInputPort;
    }

    @PostMapping("/register")
    public ResponseEntity<Void> register(@RequestBody RegisterUserRequest request) {
        registerUserInputPort.execute(request); // 调用输入端口
        return ResponseEntity.ok().build();
    }
}

2. 输出端口:用例的「依赖抽象」

输出端口是用例依赖的外部服务的抽象(比如「保存用户」「发送短信」),它的作用是:

  • 让用例不依赖具体的外部系统(比如不用关心是MySQL还是MongoDB);
  • 方便Mock外部依赖,快速编写单元测试。

设计原则

  • 输出端口是抽象接口,由适配器层实现;
  • 接口方法的命名要面向业务(比如saveUser(User user)而不是insertUserIntoDB(User user));
  • 避免暴露外部系统的细节(比如不要返回ResultSetMyBatisMapper)。

例子(输出端口的单元测试):

java
// 单元测试:测试用例的核心逻辑(Mock输出端口)
@ExtendWith(MockitoExtension.class)
public class RegisterUserUseCaseTest {
    @Mock
    private UserRepository userRepository; // Mock输出端口

    @InjectMocks
    private RegisterUserUseCase registerUserUseCase; // 注入用例

    @Test
    public void should_register_user_successfully() {
        // 1. 准备请求参数
        RegisterUserRequest request = new RegisterUserRequest("test@example.com", "password123");

        // 2. 执行用例
        registerUserUseCase.execute(request);

        // 3. 验证行为:userRepository.save被调用一次
        ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
        Mockito.verify(userRepository, Mockito.times(1)).save(userCaptor.capture());

        // 4. 验证实体的业务规则被执行(邮箱格式正确、密码已加密)
        User savedUser = userCaptor.getValue();
        assertEquals("test@example.com", savedUser.getEmail());
        assertNotEquals("password123", savedUser.getPassword()); // 密码已哈希
    }
}

五、整洁架构的常见误区:别让「整洁」变「混乱」

很多团队落地整洁架构时会踩坑,核心原因是违反了「依赖指向内部」的黄金法则。以下是最常见的4个误区:

误区1:实体变成「贫血模型」(Anemic Domain Model)

错误表现:实体只有getter/setter,没有任何业务行为(比如User类里没有validateEmail(),而是把验证逻辑放到UserServiceController里)。
为什么错:实体是业务规则的载体,贫血模型会导致业务逻辑分散到外层(适配器或用例),违反「业务核心集中」的原则。
正确做法:把所有跨用例的业务规则放到实体里(比如邮箱验证、密码加密、订单状态转换),用例只负责流程编排。

误区2:业务逻辑泄漏到适配器层

错误表现:在ControllerRepository里做业务逻辑(比如UserController里验证邮箱格式,MyBatisUserRepository里计算订单总价)。
为什么错:适配器的职责是「翻译」,不是「业务决策」。如果业务逻辑放到适配器,替换外部系统时会导致逻辑重复或丢失。
正确做法:适配器只做「格式转换」(比如HTTP参数→用例请求、实体→数据库DO),所有业务逻辑必须放在用例或实体里。

误区3:依赖方向搞反(内层依赖外层)

错误表现:用例层依赖具体的数据库实现(比如RegisterUserUseCase里直接注入MyBatisUserRepository,而不是UserRepository接口)。
为什么错:这会让用例层耦合到具体的外部系统,无法独立测试或替换数据库。
正确做法:所有内层(实体、用例)只能依赖抽象接口,具体实现由外层(适配器)提供(依赖注入)。

误区4:用例粒度太粗或太细

错误表现

  • 太粗:一个用例处理多个业务场景(比如UserUseCase包含「注册」「登录」「修改密码」所有逻辑);
  • 太细:把一个业务流程拆成多个用例(比如「注册用户」拆成「验证邮箱用例」「加密密码用例」「保存用户用例」)。
    为什么错
  • 太粗:违反单一职责原则,难以维护和测试;
  • 太细:增加不必要的复杂度,用例变成「函数的包装」,失去「业务流程」的意义。
    正确做法:一个用例对应一个完整的业务场景(比如「用户注册」「订单支付」),粒度与「用户故事」对齐。

六、整洁架构与其他架构的对比:本质是同一思想的不同表述

整洁架构不是「全新发明」,而是对经典架构思想的系统化总结。以下是它与其他常见架构的关系:

1. 与「六边形架构(Hexagonal Architecture)」的关系

六边形架构(又称「端口适配器架构」)由Alistair Cockburn提出,核心思想与整洁架构完全一致

  • 六边形的「内部」是业务核心(实体+用例);
  • 六边形的「端口」是输入/输出接口(对应整洁架构的输入/输出端口);
  • 六边形的「适配器」是外部系统的实现(对应整洁架构的接口适配器层)。

区别:六边形架构用「六边形」比喻(强调业务核心的「通用性」),整洁架构用「同心圆」比喻(强调「依赖方向」),本质是同一思想的不同可视化表达。

2. 与「传统三层架构」的区别

传统三层架构(表现层→业务层→数据层)是「依赖从外到内」的:

  • 表现层依赖业务层,业务层依赖数据层;
  • 数据层的变化会影响业务层(比如换数据库需要改业务层代码)。

整洁架构是「依赖反转」的:

  • 表现层(适配器)依赖业务层(用例),业务层依赖数据层的抽象接口
  • 数据层的变化(比如换数据库)只需要改适配器,不影响业务层。

总结:传统三层是「紧耦合的分层」,整洁架构是「松耦合的分层」(通过依赖反转实现)。

3. 与「领域驱动设计(DDD)」的关系

DDD是「业务分析方法论」,整洁架构是「系统设计方法论」,两者是互补的:

  • DDD帮你「理清业务」(比如通过聚合根、领域服务定义实体和用例);
  • 整洁架构帮你「落地业务」(通过分层和依赖反转保护DDD的成果)。

比如:

  • DDD的「聚合根」对应整洁架构的「实体」;
  • DDD的「领域服务」对应整洁架构的「用例」;
  • DDD的「基础设施层」对应整洁架构的「接口适配器+外部系统」。

七、整洁架构的落地技巧:从0到1搭建项目

讲了这么多理论,最后我们用Spring Boot项目为例,说明如何实际落地整洁架构。

1. 项目模块划分(按层次拆分)

将项目拆分为4个模块,严格遵循「内层不依赖外层」的原则:

模块名称职责依赖关系
domain实体(Entities)+ 用例(Use Cases)+ 端口(Ports)无依赖(纯Java)
adapter接口适配器(数据库、API、缓存等)依赖domain
application启动类(Spring Boot Application)+ 配置依赖domain+adapter
common通用工具类(如哈希工具、异常类)无依赖

2. 模块内的包结构(以domain为例)

domain/
├── entity/          # 实体(User、Order等)
│   └── User.java
├── usecase/         # 用例(注册、支付等)
│   ├── RegisterUserUseCase.java
│   └── request/     # 用例输入参数(RegisterUserRequest.java)
└── port/            # 端口(输入+输出)
    ├── input/       # 输入端口(RegisterUserInputPort.java)
    └── output/      # 输出端口(UserRepository.java)

3. 依赖注入的实现(Spring Boot)

  • domain模块中,用例和端口是纯Java类/接口(不依赖Spring注解);
  • adapter模块中,适配器类用@Repository/@RestController注解,注入用例或端口的实现;
  • application模块中,用@SpringBootApplication扫描adapter模块的Bean,通过@Autowired注入到启动类。

例子application模块的启动类):

java
@SpringBootApplication(scanBasePackages = "com.example.adapter") // 扫描适配器的Bean
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

4. 跨层数据传递:避免实体泄漏到外层

  • 用例的输入参数:用简单POJO/Record(比如RegisterUserRequest),避免传递实体;
  • 用例的输出结果:用DTO(数据传输对象,比如UserDTO),将实体转换为外层需要的格式(比如隐藏密码哈希);
  • 适配器的转换逻辑:在ControllerRepository中,将DTO/DO转换为用例的Request/实体。

例子(用例输出结果的转换):

java
// 用例输出结果:UserDTO(隐藏敏感信息)
public record UserDTO(String userId, String email) {}

// 用例:返回UserDTO
public class GetUserByIdUseCase implements GetUserByIdInputPort {
    private final UserRepository userRepository;

    public GetUserByIdUseCase(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDTO execute(String userId) {
        User user = userRepository.findById(userId);
        return new UserDTO(user.getUserId(), user.getEmail()); // 转换为DTO
    }
}

// API适配器:返回DTO的JSON
@RestController
public class UserController {
    private final GetUserByIdInputPort getUserByIdInputPort;

    @Autowired
    public UserController(GetUserByIdInputPort getUserByIdInputPort) {
        this.getUserByIdInputPort = getUserByIdInputPort;
    }

    @GetMapping("/users/{id}")
    public ResponseEntity<UserDTO> getUserById(@PathVariable String id) {
        UserDTO userDTO = getUserByIdInputPort.execute(id);
        return ResponseEntity.ok(userDTO);
    }
}

八、最后总结:整洁架构的「本质」

整洁架构不是一套「必须严格遵守的代码模板」,而是一种**「保护业务核心」的设计哲学**。它的所有规则都是为了实现一个目标:

让业务逻辑不依赖任何容易变化的东西(框架、数据库、UI),只依赖它自己。

对于小项目(比如Demo或一次性工具),整洁架构可能显得「过度设计」;但对于中大型业务系统(尤其是需要长期维护、频繁迭代的系统),整洁架构能帮你:

  • 快速响应需求变化(比如换前端框架只需改API适配器);
  • 轻松编写单元测试(不用启动数据库或服务器);
  • 降低新人的学习成本(层次清晰,业务核心一目了然)。

最后的建议

  • 不要一开始就追求「完美的整洁架构」,可以从核心用例开始落地(比如先实现「用户注册」的整洁结构,再扩展其他功能);
  • 定期Review代码,确保没有「依赖方向反转」或「业务逻辑泄漏」的问题;
  • 结合团队的技术栈(比如Spring Boot),调整适配器层的实现(比如用@Autowired代替手动依赖注入)。

希望这两部分内容能帮你真正理解并落地整洁架构!如果有具体的项目场景或问题,欢迎随时提问。