Clean Architecture with Spring Boot – 使用Spring Boot的简洁架构

最后修改: 2021年 1月 19日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

When we’re developing long-term systems, we should expect a mutable environment.

当我们在开发长期系统时,我们应该期待一个易变的环境。

In general, our functional requirements, frameworks, I/O devices, and even our code design may all change for various reasons. With this in mind, the Clean Architecture is a guideline to a high maintainable code, considering all the uncertainties around us.

一般来说,我们的功能需求、框架、I/O设备,甚至我们的代码设计都可能因为各种原因而改变。考虑到这一点,清洁架构是一个高可维护代码的准则,考虑到我们周围的所有不确定性

In this article, we’ll create an example of a user registration API  following Robert C. Martin’s Clean Architecture. We’ll use his original layers  – entities, use cases, interface adapters, and frameworks/drivers.

在本文中,我们将按照Robert C. Martin的清洁架构创建一个用户注册API的示例。我们将使用他的原始层–实体、用例、接口适配器和框架/驱动程序。

2. Clean Architecture Overview

2.洁净结构概述

The clean architecture compiles many code designs and principles, like SOLID, stable abstractions, and others. But, the core idea is to divide the system into levels based on the business value. Hence, the highest level has business rules, with each lower one getting closer to the I/O devices.

简洁的架构汇编了许多代码设计和原则,如SOLID稳定的抽象,以及其他。但是,核心思想是根据业务价值将系统划分为几个层次。因此,最高级别有业务规则,每一个较低的级别越来越接近I/O设备。

Also, we can translate the levels into layers. In this case, it is the opposite. The inner layer is equal to the highest level, and so on:

此外,我们还可以将层次转化为图层。在这种情况下,它是相反的。内层等于最高层,以此类推。

With this in mind, we can have as many levels as our business requires. But, always considering the dependency rule – a higher level must never depend on a lower one.

考虑到这一点,我们可以根据我们的业务需要拥有尽可能多的级别。但是,要始终考虑到依赖性规则–较高层次决不能依赖较低层次

3. The Rules

3.规则

Lets’s start defining the system rules for our user registration API. First, business rules:

让我们开始为我们的用户注册API定义系统规则。首先,业务规则。

  • The user’s password must have more than five characters

Second, we have the application rules. They can be in different formats, as use cases or stories. We’ll use a storytelling phrase:

第二,我们有应用规则。它们可以是不同的格式,如用例或故事。我们将使用一个讲故事的短语。

  • The system receives the user name and password, validates if the user doesn’t exist, and saves the new user along with the creation time

Notice how there is no mention of any database, UI, or similar. Because our business doesn’t care about these details, neither should our code.

注意到没有提到任何数据库、用户界面或类似的东西。因为我们的业务并不关心这些细节,我们的代码也不应该关心。

4. The Entity Layer

4.实体层

As the clean architecture suggests, let’s start with our business rule:

正如干净的架构所建议的,让我们从我们的业务规则开始。

interface User {
    boolean passwordIsValid();

    String getName();

    String getPassword();
}

And, a UserFactory:

还有,一个UserFactory

interface UserFactory {
    User create(String name, String password);
}

We created a user factory method because of two reasons. To stock to the stable abstractions principle and to isolate the user creation.

我们创建用户工厂法因为有两个原因。为了向稳定的抽象原则和隔离用户创建。

Next, let’s implement both:

接下来,让我们来实现这两点。

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);
    }
}

If we have a complex business, then we should build our domain code as clear as possible. So, this layer is a great place to apply design patterns. Particularly, the domain-driven design should be taken into account.

如果我们有一个复杂的业务,那么我们应该尽可能清晰地构建我们的领域代码。因此,这一层是应用设计模式的好地方。特别是,应该考虑到域驱动设计

4.1. Unit Testing

4.1.单元测试

Now, let’s test our CommonUser:

现在,让我们测试一下我们的CommonUser

@Test
void given123Password_whenPasswordIsNotValid_thenIsFalse() {
    User user = new CommonUser("Baeldung", "123");

    assertThat(user.passwordIsValid()).isFalse();
}

