Spring Boot的Clean架構教程與原始碼 - Baeldung

發表於2021-01-24

在本文中,我們將根據Robert C. Martin的Clean Architecture建立一個使用者註冊API的示例。我們將使用他的原始層-實體,用例,介面介面卡和框架/驅動程式。

 

Clean簡潔架構概述

Clean的體系結構包含許多程式碼設計和原理,例如 SOLID穩定的抽象等。但是,核心思想是 根據業務價值將系統劃分為多個級別。因此,最高階別具有業務規則,每一個較低階別的業務規則都離I / O裝置越來越近。

同樣,我們可以將級別轉換為層。在這種情況下,情況恰恰相反。內層等於最高階別,依此類推:

Spring Boot的Clean架構教程與原始碼 - Baeldung

考慮到這一點,我們可以根據業務需要設定多個級別。但是,始終要考慮 依賴性規則–較高的級別絕不能依賴較低的級別。

 

規則

讓我們開始為我們的使用者註冊API定義系統規則。一,業務規則:

  • 使用者密碼必須超過五個字元

其次,我們有應用規則。它們可以是不同的格式,例如用例或故事。我們將使用講故事的短語:

  • 系統接收使用者名稱和密碼,驗證使用者是否不存在,並儲存新使用者以及建立時間

請注意,這裡沒有提到任何資料庫,UI或類似內容。因為 我們的業務不關心這些細節,所以我們的程式碼也不關心

 

實體層

正如干淨的架構所建議的那樣,讓我們​​從業務規則開始:

interface User {
    boolean passwordIsValid();

    String getName();

    String getPassword();
}

並且,一個UserFactory:

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

我們建立使用者 工廠方法的原因有兩個。保留穩定的抽象原理並隔離使用者建立。

接下來,讓我們同時實現:

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

如果我們的業務很複雜,那麼我們應該儘可能清晰地構建領域程式碼。因此,此層是應用設計模式的好地方。特別是,應該考慮到領域驅動設計。

 

單元測試

測試我們的CommonUser:

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

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

如我們所見,單元測試非常清楚。畢竟,缺少mocks 是這一層的一個好訊號。

通常,如果我們在這裡開始考慮mocks ,也許我們正在將實體與用例混合在一起。

 

用例層

用例是 與系統自動化相關的規則。在“乾淨的體系結構”中,我們將其稱為“互動器Interactors”。

首先,我們將構建我們的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或資料庫的工作方式做任何假設。但是,我們正在使用UserDsGateway和UserPresenter,連同UserInputBoundary,這些都是我們的輸入和輸出邊界。

 

輸入和輸出邊界

邊界是定義元件如何互動的契約。輸入邊界暴露出我們的用例到外層是:

interface UserInputBoundary {
    UserResponseModel create(UserRequestModel requestModel);
}

接下來,我們有了利用外層的輸出邊界。首先,讓我們定義資料來源閘道器:

interface UserRegisterDsGateway {
    boolean existsByName(String name);

    void save(UserDsRequestModel requestModel);
}
檢視展現:
interface UserPresenter {
    UserResponseModel prepareSuccessView(UserResponseModel user);

    UserResponseModel prepareFailView(String error);
}

請注意,我們使用的是 依賴倒置原則,使我們的業務擺脫了資料庫和UI等細節的困擾。

 

去耦模式

在繼續之前,請注意邊界是如何 定義系統的自然劃分的契約。但是我們還必須決定如何交付我們的應用程式:

  • 單體式/整體式-可能使用某些封裝結構來組織
  • 通過使用模組
  • 通過使用服務/微服務

考慮到這一點,我們可以 使用任何去耦模式達到乾淨的架構目標。因此,我們應該準備根據當前和將來的業務需求在這些策略之間進行更改。選擇了我們的解耦模式後,應根據我們的邊界進行程式碼劃分。

 

請求和響應模型

到目前為止,我們已經使用介面跨層建立了操作。接下來,讓我們看看如何跨這些邊界傳輸資料。

注意我們所有的邊界如何僅處理String或Model物件:

class UserRequestModel {

    String login;
    String password;

    // Getters, setters, and constructors
}

基本上,只有簡單的資料結構才能跨越邊界。而且,所有模型都只有欄位和getter/setter(banq注:實則是DTO或領域事件等VO)。另外,資料物件屬於內部。因此,我們可以保留依賴性規則。

 

測試UserRegisterInteractor

現在,讓我們建立單元測試:

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

我們可以看到,大多數用例測試都是關於控制實體和邊界請求的。而且,我們的介面使我們可以輕鬆地模擬mock這些細節。

 

介面介面卡

至此,我們完成了所有業務。現在,讓我們開始插入我們的細節。

我們的業務應該只處理最方便的資料格式,我們的外部代理(如資料庫或UI)也應該處理。但是,這種格式通常是不同的。因此,介面介面卡層負責轉換資料。

首先,讓我們使用JPA對映使用者表:

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

    @Id
    String name;

    String password;

    LocalDateTime creationTime;

    //Getters, setters, and constructors
}

Mapper的目標是將物件對映到資料庫格式。

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

現在,是時候實現我們的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);
    }
}

在大多數情況下,程式碼可以說明一切。除了我們的方法外,請注意UserRegisterDsGateway的名稱。如果我們改為選擇UserDsGateway,那麼除註冊以外其他使用者功能也只能放入UserDsGateway中。將很容易違反介面隔離原則

現在,讓我們建立我們的HTTP介面卡:

@RestController
class UserRegisterController {

    final UserInputBoundary userInput;

    // Constructor

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

這裡的唯一目標是接收請求並將響應傳送給客戶端。

在將響應傳送客戶端之前,我們應該格式化回應:

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

介面展現的規則僅與介面卡有關。

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

我們在將所有邏輯傳送到檢視之前已經對其進行了測試。有些東西是很難測試,我們應該把它分成一個可測試和humble object。UserResponseFormatter是一種可測試的物件,能輕鬆地讓我們來測試。

 

驅動和框架 

實際上,我們通常不在此處編寫程式碼。這是因為該層表示與外部代理的最低連線級別。例如,H2驅動程式連線到資料庫或Web框架。在這種情況下,我們將使用spring-boot作為Web依賴注入框架。因此,我們需要它的啟動應用:

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

到目前為止,我們在業務中沒有使用任何 spring註釋,包括UserRegisterController,我們應該 將spring-boot視為類似資料庫、介面的其他任何細節。

 

可怕的主類

到目前為止,我們遵循穩定的抽象原理。同樣,我們通過反轉控制來保護我們的內層免受外部代理的攻擊。最後,我們將所有物件建立與使用分開。在這一點上,我們需要建立剩餘的依賴項並將它們注入到我們的專案中:

@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-boot 依賴項注入 來建立所有例項。但是我們沒有使用 @Component,同時實現了根包的掃描,但只忽略Model物件。

儘管此策略可能看起來更復雜,但它使我們的業務與DI框架脫鉤。另一方面,主類統管了我們整個系統。這就是為什麼乾淨的體系結構認為它特殊層中可包含所有其他層的原因:

Spring Boot的Clean架構教程與原始碼 - Baeldung

總結

在本文中,我們瞭解了Bob叔叔的乾淨架構是如何 在許多設計模式和原則之上構建的。另外,我們使用Spring Boot建立了一個用例。

完整的程式碼可以 在GitHub上找到。點選標題見原文

 

相關文章