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)

user clean architecture layers 1

⚠️ 注意:高层不能依赖低层,这就是著名的“依赖规则”(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


原始标题:Clean Architecture with Spring Boot | Baeldung