As we can see, the unit tests are very clear. After all, the absence of mocks is a good signal for this layer.

我们可以看到,单元测试是非常清晰的。毕竟,没有模拟是这一层的一个好信号

In general, if we start thinking about mocks here, maybe we’re mixing our entities with our use cases.

一般来说,如果我们在这里开始考虑模拟,也许我们就会把我们的实体和我们的用例混在一起。

5. The Use Case Layer

5.用例层

The use cases are the rules related to the automatization of our system. In Clean Architecture, we call them Interactors.

用例是与我们系统的自动化有关的规则。在Clean Architecture中,我们称它们为交互者。

5.1. UserRegisterInteractor

5.1. UserRegisterInteractor

First, we’ll build our UserRegisterInteractor so we can see where we’re going. Then, we’ll create and discuss all used parts:

首先,我们将建立我们的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);
    }
}

As we can see, we’re doing all the use case steps. Also, this layer is responsible for controlling the entity’s dance. Still, we’re not making any assumptions on how the UI or database works. But, we’re using the UserDsGateway and UserPresenter. So, how can we not know them? Because, along with the UserInputBoundary, these are our input and output boundaries.

正如我们所见,我们正在做所有的用例步骤。另外,这一层负责控制实体的舞蹈。尽管如此,我们还是不对UI或数据库的工作方式做出任何假设。但是,我们正在使用UserDsGatewayUserPresenter。那么,我们怎么可能不知道它们呢?因为,与UserInputBoundary一起,这些是我们的输入和输出边界。

5.2. Input and Output Boundaries

5.2.输入和输出的界限

The boundaries are contracts defining how components can interact. The input boundary exposes our use case to outer layers:

边界是定义组件如何互动的契约。输入边界将我们的用例暴露给外层:

interface UserInputBoundary {
    UserResponseModel create(UserRequestModel requestModel);
}

Next, we have our output boundaries for making use of the outer layers. First, let’s define the data source gateway:

接下来,我们有了利用外层的输出边界。首先,让我们定义数据源网关。

interface UserRegisterDsGateway {
    boolean existsByName(String name);

    void save(UserDsRequestModel requestModel);
}

Second, the view presenter:

第二,观点的提出者。

interface UserPresenter {
    UserResponseModel prepareSuccessView(UserResponseModel user);

    UserResponseModel prepareFailView(String error);
}

Note we’re using the dependency inversion principle to make our business free from details such as databases and UIs.

请注意我们正在使用依赖反转原则来使我们的业务不受数据库和UI等细节的影响。

5.3. Decoupling Mode

5.3.去耦模式

Before proceeding, notice how the boundaries are contracts defining the natural divisions of the system. But we must also decide how our application will be delivered:

在继续之前,注意边界是定义系统的自然划分的合同。但我们也必须决定我们的应用程序将如何交付:

  • Monolithic – likely organized using some package structure
  • By using Modules
  • By using Services/Microservices

With this in mind, we can reach clean architecture goals with any decoupling mode. Hence, we should prepare to change between these strategies depending on our current and future business requirements. After picking up our decoupling mode, the code division should happen based on our boundaries.

考虑到这一点,我们可以通过任何解耦模式达到清洁架构目标。因此,我们应该准备根据我们当前和未来的业务需求在这些策略之间进行转换。在选择了我们的解耦模式后,代码的划分应该根据我们的边界来进行。

5.4. Request and Response Models

5.4.请求和响应模型

So far, we have created the operations across layers using interfaces. Next, let’s see how to transfer data across these boundaries.

到目前为止,我们已经使用接口创建了跨层的操作。接下来,让我们看看如何跨越这些边界传输数据。

Notice how all our boundaries are dealing only with String or Model objects:

注意我们所有的边界都只处理StringModel对象。

class UserRequestModel {

    String login;
    String password;

    // Getters, setters, and constructors
}

Basically, only simple data structures can cross boundaries. Also,  all Models have only fields and accessors. Plus, the data object belongs to the inner side. So, we can keep the dependency rule.

