分層設計
領域驅動設計(Domain-driven design, DDD) 作為一種複雜軟件系統的應對方案,在設計和編碼提供了一種新的解決方式,即領域驅動,要求程序員在設計和編碼時從領域專家的角度
出發來實現架構/代碼,做到代碼即業務。同時利用各種方式拆解複雜模塊,常用的方式有拆分子域、構建富血對象。
設計時,需要建立統一語言,確保領域中的業務概念處於同一個限界上下文,比如在一套電商系統中,用户買了一個東西,對應後台有一個訂單,此時訂單指代
訂單域的一項數據,當該訂單需要發貨時,在物流域中也會接受訂單域輸入併產生髮貨訂單,此時,物流域的訂單和訂單域的訂單就不處於一個限界上下文。建立統一語言有助於
後續的產品和研發之間的高效溝通,打破代碼和業務的語義鴻溝。領域模型的設計方法有 用例分析、事件風暴,領域模型需要提取出核心功能,並保證一定的擴展性,往往該過程是最重要也是最困難的。
進入編碼階段,構建聚合、聚合根、實體、值對象。雖然領域層與業務邏輯強關聯,但是為了技術實現,在設計時也會有一些妥協,如,聚合不宜設計的過大,聚合的設計需要考慮
實體之間的一致性要求,同時有一些事務、鎖的使用在某些時候會侵入領域層(並非不能這樣,實踐中往往在實現時會借鑑DDD的思想,但不會全套照搬);除此之外,結合事件驅動的方式,
可以讓領域層代碼保留一定的擴展性,實現上可以參考文章SpringEvent擴展性利器;
領域層作為核心不應該依賴具體實現,借鑑六邊形架構,領域層中定義了倉儲協議(Repository接口),業務邏輯只需要從倉儲接口中獲取數據,至於實現領域層並不關係,而具體的實現由其他模塊如infrastructure層來實現;
同時,在實際處理輸入時(http,rpc,job...)通常涉及與其他域的交互,DDD中通過構建防腐層來應對外部變化。
最終得到的代碼分層結構如下圖,Maven archetype代碼參見:ddd-spring-web-maven-archetype:
編碼tips
構建富血實體
經典的MVC架構基於貧血對象構建,貧血對象只作為data class,其業務含義丟失,通過構建富血對象將業務實體的
邏輯內聚,不在分散在各個service中,一是業務含義清晰,二是能夠單點控制。
比如,判斷ExpressAggregate物流聚合的發貨狀態,其含有字段如下:
@Data
public class ExpressAggregate {
private ExpressNumber expressNumber;
// 狀態
private ExpressStatus expressStatus;
...
}
基於貧血對象,判斷該物流實體是否發貨需要在service中調用ExpressAggregate做判斷:
ExpressAggregate expressAggregate = ...;
if (Objects.equals(express.status,...)){
// bisiness logic
...
}
而基於富血對象,我們可以將是否發貨的邏輯內置與ExpressAggregate中:
@Data
public class ExpressAggregate {
private ExpressNumber expressNumber;
private ExpressStatus expressStatus;
...
/**
* 判斷是否發貨
* @return
*/
public boolean hasSent() {
return Objects.equals(this.expressStatus, ExpressStatus.SENT)
|| Objects.equals(this.expressStatus, ExpressStatus.RECEIVED)
|| Objects.equals(this.expressStatus, ExpressStatus.RETURN);
}
}
這樣,調用方直接使用 expressAggregate.hasSent() 即可知道結果,避免了判斷邏輯散落各處。
值對象不可變
使用值對象表示無唯一標識(id)含義的實體,其各項屬性相等即視為同一個值對象,因此值對象不可變。在實現層面,
值對象不應有setter:
// 無setter
@Getter
@AllArgsConstructor(staticName = "of")
public class ExpressNumber {
private String expressNumber;
}
// usage
ExpressNumber expressNum = ExpressNumber.of("abc123");
相比於直接使用String expressNumber , 在業務代碼中使用ExpressNumber具有更強的業務含義,且作為方法入參時不易與其他String類型參數弄混。
層間對象轉換
不同層的對象不應混用,層間調用應使用轉換器轉換,轉換工作由誰來做?誰有轉換需求誰來做。
CQRS
CQRS(Command Query Responsibility Segregation) 將輸入分為 Command 和 Query,
Command作為變更系統狀態的輸入由領域層(聚合根)處理,而Query可不走領域層。
圖片來源:Axon Framework : Architecture Overview