Appearance
整洁架构
要理解整洁架构(Clean Architecture),我们需要先回到它的设计目标:让软件系统的「业务核心」与「外部依赖」彻底解耦,从而实现可维护、可扩展、可测试的终极目标。它由Robert C. Martin( Uncle Bob)在2012年提出,本质是对「依赖倒置原则(DIP)」「关注点分离(SoC)」等经典设计原则的系统性落地。
一、整洁架构的核心原则
整洁架构的所有规则都围绕一个黄金法则展开:
内层不依赖外层,所有依赖都指向内部(依赖方向只能从「具体实现」指向「抽象核心」)。
换句话说:
- 业务核心(比如「用户必须有有效邮箱」的规则)不应该知道任何外部系统的存在(比如数据库是MySQL还是PostgreSQL,前端是React还是Vue);
- 外部系统(比如框架、数据库、UI)必须依赖业务核心的抽象接口,而非具体实现。
二、整洁架构的四层结构
整洁架构用同心圆分层来划分系统职责,从内到外依次是:
实体(Entities)→ 用例(Use Cases)→ 接口适配器(Interface Adapters)→ 外部系统(Frameworks & Drivers)
每层都有明确的职责,且仅依赖内层(或同层的抽象)。
1. 最内层:实体(Entities)—— 业务的「原子规则」
职责:封装跨用例的核心业务规则(即「什么是对的」),是系统中最稳定、最不应该变化的部分。
- 实体可以是领域模型(比如
User、Order),包含属性(如userId、email)和业务行为(如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依赖用例)。
三、示意图

四、整洁架构的实现细节:输入/输出端口的设计
用例层是整洁架构的「业务流程中枢」,它通过**输入端口(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)); - 避免暴露外部系统的细节(比如不要返回
ResultSet或MyBatisMapper)。
例子(输出端口的单元测试):
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(),而是把验证逻辑放到UserService或Controller里)。
为什么错:实体是业务规则的载体,贫血模型会导致业务逻辑分散到外层(适配器或用例),违反「业务核心集中」的原则。
正确做法:把所有跨用例的业务规则放到实体里(比如邮箱验证、密码加密、订单状态转换),用例只负责流程编排。
误区2:业务逻辑泄漏到适配器层
错误表现:在Controller或Repository里做业务逻辑(比如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),将实体转换为外层需要的格式(比如隐藏密码哈希); - 适配器的转换逻辑:在
Controller或Repository中,将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代替手动依赖注入)。
希望这两部分内容能帮你真正理解并落地整洁架构!如果有具体的项目场景或问题,欢迎随时提问。
