Show Me Code——領域驅動設計中的代碼實現

語言: CN / TW / HK

1. 初步填充分層架構實現

分層架構設計後我們確定了代碼實現的層間關係,現在就先以布控任務這個領域模型為例填充下我們的代碼骨架。代碼已託管至Gitee

1.1 領域對象實現

二話不説先把領域對象連人帶椅子搬上來,在domain層創建ControlTask。主鍵外鍵、審計字段、關鍵屬性悉數登場。

java  public class ControlTask {  ​      private Long id;      private Long userId;      private String name;      private String target;      private LocalDateTime startAt;      private LocalDateTime endAt;      private LocalDateTime createdAt;      private Long createdBy;      private LocalDateTime lastUpdatedAt;      private Long lastUpdatedBy;  ​      public ControlTask() {     }            // Getter and Setter  }

然後可以按照類間調用時序或者層間依賴逐個填充各層實現。

1.2 被動適配器實現

作為被動觸發器,需要http端點接收調用請求,依賴應用層的應用服務。通過Dto(Data Transfer Object)和領域對象做數據轉換。

java  @RestController  public class ControlResource {  ​      private ControlService controlService;  ​      @Autowired      public ControlResource(ControlService controlService) {          this.controlService = controlService;     }  ​      @PostMapping("api/controls")      public ControlDto addControl(@RequestBody ControlDto request) {          // Get UserId          return controlService.addControl(request, userId);     }  }

1.3 數據傳輸對象與應用服務實現

實現過程始終要注意不壞層間依賴關係。涉及對象轉換這裏採用的方法是將ControlDto直接放到應用層。

應用服務作為領域層的門面,負責整合領域層的業務邏輯,封裝成更粗粒度的數據對象。

這裏應用服務需要做的事情具有代表性——帶有業務規則的校驗請求參數、創建領域對象、持久化、封裝數據傳輸對象返回。從而先形成對領域對象和倉庫的依賴。

java  @Service  public class ControlService {  ​      private final ControlValidator controlValidator;      private final ControlRepository controlRepository;  ​      @Autowired      public ControlService(              ControlValidator controlValidator,              ControlRepository controlRepository     ) {          this.controlValidator = controlValidator;          this.controlRepository = controlRepository;     }  ​      public ControlDto addControl(ControlDto request, Long userId) {          controlValidator.validate(request);          ControlTask controlTask = buildControlTask(request, userId);          controlTask = controlRepository.save(controlTask);          return buildControlDto(controlTask);     }  ​      private ControlDto buildControlDto(ControlTask controlTask) {          // todo     }  ​      private ControlTask buildControlTask(ControlDto request, Long userId) {          // todo     }  }

1.4 領域服務實現

至於業務規則的參數校驗,可以進一步抽象為領域服務(Domain Service),劃撥到領域層。這個過程對應一種DDD設計模式——表意接口(Intention-Revealing Interfaces),是DDD特別重視代碼命名反映領域知識,與統一語言保持一致的一種實踐方法。

通過布控任務校驗器ControlValidator統一封裝請求對象的業務規則校驗,比如這裏對布控時間段以及布控任務名稱的校驗。

java  @Component  public class ControlValidator {  ​      private final InputTextValidator inputTextValidator;      private final DateTimeValidator dateTimeValidator;  ​      @Autowired      public ControlValidator(InputTextValidator inputTextValidator, DateTimeValidator dateTimeValidator) {          this.inputTextValidator = inputTextValidator;          this.dateTimeValidator = dateTimeValidator;     }  ​      public void validate(ControlDto request) {          inputTextValidator.defaultInputShouldNotLongerThan(request.getName());          dateTimeValidator.startTimeShouldEarly(request.getStartAt(), request.getEndAt());     }  }

這裏留一個隱患,Validator位於領域層,ControDto位於應用層,validate導致層間依賴關係又被破壞了,我們將在下一章節徹底處理掉。

補充一下應用層與領域層內的業務邏輯歸屬,在應用層和領域層都會處理業務邏輯,區別是隻能由研發自己瞭解的放在應用層,需要和領域專家共同探討的放在領域層,領域對象是不能包含只有研發自己才懂的內容。

1.5 主動適配器實現

再回到應用服務上,需要持久化領域對象。在分層架構設計中我們重點介紹了引入DIP保證層間依賴關係。

我們直接給出依賴倒置後的類圖效果。

應用服務ControlService到倉庫接口有三個箭頭,代表着ControService使用了這三個接口定義的屬性,能夠通過屬性導航到倉庫,但是倉庫沒有屬性能夠導航到ControlService,所以關聯關係是單向的,應用層依賴領域層。

另一方面,主動適配器的倉庫實現也有虛線箭頭指向倉庫接口,代表着倉庫實現關係,適配器也依賴於領域層。從而順利解決層間依賴關係。

DIP倒置主動適配器依賴舉例.png

面向過程的三層架構直接是在service中依賴repository,這裏只需要把原來的倉庫接口變為倉庫的實現類,抽離出的接口移入領域層。就像這樣,ControlRepositoryJdbc就是原來的倉庫接口,變為了倉庫實現。

java  @Repository  public class ControlRepositoryJdbc implements ControlRepository {  ​      @Override      public ControlTask save(ControlTask controlTask) {          // todo     }  }

ControlRepository移入到了領域層,領域服務的依賴還在領域層,被動適配器作為倉庫接口的實現還在適配器層,並沒有像原來一樣依賴領域層或應用層,從而保證了層間依賴關係的穩定。

java  public interface ControlRepository {  ​      ControlTask save(ControlTask controlTask);  }

1.6 代碼結構

經過這麼一番折騰,布控任務管理的領域模塊基本建成,代碼結構是這樣的:

java  |-- adapter   |-- driven   |-- restful   |-- ControlResource.java      |-- driving     |-- persistence     |-- ControlRepositoryJdbc.java  |-- application   |-- controlmng   |-- ControlService.java   |-- ControlDto.java  |-- domain   |-- controlmng   |-- ControlTask.java     |-- ControlValidator.java   |-- ControlRepository.java  |-- SafetymonitorApplication.java

2.領域對象創建規則複雜度降解

我們不難發現,領域對象的創建邏輯也會領域層的一部分。簡單創建可以直接在構造器完成,但是對於複雜創建就需要考慮解耦保證領域對象的簡潔與聚焦,降低領域對象的創建複雜度。

創建邏輯複雜包括規則複雜與結構複雜,上文領域服務的抽取就是應對規則複雜的解決辦法,更準確的説是校驗規則複雜。後面的文章我們還會碰到結構複雜的情況。這裏就規則複雜看下常用降解複雜度的方法。

2.1 表意接口與領域服務

拿到領域模型後我們創建實體,對照業務規則表寫了一大片校驗邏輯,這些校驗邏輯互相依賴較少,而且會涉及訪問倉庫接口的情況。

如何重構呢,首先提取函數,可以通過現代IDE的快捷鍵迅速完成,比如説IDEA的Extract Method功能(Alt+Shift+M)。方法與變量命名要能體現領域知識,也就是表意接口,比如verify,startTimeShouldEarly等。

接着,將這些方法移動到領域層成為各個校驗器如DateTimeValidator,通過校驗器ControlValidator組合其他校驗器,ControlValidator也就是領域服務,應用服務從耦合校驗邏輯變為依賴領域服務。

布控模塊業務邏輯的代碼結構變成了這樣:

java  |-- application   |-- controlmng   |-- ControlService.java   |-- ControlDto.java  |-- domain   |-- controlmng   |-- validator   |-- DateTimeValidator.java   |-- InputTextValidator.java   |-- ControlValidator.java   |-- ControlTask.java   |-- ControlRepository.java

2.2 工廠模式

對於領域對象的創建邏輯,DDD提供了工廠(Factory)模式來聚焦。從而達成一種隱喻(metaphor)——工廠創造產品然後存入倉庫。以及上一章節留下的一個坑,領域服務依賴了應用層的數據傳輸對象,解決辦法就是採用Builder模式。

首先,創建ControlBuilder,接管並替換掉原來的領域服務ControlValidator,將規則校驗以及領域對象創建封裝起來。

java  public class ControlBuilder {  ​      private final InputTextValidator inputTextValidator;      private final DateTimeValidator dateTimeValidator;  ​      private Long userId;      private String name;      private String target;      private LocalDateTime startAt;      private LocalDateTime endAt;      private Long createdBy;  ​      @Autowired      public ControlBuilder(InputTextValidator inputTextValidator, DateTimeValidator dateTimeValidator) {          this.inputTextValidator = inputTextValidator;          this.dateTimeValidator = dateTimeValidator;     }  ​      public ControlBuilder userId(Long userId) {          this.userId = userId;          return this;     }  ​      // 省略其他build屬性  ​      public ControlTask build() {          validate();  ​          ControlTask controlTask = new ControlTask();          controlTask.setUserId(this.userId);          controlTask.setName(this.name);          controlTask.setTarget(this.target);          controlTask.setStartAt(this.startAt);          controlTask.setEndAt(this.endAt);          controlTask.setCreatedBy(this.createdBy);          controlTask.setCreatedAt(LocalDateTime.now());  ​          return controlTask;     }  ​      private void validate() {          inputTextValidator.defaultInputShouldNotLongerThan(this.name);          dateTimeValidator.startTimeShouldEarly(this.startAt, this.endAt);     }  }

然後,創建布控任務領域對象Builder工廠ControlBuilderFactory生產Builder。

注意因為創建ControlBuilder有可變屬性,所以不能單例注入到ControlService中(沒有使用@Component註解),可以使用Spring的prototype原型模式,或是就像這樣直接new一個新的ControlBuilder對象。

java  @Component  public class ControlBuilderFactory {  ​      private final InputTextValidator inputTextValidator;      private final DateTimeValidator dateTimeValidator;  ​      @Autowired      public ControlBuilderFactory(InputTextValidator inputTextValidator, DateTimeValidator dateTimeValidator) {          this.inputTextValidator = inputTextValidator;          this.dateTimeValidator = dateTimeValidator;     }  ​      public ControlBuilder create() {          return new ControlBuilder(inputTextValidator, dateTimeValidator);     }  }

最後,再來看下我們布控任務領域模型的門面——應用服務ControlService。

屬性依賴只有工廠與倉庫,是不就能和上面的隱喻對上了,代碼整潔邊界清晰。

通過Builder模式,創建領域對象也變得直觀,減少了參數順序不對導致參數錯誤的失誤。

java  @Service  public class ControlService {  ​      private final ControlBuilderFactory controlBuilderFactory;      private final ControlRepository controlRepository;  ​      @Autowired      public ControlService(              ControlBuilderFactory controlBuilderFactory,              ControlRepository controlRepository     ) {          this.controlBuilderFactory = controlBuilderFactory;          this.controlRepository = controlRepository;     }  ​      public ControlDto addControl(ControlDto request, Long userId) {          ControlBuilder controlBuilder = controlBuilderFactory.create();          ControlTask controlTask = controlBuilder.userId(userId)                 .name(request.getName())                 .target(request.getTarget())                 .startAt(request.getStartAt())                 .endAt(request.getEndAt())                 .createdBy(userId)                 .build();  ​          controlTask = controlRepository.save(controlTask);          return buildControlDto(controlTask);     }  ​      private ControlDto buildControlDto(ControlTask controlTask) {          // todo     }  }

3.面向對象還是面向過程

經典的編程範式大家討論了很多年,究竟是面向對象還是面向過程,通過老馬的著書經歷可見一二。

在OO剛問世的時候,桌面軟件開發盛行,比如Microsoft Office套件。開發特點是軟件運行的所有的數據都可以裝入內存,對象之間的導航自由;而在J2EE誕生時,企業應用開發逐漸佔據市場主導地位,應用數據日益龐大,數據庫技術得到大力發展,每次只能取出很小一部分放入內存,對象之間是沒法自由導航的,這也就造成了OO很難在企業應用中使用,貧血模型氾濫。

老馬的《重構》也在二十多年後的第二版中將實現語言變為了工程化沒那麼強的JavaScript,由面向對象的原教旨主義者變成了編程範式的中立主義者。也就是説對於編程範式而言,不拘泥於面向過程、面向對象、面向方面等等,而是將他們結合起來——面向多種編程範式編程

這也是鍾敬老師帶來的啟發,最後,給出鍾敬老師的代碼風格規則:

  • 領域對象不訪問數據庫
  • 領域服務只能讀數據庫
  • 應用服務可以讀寫數據庫
  • 用 ID 表示對象之間的關聯
  • 領域對象有自己的領域服務
  • 在以上前提下利用封裝和繼承

參考