基本上,只有简单的数据结构可以跨越边界。而且,所有的Models都只有字段和访问器。另外,数据对象属于内侧。所以,我们可以保留依赖规则。

But why do we have so many similar objects? When we get repeated code, it can be of two types:

但为什么我们会有这么多类似的对象?当我们得到重复的代码时,它可能有两种类型:

  • False or accidental duplication – the code similarity is an accident, as each object has a different reason to change. If we try to remove it, we”ll risk violating the single responsibility principle.
  • True duplication – the code changes for the same reasons. Hence, we should remove it

As each Model has a different responsibility, we got all these objects.

由于每个模型有不同的责任,我们得到了所有这些对象。

5.5. Testing the UserRegisterInteractor

5.5.测试UserRegisterInteractor

Now, let’s create our unit test:

现在,让我们来创建我们的单元测试。

@Test
void givenBaeldungUserAnd12345Password_whenCreate_thenSaveItAndPrepareSuccessView() {
    given(userDsGateway.existsByIdentifier("identifier"))
        .willReturn(true);

    interactor.create(new UserRequestModel("baeldung", "123"));

    then(userDsGateway).should()
        .save(new UserDsRequestModel("baeldung", "12345", now()));
    then(userPresenter).should()
        .prepareSuccessView(new UserResponseModel("baeldung", now()));
}

As we can see, most of the use case test is about controlling the entities and boundaries requests. And, our interfaces allow us to easily mock the details.

我们可以看到,大部分的用例测试是关于控制实体和边界请求的。而且,我们的接口允许我们轻松地模拟这些细节。

6. The Interface Adapters

6.接口适配器

At this point, we finished all our business. Now, let’s start plugging in our details.

在这一点上,我们完成了所有的业务。现在,让我们开始插入我们的细节。

Our business should deal only with the most convenient data format for it, and so should our external agents, as DBs or UIs. But, this format usually is different. For this reason, the interface adapter layer is responsible for converting the data.

我们的业务应该只处理对它来说最方便的数据格式,而我们的外部代理也应该如此,作为DB或UI。但是,这种格式通常是不同的。出于这个原因,接口适配器层负责转换数据

6.1. UserRegisterDsGateway Using JPA

6.1.UserRegisterDsGateway使用JPA

First, let’s use JPA to map our user table:

首先,让我们使用JPA来映射我们的user表。

@Entity
@Table(name = "user")
class UserDataMapper {

    @Id
    String name;

    String password;

    LocalDateTime creationTime;

    //Getters, setters, and constructors
}

As we can see, the Mapper goal is to map our object to a database format.

我们可以看到,Mapper的目标是将我们的对象映射到数据库格式。

Next, the JpaRepository using our entity:

接下来,JpaRepository使用我们的entity

@Repository
interface JpaUserRepository extends JpaRepository<UserDataMapper, String> {
}

Given that we’ll be using spring-boot, then this is all it takes to save a user.

鉴于我们将使用spring-boot,那么这就是保存一个用户的全部内容。

Now, it’s time to implement our UserRegisterDsGateway:

现在,是时候实现我们的UserRegisterDsGateway:

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);
    }
}

For the most part, the code speaks for itself. Besides our methods, note the UserRegisterDsGateway’s name. If we chose UserDsGateway instead, then other User use cases would be tempted to violate the interface segregation principle.

在大多数情况下,代码是不言自明的。除了我们的方法,请注意UserRegisterDsGateway的名称。如果我们选择UserDsGateway而不是,那么其他User用例就会受到诱惑而违反interface segregation原则

6.2. User Register API

6.2.用户注册API

Now, let’s create our HTTP adapter:

现在,让我们来创建我们的HTTP适配器。

@RestController
class UserRegisterController {

    final UserInputBoundary userInput;

    // Constructor

    @PostMapping("/user")
    UserResponseModel create(@RequestBody UserRequestModel requestModel) {
        return userInput.create(requestModel);
    }
}

As we can see, the only goal here is to receive the request and send the response to the client.

正如我们所看到的,这里的唯一目标是接收请求并向客户发送响应。

6.3. Preparing the Response

6.3.准备应对措施

Before responding back, we should format our response:

