一、DDD概述
-
DDD,即領域驅動設計,核心是不斷提煉通用語言並用於與領域專家等團隊所有成員交流,並用程式碼來表達出一個與通用語言一致的領域模型。
-
通用語言:透過團隊交流達成共識的能夠簡單清晰準確傳遞業務規則的語言(可以是文字、圖片等)
-
領域:軟體系統要解決的問題域,是有邊界的。領域一般包含多個子域,子域根據其功能劃分為核心域、通用域、支撐域。
-
限界上下文:描述領域邊界,一個限界上下文可能包含多個子域,但一般實踐上都以一對一為好。應用單元和部署單元一般也與限界上下文一致。
-
限界上下文對映:多個上下文之間如何進展系統互動整合。
-
領域模型:對我們軟體系統中要解決問題的抽象表達(解決方案)。模型一般在一個限界上下文中有效。
- 模組
- 聚合根
- 實體
- 值物件
- 領域事件
- 倉儲定義
- 領域服務
- 工廠
-
限界上下文對映的反腐層定義
-
領域實現:
- 領域模型
- 應用服務
- 基礎設施
- 服務暴露
- 倉儲實現
- 反腐層實現
-
實踐步驟為:
- 找到子域
- 識別核心域、通用域、支撐域
- 確定限界上下文對映
- 在每個子域內設計領域模型
- 實現領域模型和應用
二、示例需求分析
要實現多規格商品的建立和查詢。
Spu相關操作如下:
@RestController
@RequestMapping("/v1/spu")
public class SpuRestApi {
//spu建立
@PostMapping("/create")
public Result<Long> create(@RequestBody SpuCreateParam param){
}
//spu詳情
@GetMapping("/detail")
public Result<SpuVO> findSpuById(Long shopId, Long spuId){
}
}
SpuCreateParam的定義如下:
其中spuNo,skuNo,barCodes等要求唯一
{
"shopId": 0, //店鋪ID
"categoryId": 0,//分類ID
"unitId": 0,//單位ID
"name": "string",//SPU名稱,長度20,不能為空
"spuNo": "string",//SPU編碼,不可變更,用於各系統間傳遞
"barCodes": [//SPU條碼列表,最多10個,用於搜尋
"string"
],
"photoTuple": {//圖片列表,最多10張
"photos": [
{
"url": "string" //必須為合法url,每個url長度最大為120
}
]
},
"specDefineTuple": {//規格定義,項與值都不能重複,相對順序用於sku列表的排序
"defines": [//規格項列表,如【顏色+尺寸】
{
"key": "string",//規格項,如顏色
"values": [//規格值列表,如紅色、白色等
"string"
]
}
]
},
"skus": [//SKU列表,要符合規格定義的笛卡爾積
{
"skuNo": "string",//SKU編碼
"barCodes": [//SKU條碼,用於SKU維度搜尋,最多10個
"string"
],
"retailPrice": 0,//零售價,分,最大為 100w*100
"specTuple": {//與規格定義笛卡爾積中每一個組合對應,如【紅色 + 20號】
"specs": [
{
"key": "string", //規格項,如顏色
"value": "string" //規格值,如紅色
}
]
}
}
]
}
三、分層架構 + 程序導向設計
特徵:
- 包劃分上以功能為準,如所有model放一個包,所有service放另一個
- 服務依賴:SpuApi -> SpuService -> SpuMapper -> Mybatis
- 建立時資料流向:SpuSaveParam -> Spu -> table
- 查詢時資料流向:SpuVO <- Spu <- table
- 整個SpuService只包含簡單的CRUD操作,尤其是更新操作,一般傾向於只有一個萬能的Update。從方法名稱,你看不出任何的業務含義。
- SpuService:一個服務方法幾乎包含了所有的邏輯,負責校驗、獲取外部資訊、組裝、轉換SpuSaveParam為Spu、並呼叫SpuMapper儲存到資料庫。
- Spu為失血模型,只包含欄位,沒有get/set之外的方法,Spu與table的欄位幾乎一一對應。
優缺點:
- 在邏輯很簡單場景下,crud迭代最快,程序導向與人類思考的方式相近。
- 在複雜場景下,如spu建立涉及大量的校驗組裝等,很快SpuService.save方法就會過於龐大。另外,有大量的校驗邏輯,在更新場景下是可以複用的。
四、pipeline設計
spu的校驗可以根據spu的內聚資訊塊劃分成多個checker,然後將多個checker組合成一個pipeline流,從而可以更好的重用,並快速應對新增的校驗(加個checker就行了)。
另外,獲取外部資訊,如category、unit等,也可以用rxjava等併發去做,以加快速度。
缺點:
- 但是pipeline要求設計出一個好的context,用於上下文傳遞,一般會出現context的腐化。
- 另外,service的主邏輯不清晰,讀程式碼的成本變高。
五、六邊形架構 + 物件導向設計
示例實現的github地址
特徵:
- 採用分治法,將資料、約束、行為等劃分到最能表達它的領域模型中。
- 包劃分上以業務模組為準,同業務的identity、valueObject、event、repository、service等放在一個包下。
- SpuAppService:為應用服務,只是呼叫領域服務和倉儲等來串流程,不包含業務邏輯,如校驗等。
- 領域服務:本例項中沒有領域服務,如果有的話,會定義為SpuXxxService(Xxx指明業務操作)
- Spu: 包含欄位和行為,如校驗在構造和set時內建,方法體現業務操作如changeName,不是單一的update動作。
- SpuRepo:定義了倉儲的操作,實現在infra中基於mybatis等
- MybatisSpuRepo: 實現
- SpuMapper:基於mybatis訪問資料庫
具體包劃分如下:
- domain
- shop
- category
- unit
- spu
- sku
- spec
- code
- event
- Spu
- SpuRepo
- SpuService
- infra
- repo
- proxy
具體程式碼實現如下:
class SpuAppService{ //應用服務
@Transactional
public SpuId save(SpuCreateParam param){
ShopId shopId = new ShopId(param.getShopId());
//呼叫外部服務獲取關聯資訊,並驗證了關聯資訊的合法性
Category category = categoryService.findById(param.getShopId(), param.getCategoryId());
Unit unit = unitService.findById(param.getShopId(), param.getUnitId());
//呼叫Repo生成ID,後續流程中很有可能需要它
SpuId spuId = spuRepo.nextId();
//SpuNo構造時驗證引數的合法性,不包含特殊字元,不會超長等
//lockSpuNo, 用於保證編碼的唯一性,注意要實現為可重入鎖
SpuNo spuNo = codeLockService.lockSpuNo(new SpuNo(shopId, spuId, param.getSpuNo()));
//與SpuNo相似
SpuBarCodeTuple spuBarCodeTuple = codeLockService.lockSpuBarCodes(new SpuBarCodeTuple(shopId,spuId,param.getBarCodes()));
//用於根據引數生成對應的sku列表
List<Sku> skus = skuService.buildSkus(shopId, spuId, param.getSkus());
Spu spu = Spu.builder()
.shopId(shopId)
.spuId(spuId)
.no(spuNo)
.barCodes(spuBarCodeTuple)
.name(param.getName())
.photoTuple(param.getPhotoTuple())
.category(category)
.unit(unit)
.specDefineTuple(param.getSpecDefineTuple())
.skus( skus) //構造時觸發笛卡爾積相關校驗
.build(); //當例項化Spu時會呼叫法律時刻建構函式來校驗以上各資訊的約束條件
//在本步驟前,spu和sku都未生成
//spu是聚合根,其包含sku的實體的建立。
//因為sku的規格組與spu的規格定義是有對應約束的。
spuRepo.save(spu);
return spuId;
}
}
class MybatisSpuRepo implements SpuRepo{//倉儲實現
@Override
public void save(Spu spu) {
spuMapper.create(spu);
skuMapper.batchCreate(spu.getSkuTuple().getSkus());
}
}
@Mapper
public interface SpuMapper { //Mybatis實現資料庫訪問
@Options(useGeneratedKeys = true, keyProperty = "id")
@InsertProvider(type = SpuMapper.class, method = "createSql")
void create(Spu spu);
@Results(
id = "spuDetail",
value = {
@Result(property = "shopId.id", column = "shop_id"),//複雜物件對映
@Result(property = "barCodes", column = "bar_codes", typeHandler = SpuBarCodeTupleHandler.class)) //複雜物件JSON化為字串
}
)
@Select("select * from spu where shop_id = #{shopId} and spu_id = #{spuId}")
Spu findById(@Param("shopId") Long shopId, @Param("spuId") Long spuId);
}
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Getter
@Setter(AccessLevel.PROTECTED)
@Accessors(chain = true)
@Builder
@Slf4j
public class Spu extends IdentifiedEntity {//實體,同時也是聚合根
@NotNull(message = "商家不能為空")
private ShopId shopId;
@NotNull(message = "ID不能為空")
private SpuId spuId;
@NotNull(message = "名稱不可為空")
@Size(max = 100, min = 1, message = "名稱字元數為1到100個字")
private String name;
@NotNull(message = "編碼不能為空")
private SpuNo no;
//SpuBarCodeTuple內部保證其合法性,spu不用管理其細節,只要不為空,這個條碼組就是合法的。
@NotNull(message = "條碼組不能為空")
private SpuBarCodeTuple barCodes;
@NotNull(message = "圖片不能為空")
private PhotoTuple photoTuple;
@NotNull(message = "分類不能為空")
private CategoryId categoryId;
//導航屬性,可空,在某些需要的場景下去載入它
//如Spu詳情中應該包含,而spu列表中可以不存在
private Category category;
@NotNull(message = "單位不能為空")
private UnitId unitId;
private Unit unit;
@NotNull(message = "規格定義不能為空")
private SpecDefineTuple specDefineTuple;
@ListDistinct(message = "規格不能重複")
@Size(max = 600, message = "規格數最大不能超過600")
private List<Sku> skus = new ArrayList<>();
protected Spu(){ //用於使mybatis等框架能正常工作
}
public Spu(
ShopId shopId,
SpuId spuId,
SpuNo no,
SpuBarCodeTuple barCodes,
String name,
PhotoTuple photoTuple,
Category category,
Unit unit,
SpecDefineTuple specDefineTuple,
List<Sku> skuTuple) {
this.shopId = shopId;
this.spuId = spuId;
this.name = name;
this.no = no;
this.barCodes= barCodes;
this.photoTuple = photoTuple;
this.category = category;
this.categoryId = category.getCategoryId();
this.unit = unit;
this.unitId = unit.getUnitId();
this.specDefineTuple = specDefineTuple;
this.skus = skuTuple;
//整合valiation框架,能基於上面定義的註解去校驗,從而讓校驗以宣告式寫法來表述
super.