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 表示物件之間的關聯
  • 領域物件有自己的領域服務
  • 在以上前提下利用封裝和繼承

參考