在回击之前,我们应该对我们的回应进行格式化。

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);
    }
}

Our UserRegisterInteractor forced us to create a presenter. Still, the presentation rules concerns only within the adapter. Besides, whenever something is hard to test, we should divide it into a testable and a humble object. So, UserResponseFormatter easily allows us to verify our presentation rules:

我们的UserRegisterInteractor迫使我们创建一个演示器。不过,演示器的规则只在适配器内关注。此外,w凡是难以测试的东西,我们应该把它分成可测试的和所以,UserResponseFormatter很容易让我们验证我们的呈现规则:

@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");
}

As we can see, we tested all our logic before sending it to the view. Hence, only the humble object is in the less testable part.

正如我们所看到的,我们在将逻辑发送到视图之前测试了所有的逻辑。因此,只有卑微的对象处于不太可测试的部分

7. Drivers and Frameworks

7.驱动力和框架

In truth, we usually don’t code here. That is because this layer represents the lowest level of connection to external agents. For example, the H2 driver to connect to the database or the web framework. In this case, we’re going to use spring-boot as the web and dependency injection framework.  So, we need its start-up point:

事实上,我们通常不在这里编码。这是因为这一层代表了与外部代理连接的最低层次。例如,连接到数据库或Web框架的H2驱动。在本例中,我们将使用spring-boot作为webdependency injection框架。 因此,我们需要它的启动点。

@SpringBootApplication
public class CleanArchitectureApplication {
    public static void main(String[] args) {
      SpringApplication.run(CleanArchitectureApplication.class);
    }
}

Until now, we didn’t use any spring annotation in our business. Except for the spring-specifics adapters, as our UserRegisterController. This is because we should treat spring-boot as any other detail.

到目前为止,我们没有使用任何spring注释在我们的业务中。除了spring-specifics适配器,如我们的UserRegisterController。这是因为 我们应该 将spring-boot当作任何其他细节

8. The Terrible Main Class

8.可怕的主课

At last, the final piece!

终于,最后一块了!

So far, we followed the stable abstractions principle. Also, we protected our inner layers from the external agents with the inversion of control. Lastly, we separated all object creation from its use. At this point, it’s up to us to create our remaining dependencies and inject them into our project:

到目前为止,我们遵循稳定的抽象原则此外,我们用inversion of control保护我们内部层免受外部代理的攻击。最后,我们将所有对象的创建与使用分开。在这一点上,我们要做的是创建我们剩余的依赖项并将其注入我们的项目

@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");
}

In our case, we’re using the spring-boot dependency injection to create all our instances. As we’re not using @Component, we’re scanning our root package and ignoring only the Model objects.

在我们的案例中,我们使用spring-boot 依赖注入来创建所有的实例。由于我们没有使用@Component,我们正在扫描我们的根包,只忽略了Model objects

Although this strategy may look more complex, it decouples our business from the DI framework. On the other hand, the main class got power over all our system. That is why clean architecture considers it in a special layer embracing all others:

虽然这个策略看起来更复杂,但它将我们的业务与DI框架解耦。另一方面,主类得到了对我们所有系统的权力。这就是为什么干净的架构把它放在一个特殊的层中,包含了所有其他的层。

9. Conclusion

9.结语

In this article, we learned how Uncle Bob’s clean architecture is built on top of many design patterns and principles. Also, we created a use case applying it using Spring Boot.

在这篇文章中,我们了解到Bob叔叔的干净的架构是如何建立在许多设计模式和原则之上的。此外,我们还创建了一个使用Spring Boot应用它的用例。

Still, we left some principles aside. But, all of them lead in the same direction. We can summarize it by quoting its creator: “A good architect must maximize the number of decisions not made.”, and we did it by protecting our business code from the details using boundaries.

仍然,我们把一些原则留在一边。但是,所有这些都指向同一个方向。我们可以通过引用其创造者的话来总结它。”一个好的架构师必须最大限度地增加不做决定的数量。”,我们通过使用边界保护我们的业务代码,使之不受细节的影响

As usual, the complete code is available over on GitHub.

像往常一样,完整的代码可以在GitHub上找到