1. 概述
当我们开发长期运行的系统时,必须预见到环境的变化。无论是功能需求、框架选型、I/O 设备,还是代码结构,都有可能因为各种原因发生变动。
在这种背景下,Clean Architecture(整洁架构)提供了一种高可维护性代码的设计指导原则,帮助我们应对各种不确定性。
本文将基于 Robert C. Martin(Uncle Bob)提出的 Clean Architecture 设计理念,构建一个用户注册接口的示例。我们将使用其原始的四层结构:实体(Entities)、用例(Use Cases)、接口适配器(Interface Adapters)、框架与驱动(Frameworks & Drivers)。
2. 整洁架构概览
整洁架构融合了多种设计原则,如 SOLID、稳定抽象原则 等。其核心思想是:根据业务价值将系统划分为不同层级,业务规则位于最高层,越往下越接近 I/O 设备。
这些层级可以映射为架构中的“层”,其中:
✅ 内层(Inner Layer) = 高层级(High Level) ✅ 外层(Outer Layer) = 低层级(Low Level)
⚠️ 注意:高层不能依赖低层,这就是著名的“依赖规则”(Dependency Rule)。
3. 系统规则定义
我们先来定义用户注册接口的系统规则。
3.1 业务规则(Business Rules)
- 用户密码长度必须大于 5 个字符
3.2 应用规则(Application Rules)
- 系统接收用户名和密码
- 验证用户是否已存在
- 若不存在,则保存用户信息及创建时间
✅ 注意:规则中没有涉及数据库、UI 等细节,因为业务不关心这些实现细节,代码也不应该关心。
4. 实体层(Entity Layer)
按照整洁架构的建议,我们从最核心的业务规则开始。
4.1 User 接口
interface User {
boolean passwordIsValid();
String getName();
String getPassword();
}
4.2 UserFactory 接口
interface UserFactory {
User create(String name, String password);
}
我们使用工厂方法的原因有两个:
✅ 遵循稳定抽象原则
✅ 将用户创建逻辑隔离
4.3 实现实体类
class CommonUser implements User {
String name;
String password;
@Override
public boolean passwordIsValid() {
return password != null && password.length() > 5;
}
// Constructor and getters
}
class CommonUserFactory implements UserFactory {
@Override
public User create(String name, String password) {
return new CommonUser(name, password);
}
}
✅ 如果业务逻辑复杂,我们应该尽量让领域代码清晰明了。这一层非常适合使用 领域驱动设计(DDD) 和 设计模式。
4.4 单元测试
@Test
void given123Password_whenPasswordIsNotValid_thenIsFalse() {
User user = new CommonUser("Baeldung", "123");
assertThat(user.passwordIsValid()).isFalse();
}
✅ 没有使用 Mock 是这一层的良好信号。如果在这里频繁使用 Mock,可能说明我们混淆了实体和用例的职责。
5. 用例层(Use Case Layer)
用例层封装了系统自动化的业务规则,也被称为 Interactors(交互器)。
5.1 UserRegisterInteractor
class UserRegisterInteractor implements UserInputBoundary {
final UserRegisterDsGateway userDsGateway;
final UserPresenter userPresenter;
final UserFactory userFactory;
// Constructor
@Override
public UserResponseModel create(UserRequestModel requestModel) {
if (userDsGateway.existsByName(requestModel.getName())) {
return userPresenter.prepareFailView("User already exists.");
}
User user = userFactory.create(requestModel.getName(), requestModel.getPassword());
if (!user.passwordIsValid()) {
return userPresenter.prepareFailView("User password must have more than 5 characters.");
}
LocalDateTime now = LocalDateTime.now();
UserDsRequestModel userDsModel = new UserDsRequestModel(user.getName(), user.getPassword(), now);
userDsGateway.save(userDsModel);
UserResponseModel accountResponseModel = new UserResponseModel(user.getName(), now.toString());
return userPresenter.prepareSuccessView(accountResponseModel);
}
}
✅ 这一层控制实体的“舞蹈”,但不假设 UI 或数据库的实现方式。
5.2 输入与输出边界(Boundaries)
输入边界(Input Boundary)
interface UserInputBoundary {
UserResponseModel create(UserRequestModel requestModel);
}
输出边界(Output Boundaries)
数据源网关
interface UserRegisterDsGateway {
boolean existsByName(String name);
void save(UserDsRequestModel requestModel);
}
视图呈现器
interface UserPresenter {
UserResponseModel prepareSuccessView(UserResponseModel user);
UserResponseModel prepareFailView(String error);
}
✅ 我们通过 依赖倒置原则(DIP) 来让业务逻辑摆脱数据库和 UI 的束缚。
5.3 解耦模式
我们可以选择以下解耦方式:
- 单体架构(Monolithic)
- 模块化(Modules)
- 微服务(Services/Microservices)
✅ 无论选择哪种方式,只要遵循边界划分,都可以实现整洁架构的目标。
5.4 请求与响应模型
class UserRequestModel {
String login;
String password;
// Getters, setters, and constructors
}
✅ 只有简单的数据结构可以跨越边界,并且这些模型只包含字段和访问器。
5.5 测试 UserRegisterInteractor
@Test
void givenBaeldungUserAnd123456Password_whenCreate_thenSaveItAndPrepareSuccessView() {
User user = new CommonUser("baeldung", "123456");
UserRequestModel userRequestModel = new UserRequestModel(user.getName(), user.getPassword());
when(userFactory.create(anyString(), anyString()))
.thenReturn(new CommonUser(user.getName(), user.getPassword()));
interactor.create(userRequestModel);
verify(userDsGateway, times(1)).save(any(UserDsRequestModel.class));
verify(userPresenter, times(1)).prepareSuccessView(any(UserResponseModel.class));
}
✅ 测试重点是控制实体和边界之间的交互。
6. 接口适配器层(Interface Adapters)
这一层负责将数据在不同格式之间转换。
6.1 使用 JPA 实现 UserRegisterDsGateway
@Entity
@Table(name = "user")
class UserDataMapper {
@Id
String name;
String password;
LocalDateTime creationTime;
//Getters, setters, and constructors
}
@Repository
interface JpaUserRepository extends JpaRepository<UserDataMapper, String> {
}
class JpaUser implements UserRegisterDsGateway {
final JpaUserRepository repository;
// Constructor
@Override
public boolean existsByName(String name) {
return repository.existsById(name);
}
@Override
public void save(UserDsRequestModel requestModel) {
UserDataMapper accountDataMapper = new UserDataMapper(requestModel.getName(), requestModel.getPassword(), requestModel.getCreationTime());
repository.save(accountDataMapper);
}
}
⚠️ 注意命名:
UserRegisterDsGateway
而不是UserDsGateway
,避免违反 接口隔离原则。
6.2 用户注册接口
@RestController
class UserRegisterController {
final UserInputBoundary userInput;
// Constructor
@PostMapping("/user")
UserResponseModel create(@RequestBody UserRequestModel requestModel) {
return userInput.create(requestModel);
}
}
✅ 控制器的唯一职责是接收请求、返回响应。
6.3 响应格式化
class UserResponseFormatter implements UserPresenter {
@Override
public UserResponseModel prepareSuccessView(UserResponseModel response) {
LocalDateTime responseTime = LocalDateTime.parse(response.getCreationTime());
response.setCreationTime(responseTime.format(DateTimeFormatter.ofPattern("hh:mm:ss")));
return response;
}
@Override
public UserResponseModel prepareFailView(String error) {
throw new ResponseStatusException(HttpStatus.CONFLICT, error);
}
}
✅ 将难测的部分划分为 Humble Object,便于测试。
@Test
void givenDateAnd3HourTime_whenPrepareSuccessView_thenReturnOnly3HourTime() {
UserResponseModel modelResponse = new UserResponseModel("baeldung", "2020-12-20T03:00:00.000");
UserResponseModel formattedResponse = userResponseFormatter.prepareSuccessView(modelResponse);
assertThat(formattedResponse.getCreationTime()).isEqualTo("03:00:00");
}
7. 驱动与框架层(Drivers & Frameworks)
这一层通常不写业务代码,只使用外部库和框架。
我们使用 Spring Boot 作为 Web 框架和依赖注入容器:
@SpringBootApplication
public class CleanArchitectureApplication {
public static void main(String[] args) {
SpringApplication.run(CleanArchitectureApplication.class);
}
}
✅ 除了适配器类,业务层不使用任何 Spring 注解,因为 Spring 是一个“细节”。
8. 主类(Main Class)
@Bean
BeanFactoryPostProcessor beanFactoryPostProcessor(ApplicationContext beanRegistry) {
return beanFactory -> {
genericApplicationContext(
(BeanDefinitionRegistry) ((AnnotationConfigServletWebServerApplicationContext) beanRegistry)
.getBeanFactory());
};
}
void genericApplicationContext(BeanDefinitionRegistry beanRegistry) {
ClassPathBeanDefinitionScanner beanDefinitionScanner = new ClassPathBeanDefinitionScanner(beanRegistry);
beanDefinitionScanner.addIncludeFilter(removeModelAndEntitiesFilter());
beanDefinitionScanner.scan("com.baeldung.pattern.cleanarchitecture");
}
static TypeFilter removeModelAndEntitiesFilter() {
return (MetadataReader mr, MetadataReaderFactory mrf) -> !mr.getClassMetadata()
.getClassName()
.endsWith("Model");
}
✅ 我们通过 Spring 的依赖注入来创建实例,但不使用 @Component
扫描,而是手动控制注入。
9. 总结
本文通过用户注册接口的示例,展示了如何在 Spring Boot 中实践整洁架构。
✅ 我们遵循了依赖规则、接口隔离原则、依赖倒置原则等设计思想。
正如 Uncle Bob 所说:
“一个好的架构师应该尽可能推迟决策。”
我们通过边界隔离业务与细节,正是实现这一目标的关键。
完整代码见:GitHub