領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(Domain Driven Design,簡(jiǎn)稱:DDD)設(shè)計(jì)思想和方法論早在2005年時(shí)候就被提出來(lái),但是一直沒有被重視和推薦使用,直到2015年之后微服務(wù)流行之后,再次被人重視和推薦使用。
下面我來(lái)介紹一下DDD設(shè)計(jì)思想和方法論,同時(shí)結(jié)合我們?cè)趯?shí)際項(xiàng)目中應(yīng)用總結(jié)和思考。
目錄
1、為什么要用DDD
2、DDD架構(gòu)設(shè)計(jì)
3、領(lǐng)域建模方法
4、代碼實(shí)踐
5、問題總結(jié)
?文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-482315.html
1、為什么要用DDD
面向過(guò)程
? ? ? 很多時(shí)候,我們都是操著面向?qū)ο蟮恼Z(yǔ)言干著面向過(guò)程的勾當(dāng)。大部分的系統(tǒng)都是從單一業(yè)務(wù)開始的。但是隨著支持的業(yè)務(wù)越來(lái)越多,代碼里面開始出現(xiàn)大量的if-else邏輯,這個(gè)時(shí)候代碼開始有壞味道,沒聞到的同學(xué)就這么繼續(xù)往上堆,聞到的同學(xué)會(huì)重構(gòu)一下,但因?yàn)橄到y(tǒng)沒有統(tǒng)一的可擴(kuò)展架構(gòu),重構(gòu)的技法也各不相同,這種代碼的不一致性也是一種理解上的復(fù)雜度。
分層不合理
? ? ? 分層太多和沒有分層都會(huì)導(dǎo)致系統(tǒng)復(fù)雜度的上升。
隨心所欲
? ? ? 隨心所欲是因?yàn)槿鄙僖?guī)范和約束。
面向?qū)ο?/strong>
? ? ?深入的理解面向?qū)ο笤O(shè)計(jì)原則、模式、方法論有很多,同時(shí)要具備非常好的業(yè)務(wù)理解力和抽象能力。
? ? ?SOLID是單一職責(zé)原則(SRP),開閉原則(OCP),里氏替換原則(LSP),接口隔離原則(ISP)和依賴倒置原則(DIP)的縮寫,原則是要比模式更基礎(chǔ)更重要的指導(dǎo)準(zhǔn)則,深入理解面向?qū)ο笤O(shè)計(jì)原則極大的提升我們的面向?qū)ο笤O(shè)計(jì)的能力和代碼質(zhì)量。
分層設(shè)計(jì)
? ? ? 分層最大的好處就是分離關(guān)注點(diǎn),讓每一層只解決該層關(guān)注的問題,從而將復(fù)雜的問題簡(jiǎn)化,起到分而治之的作用。
領(lǐng)域建模
? ? ? 領(lǐng)域模型可以準(zhǔn)確地反映業(yè)務(wù)語(yǔ)言,使業(yè)務(wù)語(yǔ)義顯性化,而傳統(tǒng)的J2EE或Spring+Hibernate/Mybatis等事務(wù)性編程模型只關(guān)心數(shù)據(jù),這些數(shù)據(jù)對(duì)象除了簡(jiǎn)單setter/getter方法外,沒有任何業(yè)務(wù)方法,被稱為成貧血模式。
規(guī)范設(shè)計(jì)
? ? ? ? 各歸其位、命名約定、通用業(yè)務(wù)狀態(tài)碼約定等。
DDD概念定義
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì):一種軟件開發(fā)方法,是一種軟件核心復(fù)雜性應(yīng)對(duì)之道,它可以幫助我們?cè)O(shè)計(jì)高質(zhì)量的軟件模型。
DDD目的:
- DDD是為了解決復(fù)雜業(yè)務(wù)邏輯的問題
- DDD的核心問題不是技術(shù)問題,而是關(guān)于討論、聆聽、理解、發(fā)現(xiàn)業(yè)務(wù)價(jià)值的問題
- DDD是技術(shù)人員擁有產(chǎn)品思維的一個(gè)體現(xiàn)
- DDD的學(xué)習(xí)曲線很陡主要是因?yàn)樗且环N方法論,每個(gè)人對(duì)它的理解存在差異
?
領(lǐng)域模型與事務(wù)腳本對(duì)比
?
富血模型:就是屬性和方法都封裝在Domain Object里,所有業(yè)務(wù)都直接操作Domain Object。 service層只是完成一些簡(jiǎn)單的事務(wù)之類的,甚至可以不用service層。也就是直接從action->entity。
貧血模型:就是一個(gè)對(duì)象里只有屬性,要實(shí)現(xiàn)業(yè)務(wù),要依靠service層來(lái)實(shí)現(xiàn)相關(guān)方法,service層的實(shí)現(xiàn)是面向過(guò)程的,大量傳統(tǒng)的分層應(yīng)用action->service->dao->entity的方式就是這種貧血的模式實(shí)現(xiàn)。
舉個(gè)例子,銀行轉(zhuǎn)賬實(shí)現(xiàn)類
各位看官看到上面的代碼是不是有一種似曾相似的感覺?咋一看也沒有啥問題,能正常運(yùn)行,能快速交付業(yè)務(wù)。如果僅是為了應(yīng)付需求任務(wù)交差,當(dāng)然沒有什么啥問題了。
從設(shè)計(jì)角度來(lái)看確實(shí)存在一些問題,代碼糊在一起,沒有分層設(shè)計(jì),偽面向?qū)ο蟮姆椒ā?/span>
我們碼農(nóng)總得要有自己一點(diǎn)的追求,為偉大代碼事業(yè)創(chuàng)造一點(diǎn)藝術(shù)貢獻(xiàn)!
- 如果業(yè)務(wù)需求快速變化怎么辦,需求越來(lái)越復(fù)雜怎么辦?
- 如果團(tuán)隊(duì)里面有多人協(xié)作,代碼需要經(jīng)過(guò)多人反復(fù)修改接手傳承,你敢保證后面接手的人不會(huì)罵你嗎?
- 難道設(shè)計(jì)上有沒有可以改進(jìn)的空間?
答案是正面的!
客觀來(lái)說(shuō)上面的這段代碼不算復(fù)雜,也算寫得中規(guī)中舉。下面我來(lái)讓貼一段曾經(jīng)在我們實(shí)際生產(chǎn)系統(tǒng)跑了將近一年的神代碼,你才知道什么叫復(fù)雜和神奇!16層if..else+for循環(huán)!就問你怕了沒有?
》》》請(qǐng)點(diǎn)開看下面神碼片段????
請(qǐng)看神碼--地獄18層!
if (contentOld != null && contentNew != null) {
map.put("ModelId", contentNew.getModelId());//ModelId
map.put("name", contentNew.getName());//Name
map.put("Description", contentNew.getDescription());//Description
map.put("fujianGroup", attachments);//附件組信息
for (int i = 0; i < contentOld.getGroups().size(); i++) {//數(shù)據(jù)庫(kù)保存 組
MscsApiShowerRoomModelGroups groupsOld = contentOld.getGroups().get(i);
if ("納米易結(jié)".equals(groupsOld.getGroupName())) {//新增的組信息
map.put("nanometerGroup", groupsOld);//納米易結(jié)
} else if ("石基先發(fā)".equals(groupsOld.getGroupName())) {
map.put("stoneGroupFirst", groupsOld);//石基先發(fā)
} else if ("客戶其它內(nèi)容".equals(groupsOld.getGroupName())) {
map.put("otherGroup", groupsOld);//客戶其它內(nèi)容
} else {//原來(lái)的組 通過(guò)組ID 來(lái)判定
if ("1".equals(groupsOld.getGroupId())) {//產(chǎn)品規(guī)格組
for (MscsApiShowerRoomModelGroups groups : contentNew.getGroups()) {
if (groupsOld.getGroupId().equals(groups.getGroupId())) {
groups.setOtherValue(groupsOld.getOtherValue());//設(shè)置單選按鈕
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
for (MscsApiShowerRoomModelCategories categories : groupsOld.getGategories()) {
for (MscsApiShowerRoomModelCategories categories2 : groups.getGategories()) {
if (categories.getOptions() != null && categories2.getOptions() != null) {
for (MscsApiShowerRoomModelOptions options : categories.getOptions()) {
for (MscsApiShowerRoomModelOptions options2 : categories2.getOptions()) {
if (options.getOptionId() != null && options.getOptionId().equals(options2.getOptionId())) {
options2.setIsDefault(true);//設(shè)置為默認(rèn)選項(xiàng)
for (MscsApiShowerRoomModelProperties properties : options.getProperties()) {
for (MscsApiShowerRoomModelProperties properties2 : options2.getProperties()) {
if (properties.getPropertyId() != null && properties.getPropertyId().equals(properties2.getPropertyId())) {
properties2.setDefaults(properties.getDefaults());
}
}
}
} else {
options2.setIsDefault(false);
}
}
}
}
}
}
}
}
map.put("productGroup", groups);//產(chǎn)品規(guī)格組
}
}
// map.put("productGroup",null);//產(chǎn)品規(guī)格組
} else if ("2".equals(groupsOld.getGroupId())) {//開門方向與產(chǎn)品方向
for (MscsApiShowerRoomModelGroups groups : contentNew.getGroups()) {
if (groupsOld.getGroupId().equals(groups.getGroupId())) {
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
for (MscsApiShowerRoomModelCategories categories : groupsOld.getGategories()) {
for (MscsApiShowerRoomModelCategories categories2 : groups.getGategories()) {
if (categories.getOptions() != null && categories2.getOptions() != null) {
for (MscsApiShowerRoomModelOptions options : categories.getOptions()) {
for (MscsApiShowerRoomModelOptions options2 : categories2.getOptions()) {
if (options.getOptionId() != null && options.getOptionId().equals(options2.getOptionId())) {
options2.setIsDefault(true);//設(shè)置為默認(rèn)選項(xiàng)
} else {
options2.setIsDefault(false);
}
}
}
}
}
}
}
}
map.put("doorGroup", groups);//開門方向與產(chǎn)品方向
}
}
} else if ("3".equals(groupsOld.getGroupId())) {//產(chǎn)品顏色
for (MscsApiShowerRoomModelGroups groups : contentNew.getGroups()) {
if (groupsOld.getGroupId().equals(groups.getGroupId())) {
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
for (MscsApiShowerRoomModelCategories categories : groupsOld.getGategories()) {
for (MscsApiShowerRoomModelCategories categories2 : groups.getGategories()) {
if (categories.getOptions() != null && categories2.getOptions() != null) {
for (MscsApiShowerRoomModelOptions options : categories.getOptions()) {
for (MscsApiShowerRoomModelOptions options2 : categories2.getOptions()) {
if (options.getOptionId() != null && options.getOptionId().equals(options2.getOptionId())) {
options2.setIsDefault(true);//設(shè)置為默認(rèn)選項(xiàng)
} else {
options2.setIsDefault(false);
}
}
}
}
}
}
}
}
map.put("productColorGroup", groups);//產(chǎn)品顏色
}
}
} else if ("14".equals(groupsOld.getGroupId())) {//玻璃工藝
String name = "";
for (MscsApiShowerRoomModelGroups groups : contentNew.getGroups()) {
if (groupsOld.getGroupId().equals(groups.getGroupId())) {
groups.setOtherValue(groupsOld.getOtherValue());
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
for (MscsApiShowerRoomModelCategories categories : groupsOld.getGategories()) {
for (MscsApiShowerRoomModelCategories categories2 : groups.getGategories()) {
if (categories.getName() != null && categories.getName().equals(categories2.getName())) {
name = categories.getName();
if (categories.getOptions() != null && categories2.getOptions() != null) {
for (MscsApiShowerRoomModelOptions options : categories.getOptions()) {
for (MscsApiShowerRoomModelOptions options2 : categories2.getOptions()) {
if (options.getOptionId() != null && options.getOptionId().equals(options2.getOptionId())) {
options2.setIsDefault(true);//設(shè)置為默認(rèn)選項(xiàng)
} else {
options2.setIsDefault(false);
}
}
}
}
}
}
}
}
//設(shè)置 默認(rèn)數(shù)據(jù)的位置 選中的數(shù)組 為第一個(gè) 默認(rèn)顯現(xiàn)
if (groups.getGategories() != null && !"".equals(name)) {
List<MscsApiShowerRoomModelCategories> arry = new ArrayList<MscsApiShowerRoomModelCategories>();
List<MscsApiShowerRoomModelCategories> arry1 = new ArrayList<MscsApiShowerRoomModelCategories>();
List<MscsApiShowerRoomModelCategories> arry2 = new ArrayList<MscsApiShowerRoomModelCategories>();
for (MscsApiShowerRoomModelCategories categories : groups.getGategories()) {
if (name.equals(categories.getName())) {
arry1.add(categories);
} else {
arry2.add(categories);
}
}
for (MscsApiShowerRoomModelCategories a : arry1) {
arry.add(a);
}
for (MscsApiShowerRoomModelCategories a : arry2) {
arry.add(a);
}
groups.setGategories(arry);
}
map.put("glassGroup", groups);//玻璃工藝
}
}
// map.put("glassGroup", null);//玻璃工藝
} else if ("4".equals(groupsOld.getGroupId())) {//玻璃貼膜
for (MscsApiShowerRoomModelGroups groups : contentNew.getGroups()) {
if (groupsOld.getGroupId().equals(groups.getGroupId())) {
groups.setOtherValue(groupsOld.getOtherValue());
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
for (MscsApiShowerRoomModelCategories categories : groupsOld.getGategories()) {
for (MscsApiShowerRoomModelCategories categories2 : groups.getGategories()) {
if (categories.getName() != null && categories.getName().equals(categories2.getName())) {
if (categories.getOptions() != null && categories2.getOptions() != null) {
for (MscsApiShowerRoomModelOptions options : categories.getOptions()) {
for (MscsApiShowerRoomModelOptions options2 : categories2.getOptions()) {
if (options.getOptionId() != null && options.getOptionId().equals(options2.getOptionId())) {
options2.setIsDefault(true);//設(shè)置為默認(rèn)選項(xiàng)
} else {
options2.setIsDefault(false);
}
}
}
}
}
}
}
}
map.put("filmGroup", groups);//玻璃貼膜
// break ;
}
}
} else if ("5".equals(groupsOld.getGroupId())) {//玻璃厚度
for (MscsApiShowerRoomModelGroups groups : contentNew.getGroups()) {
if (groupsOld.getGroupId().equals(groups.getGroupId())) {
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
for (MscsApiShowerRoomModelCategories categories : groupsOld.getGategories()) {
for (MscsApiShowerRoomModelCategories categories2 : groups.getGategories()) {
if (categories.getName() != null && categories.getName().equals(categories2.getName())) {
if (categories.getOptions() != null && categories2.getOptions() != null) {
for (MscsApiShowerRoomModelOptions options : categories.getOptions()) {
for (MscsApiShowerRoomModelOptions options2 : categories2.getOptions()) {
if (options.getOptionId() != null && options.getOptionId().equals(options2.getOptionId())) {
options2.setIsDefault(true);//設(shè)置為默認(rèn)選項(xiàng)
} else {
options2.setIsDefault(false);
}
}
}
}
}
}
}
}
map.put("thicknessGroup", groups);//玻璃厚度
}
}
} else if ("6".equals(groupsOld.getGroupId())) {//拉手款式
String name = "";
for (MscsApiShowerRoomModelGroups groups : contentNew.getGroups()) {
if (groupsOld.getGroupId().equals(groups.getGroupId())) {
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
for (MscsApiShowerRoomModelCategories categories : groupsOld.getGategories()) {
for (MscsApiShowerRoomModelCategories categories2 : groups.getGategories()) {
if (categories.getName() != null && categories.getName().equals(categories2.getName())) {
name = categories.getName();
if (categories.getOptions() != null && categories2.getOptions() != null) {
for (MscsApiShowerRoomModelOptions options : categories.getOptions()) {
for (MscsApiShowerRoomModelOptions options2 : categories2.getOptions()) {
if (options.getOptionId() != null && options.getOptionId().equals(options2.getOptionId())) {
options2.setIsDefault(true);//設(shè)置為默認(rèn)選項(xiàng)
} else {
options2.setIsDefault(false);
}
}
}
}
}
}
}
}
//設(shè)置 默認(rèn)數(shù)據(jù)的位置 選中的數(shù)組 為第一個(gè) 默認(rèn)顯現(xiàn)
if (groups.getGategories() != null && !"".equals(name)) {
List<MscsApiShowerRoomModelCategories> arry = new ArrayList<MscsApiShowerRoomModelCategories>();
List<MscsApiShowerRoomModelCategories> arry1 = new ArrayList<MscsApiShowerRoomModelCategories>();
List<MscsApiShowerRoomModelCategories> arry2 = new ArrayList<MscsApiShowerRoomModelCategories>();
for (MscsApiShowerRoomModelCategories categories : groups.getGategories()) {
if (name.equals(categories.getName())) {
arry1.add(categories);
} else {
arry2.add(categories);
}
}
for (MscsApiShowerRoomModelCategories a : arry1) {
arry.add(a);
}
for (MscsApiShowerRoomModelCategories a : arry2) {
arry.add(a);
}
groups.setGategories(arry);
}
map.put("handleGroup", groups);//拉手款式
}
}
// map.put("handleGroup", null);//拉手款式
} else if ("7".equals(groupsOld.getGroupId())) {//石基
String name = "";
for (MscsApiShowerRoomModelGroups groups : contentNew.getGroups()) {
if (groupsOld.getGroupId().equals(groups.getGroupId())) {
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
for (MscsApiShowerRoomModelCategories categories : groupsOld.getGategories()) {
for (MscsApiShowerRoomModelCategories categories2 : groups.getGategories()) {
if (categories.getName() != null && categories.getName().equals(categories2.getName())) {
name = categories.getName();
if (categories.getOptions() != null && categories2.getOptions() != null) {
for (MscsApiShowerRoomModelOptions options : categories.getOptions()) {
for (MscsApiShowerRoomModelOptions options2 : categories2.getOptions()) {
if (options.getOptionId() != null && options.getOptionId().equals(options2.getOptionId())) {
options2.setIsDefault(true);//設(shè)置為默認(rèn)選項(xiàng)
} else {
options2.setIsDefault(false);
}
}
}
}
}
}
}
}
//設(shè)置 默認(rèn)數(shù)據(jù)的位置 選中的數(shù)組 為第一個(gè) 默認(rèn)顯現(xiàn)
if (groups.getGategories() != null && !"".equals(name)) {
List<MscsApiShowerRoomModelCategories> arry = new ArrayList<MscsApiShowerRoomModelCategories>();
List<MscsApiShowerRoomModelCategories> arry1 = new ArrayList<MscsApiShowerRoomModelCategories>();
List<MscsApiShowerRoomModelCategories> arry2 = new ArrayList<MscsApiShowerRoomModelCategories>();
for (MscsApiShowerRoomModelCategories categories : groups.getGategories()) {
if (name.equals(categories.getName())) {
arry1.add(categories);
} else {
arry2.add(categories);
}
}
for (MscsApiShowerRoomModelCategories a : arry1) {
arry.add(a);
}
for (MscsApiShowerRoomModelCategories a : arry2) {
arry.add(a);
}
groups.setGategories(arry);
}
map.put("stoneGroup", groups);//拉手款式
}
}
} else if ("8".equals(groupsOld.getGroupId())) {//層架
for (MscsApiShowerRoomModelGroups groups : contentNew.getGroups()) {
if (groupsOld.getGroupId().equals(groups.getGroupId())) {
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
for (MscsApiShowerRoomModelCategories categories : groupsOld.getGategories()) {
for (MscsApiShowerRoomModelCategories categories2 : groups.getGategories()) {
if (categories.getOptions() != null && categories2.getOptions() != null) {
for (MscsApiShowerRoomModelOptions options : categories.getOptions()) {
for (MscsApiShowerRoomModelOptions options2 : categories2.getOptions()) {
if (options.getOptionId() != null && options.getOptionId().equals(options2.getOptionId())) {
options2.setIsDefault(true);//設(shè)置為默認(rèn)選項(xiàng)
for (MscsApiShowerRoomModelProperties properties : options.getProperties()) {
for (MscsApiShowerRoomModelProperties properties2 : options2.getProperties()) {
if (properties.getPropertyId() != null && properties.getPropertyId().equals(properties2.getPropertyId())) {
properties2.setDefaults(properties.getDefaults());
}
}
}
} else {
options2.setIsDefault(false);
}
}
}
}
}
}
}
}
map.put("shelfGroup", groups);//層架
}
}
// map.put("shelfGroup", null);//層架
} else if ("9".equals(groupsOld.getGroupId())) {//木架包裝
for (MscsApiShowerRoomModelGroups groups : contentNew.getGroups()) {
if (groupsOld.getGroupId().equals(groups.getGroupId())) {
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
for (MscsApiShowerRoomModelCategories categories : groupsOld.getGategories()) {
for (MscsApiShowerRoomModelCategories categories2 : groups.getGategories()) {
if (categories.getOptions() != null && categories2.getOptions() != null) {
for (MscsApiShowerRoomModelOptions options : categories.getOptions()) {
for (MscsApiShowerRoomModelOptions options2 : categories2.getOptions()) {
if (options.getOptionId() != null && options.getOptionId().equals(options2.getOptionId())) {
options2.setIsDefault(true);//設(shè)置為默認(rèn)選項(xiàng)
} else {
options2.setIsDefault(false);
}
}
}
}
}
}
}
}
map.put("mujiaGroup", groups);//木架包裝
}
}
} else if ("32".equals(groupsOld.getGroupId())) {//玻璃寬度
for (MscsApiShowerRoomModelGroups groups : contentNew.getGroups()) {
if (groupsOld.getGroupId().equals(groups.getGroupId())) {
if (groupsOld.getGroupId().equals(groups.getGroupId())) {
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
if (groupsOld.getGategories() != null && groups.getGategories() != null) {
for (MscsApiShowerRoomModelCategories categories : groupsOld.getGategories()) {
for (MscsApiShowerRoomModelCategories categories2 : groups.getGategories()) {
if (categories.getOptions() != null && categories2.getOptions() != null) {
for (MscsApiShowerRoomModelOptions options : categories.getOptions()) {
for (MscsApiShowerRoomModelOptions options2 : categories2.getOptions()) {
if (options.getOptionId() != null && options.getOptionId().equals(options2.getOptionId())) {
options2.setIsDefault(true);//設(shè)置為默認(rèn)選項(xiàng)
if (options.getProperties() != null) {
for (MscsApiShowerRoomModelProperties properties : options.getProperties()) {
for (MscsApiShowerRoomModelProperties properties2 : options2.getProperties()) {
if (StringUtils.isNotEmpty(properties.getCode()) && properties.getCode().equals(properties2.getCode())) {
properties2.setDefaults(properties.getDefaults());
}
}
}
}
} else {
options2.setIsDefault(false);
}
}
}
}
}
}
}
}
map.put("glassWidthGroup", groups);// 玻璃寬度
}
}
}
}
}
}
}
?
各位看官你們看,看完是不是很想吐血!像上面這種代碼請(qǐng)問誰(shuí)敢去維護(hù)?誰(shuí)看誰(shuí)罵,誰(shuí)改誰(shuí)死!這種神代碼絕對(duì)是可以拿來(lái)當(dāng)教材用的~(在這里展示這段代碼主要為了說(shuō)明如果系統(tǒng)設(shè)計(jì)和分層不合理,將會(huì)帶來(lái)嚴(yán)重的后果,可以說(shuō)代碼就像狗屎一樣)
PS:說(shuō)明一下當(dāng)時(shí)寫這些代碼的作者因?yàn)橐恍┰螂x職了,我們當(dāng)年系統(tǒng)上線后將近一年多的時(shí)間里不敢去修改這神代碼,也沒有人敢改。
如果業(yè)務(wù)需求一直穩(wěn)定不改,那可以還勉強(qiáng)維持著,但是那有正常業(yè)務(wù)不改需求的?天底下應(yīng)該沒有!所以它始終像一顆大雷在我們的頭頂上滾動(dòng)著!
后來(lái)經(jīng)過(guò)兩次重大重構(gòu)設(shè)計(jì)之后,把它消滅了!篇幅原因這里的故事就不展開講了,有興趣的請(qǐng)看另一篇文章:《那些年那些神碼》。
回到主題上,銀行轉(zhuǎn)賬那一段代碼對(duì)比之下是不是小屋見大屋?下面我們基于銀行轉(zhuǎn)賬那一段代碼,展示一下怎么拆解和設(shè)計(jì),變成領(lǐng)域設(shè)計(jì)模型。
轉(zhuǎn)變成領(lǐng)域設(shè)計(jì)如下:
- 抽出Account Domain類;
- 抽出轉(zhuǎn)賬類即領(lǐng)域服務(wù);
- 抽出透支策略接口與實(shí)現(xiàn)類
重新拆解轉(zhuǎn)換之后,設(shè)計(jì)領(lǐng)域?qū)ο螅a分層結(jié)構(gòu)清晰了很多,后續(xù)業(yè)務(wù)折騰變化容易維護(hù)和擴(kuò)展,從此世界也變得清新了,你說(shuō)香不香嗎?
?
2、領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)架構(gòu)設(shè)計(jì)
2.1、分層結(jié)構(gòu)轉(zhuǎn)變
先看分層結(jié)構(gòu)思路轉(zhuǎn)變,領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)分層與傳統(tǒng)三層結(jié)構(gòu)有比較直觀的區(qū)別。
User Interface為用戶接口層,也是經(jīng)常我們看到Controller層類似,主要負(fù)責(zé)系統(tǒng)入口協(xié)議等,該層不處理任何業(yè)務(wù)邏輯,只負(fù)責(zé)調(diào)用接入和應(yīng)用轉(zhuǎn)發(fā)。
Application是應(yīng)用層,和以往事務(wù)腳本的Service是截然不同的,該層中并不做詳細(xì)的業(yè)務(wù)邏輯的封裝,而是創(chuàng)建Domian并調(diào)用Domain中的相應(yīng)方法完成業(yè)務(wù)邏輯處理,并調(diào)用Infrastructure完成Domain的持久化操作,該層需要負(fù)責(zé)事務(wù)的控制,保證數(shù)據(jù)的一致性。
Domain層是核心領(lǐng)域?qū)?/strong>,核心的業(yè)務(wù)邏輯應(yīng)該以Domain為對(duì)象進(jìn)行分類封裝,Domain的劃分以業(yè)務(wù)為基準(zhǔn),Domain層在技術(shù)層面的建模通用技巧在下面會(huì)做詳細(xì)介紹,該層只能向下調(diào)用Infrastructure完成自身模型的初始化和持久化。
Infrastructure層類似于以往事務(wù)腳本的Dao層,以往的面向事務(wù)腳本的編程中,以數(shù)據(jù)表為主,所有的事務(wù)腳本直指目的就是完成表的CURD,而DDD中以模型為核心,Infrastructure層是為了重建已有的Domain,并在退出時(shí)持久化內(nèi)存中的Domain對(duì)象。Infrastructure層不僅包含對(duì)關(guān)系數(shù)據(jù)庫(kù)的處理,還包括對(duì)分布式緩存處理、對(duì)外系統(tǒng)的接入(integration)以及分布式消息隊(duì)列的push操作。
一些常用的關(guān)鍵術(shù)語(yǔ):
- 實(shí)體 - Entity
- 值對(duì)象 - Value Objects
- 領(lǐng)域服務(wù) - Domain Services
- 領(lǐng)域事件 - Domain Events
- 模塊 - Modules
- 聚合 - Aggregate
- 資源庫(kù) - Repository
實(shí)體和值對(duì)象在代碼中皆表示為一個(gè)類(對(duì)象),從業(yè)務(wù)上來(lái)講也分別表示一個(gè)業(yè)務(wù)實(shí)體。但是兩者是有區(qū)別,實(shí)體是一個(gè)完整的具有生命周期的可以通過(guò)唯一標(biāo)識(shí)來(lái)識(shí)別東西的類(對(duì)象),而值對(duì)象則為了度量和描述領(lǐng)域中的一個(gè)屬性,將不同的相關(guān)屬性組合成一個(gè)概念整體的類(對(duì)象) 。?
例如,客戶可以認(rèn)為是一個(gè)實(shí)體,一個(gè)客戶就是具有生命周期的東西,具有唯一的標(biāo)識(shí)可以將A客戶和B客戶分開(唯一標(biāo)識(shí):身份證號(hào)碼),而這個(gè)客戶的地址(例如:廣州市/白云區(qū)/歐派軟件園)就應(yīng)該定義為一個(gè)值對(duì)象,當(dāng)我們定義好一個(gè)地址,它既可以是A客戶的地址也可以是B客戶的地址,當(dāng)我需要更新A客戶地址時(shí),可以直接銷毀A客戶關(guān)聯(lián)的地址對(duì)象然后重新創(chuàng)建一個(gè)地址對(duì)象關(guān)聯(lián)到A客戶上。
領(lǐng)域事件是由一個(gè)特定領(lǐng)域因?yàn)橐粋€(gè)用戶Command觸發(fā)的發(fā)生在過(guò)去的行為產(chǎn)生的事件,而這個(gè)事件是系統(tǒng)中其它部分感興趣的。
在現(xiàn)在的分布式環(huán)境下,業(yè)務(wù)系統(tǒng)與業(yè)務(wù)系統(tǒng)之間并不是割裂的,而消息絕對(duì)是系統(tǒng)之間耦合度最低,最健壯,最容易擴(kuò)展的一種通信機(jī)制。
領(lǐng)域服務(wù)和以往事務(wù)腳本的Service只能說(shuō)非常像,唯一不同的是,以往事務(wù)腳本的Service是自己全權(quán)負(fù)責(zé)業(yè)務(wù)邏輯,而領(lǐng)域服務(wù)不僅編寫業(yè)務(wù)邏輯,還會(huì)調(diào)用實(shí)體和值對(duì)象的方法來(lái)協(xié)調(diào)實(shí)體和值對(duì)象,從而避免實(shí)體和值對(duì)象的耦合。當(dāng)我們建模之后發(fā)現(xiàn)有些業(yè)務(wù)既不單單屬于A領(lǐng)域?qū)ο?,又不單單屬于B領(lǐng)域?qū)ο?,我們可以那么我們可以抽象出一個(gè)Service來(lái)完成此項(xiàng)業(yè)務(wù),那么這個(gè)類就是領(lǐng)域服務(wù)。
資源庫(kù)也叫數(shù)據(jù)倉(cāng)庫(kù),主要是完成領(lǐng)域?qū)ο蟮闹亟ㄒ约皩?duì)象的持久化操作。資源庫(kù)的設(shè)計(jì)主要是為了調(diào)用基礎(chǔ)設(shè)施層來(lái)完成表的CURD、緩存的操作以及外系統(tǒng)接口的調(diào)用。
?
2.2、領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)
這里引用阿里團(tuán)隊(duì)開源的可樂(Cola)領(lǐng)域設(shè)計(jì)架構(gòu)示圖,如下圖所示:
DDD最核心思想是設(shè)計(jì)內(nèi)部六邊形結(jié)構(gòu)。
從內(nèi)往外看,最內(nèi)層也是最核心就是Domain層(包括:Domain model和Domain Service);
第二層是Application層(包括:Application Service);
第三層業(yè)務(wù)邏輯層(Business Logic也可以叫基礎(chǔ)設(shè)施層)主對(duì)外提供服務(wù)接口,對(duì)內(nèi)解決基礎(chǔ)服務(wù)包裝構(gòu)建。
?
2.3、分層調(diào)用示圖
DDD分層架構(gòu)設(shè)計(jì)之調(diào)用示圖
?
?
上圖已經(jīng)非常清楚展示了分層設(shè)計(jì)以及各層調(diào)用關(guān)系,一圖勝千言,大家認(rèn)真詳細(xì)看圖就可以理解了。
2.4、命令式讀寫分離
操作命令和對(duì)象抽象,通過(guò)命令與查詢分離設(shè)計(jì)方式實(shí)現(xiàn)服務(wù)接口調(diào)用。
CQRS(Command Query Separation,命令查詢分離) 基本思想在于,任何一個(gè)對(duì)象的方法可以分為兩大類:
- 命令(Command):不返回任何結(jié)果(void),但會(huì)改變對(duì)象的狀態(tài)
- 查詢(Query):返回結(jié)果,但是不會(huì)改變對(duì)象的狀態(tài),對(duì)系統(tǒng)沒有副作用
?
2.5、擴(kuò)展點(diǎn)
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)另外一個(gè)重要的地方是擴(kuò)展點(diǎn)。
可樂包在擴(kuò)展點(diǎn)功能設(shè)計(jì)中引入的概念:業(yè)務(wù)、用例、場(chǎng)景。
- 業(yè)務(wù)(Business):就是一個(gè)自負(fù)盈虧的財(cái)務(wù)主體,比如tmall、淘寶和零售通就是三個(gè)不同的業(yè)務(wù)。
- 用例(Use Case):描述了用戶和系統(tǒng)之間的互動(dòng),每個(gè)用例提供了一個(gè)或多個(gè)場(chǎng)景。比如,支付訂單就是一個(gè)典型的用例。
- 場(chǎng)景(Scenario):場(chǎng)景也被稱為用例的實(shí)例,包括用例所有的可能情況(正常的和異常的)。比如對(duì)于“訂單支付”這個(gè)用例,就有“可以使用花唄”,“支付寶余額不足”,“銀行賬戶余額不足”等多個(gè)場(chǎng)景。
?
?
例如我們要實(shí)現(xiàn)右圖中所展示的擴(kuò)展:在tmall這個(gè)業(yè)務(wù)下——下單用例——88VIP場(chǎng)景——用戶身份校驗(yàn)進(jìn)行擴(kuò)展,我們只需要聲明一個(gè)如下的擴(kuò)展實(shí)現(xiàn)(Extension)就可以了。
?
?
3、領(lǐng)域建模
領(lǐng)域架構(gòu)設(shè)計(jì)并不復(fù)雜,復(fù)雜的業(yè)務(wù)需求怎么轉(zhuǎn)化為領(lǐng)域模型,這也是最難的地方,需要業(yè)務(wù)梳理足夠清晰,同時(shí)需要有足夠抽象能力和領(lǐng)域劃分。
領(lǐng)域建模基于業(yè)務(wù)的建模的基礎(chǔ)上,需要花較重的時(shí)間比例在梳理業(yè)務(wù)和模型設(shè)計(jì)上面;而同時(shí)領(lǐng)域建沒有通用的統(tǒng)一結(jié)構(gòu)設(shè)計(jì),得看具體業(yè)務(wù)具體分析。下面舉個(gè)例子說(shuō)明一下領(lǐng)域建模方法。希望能夠各位得到一點(diǎn)啟發(fā)。
領(lǐng)域模型不是簡(jiǎn)單POJO或數(shù)據(jù)實(shí)體對(duì)象,他還有行為和狀態(tài),主要體現(xiàn)在事件機(jī)制、值對(duì)象上面。
這里先不深入討論,先拋個(gè)影子,后面抽空補(bǔ)上更詳細(xì)的說(shuō)明。
?
?
4、領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)實(shí)現(xiàn)
?
4.1、分層設(shè)計(jì)
各層分工:
- Application層主要負(fù)責(zé)獲取輸入,組裝context,做輸入校驗(yàn),發(fā)送消息給領(lǐng)域?qū)幼鰳I(yè)務(wù)處理,監(jiān)聽確認(rèn)消息;
- Domain層主要是通過(guò)領(lǐng)域服務(wù)(Domain Service),領(lǐng)域?qū)ο螅―omain Object)的交互,對(duì)上層提供業(yè)務(wù)邏輯的處理,然后調(diào)用下層Repository做持久化處理;
- Infrastructure層主要包含Repository,Config,Common和message,Repository負(fù)責(zé)數(shù)據(jù)的CRUD操作,數(shù)據(jù)來(lái)源可以是MySQL,NoSql,Search,甚至是HSF等;
- Config負(fù)責(zé)應(yīng)用的配置;Common是一寫工具類;負(fù)責(zé)message通信的也應(yīng)該放在這一層。
4.2、建立二方庫(kù)組件
二方庫(kù)組件:
api:存放的是應(yīng)用對(duì)外的接口。
dto.domainmodel:用來(lái)做數(shù)據(jù)傳輸?shù)妮p量級(jí)領(lǐng)域?qū)ο蟆?/span>
dto.domainevent: 用來(lái)做數(shù)據(jù)傳輸?shù)念I(lǐng)域事件。
Application里的組件:
service:接口實(shí)現(xiàn)的facade,沒有業(yè)務(wù)邏輯,可以包含對(duì)不同終端的adapter。
eventhandler:處理領(lǐng)域事件,包括本域的和外域的。
executor:用來(lái)處理(Command)和查詢(Query)。
interceptor:COLA提供的對(duì)所有請(qǐng)求的AOP處理機(jī)制。
Domain里的組件:
domain:領(lǐng)域?qū)嶓w,允許繼承domainmodel。
domainservice: 領(lǐng)域服務(wù),用來(lái)提供更粗粒度的領(lǐng)域能力。
gateway:對(duì)外依賴的網(wǎng)關(guān)接口,包括存儲(chǔ)、RPC、Search等。
?
大部分情況下,二方庫(kù)的確是用來(lái)定義服務(wù)接口和數(shù)據(jù)協(xié)議的。但是二方庫(kù)區(qū)別于JSON的地方是它不僅僅是協(xié)議,它還是一個(gè)Java對(duì)象,一個(gè)Jar包。既然是Java對(duì)象,就意味著我們就有可能讓DTO升級(jí)為一個(gè)Domain Model(有數(shù)據(jù),有行為,有繼承) 。
Domain Model用到的所有數(shù)據(jù)如果都是自恰的,那么這些計(jì)算是不需要借助外面的輔助,自己就能完成。比如CustomerCO擁有身份證號(hào)碼,那么判斷當(dāng)前這個(gè)CustomerCO的性別、年齡等信息時(shí)是依靠自己(身份證號(hào)碼)就能完成的。是一種共享內(nèi)核, CustomerCO把自己領(lǐng)域的知識(shí)(語(yǔ)言、數(shù)據(jù)和行為)通過(guò)二方庫(kù)暴露出去了,假如有100個(gè)應(yīng)用需要獲取性別或年齡時(shí)做判斷,客戶端就不需要自己實(shí)現(xiàn)了。
4.3、上下文
應(yīng)用不同Bounded Context之間的協(xié)作,要充分利用好二方庫(kù)的橋梁作用。其協(xié)作方式如下圖所示:
?
4.4、主要組件依賴關(guān)系
依賴關(guān)系示例,如以Customer會(huì)員業(yè)務(wù)對(duì)象舉例如下圖所示
?
4.5、代碼實(shí)現(xiàn)
下面以我們系統(tǒng)中客戶中心會(huì)員體系設(shè)計(jì)為示例介紹一下怎么使用DDD方法實(shí)現(xiàn)代碼。
對(duì)外接口代碼示例
參數(shù)校驗(yàn)
?
API接口服務(wù)層示例
命令總線示例
?
全局異常捕獲示例代碼
?
4.6、舊項(xiàng)目神碼改造
對(duì)于舊項(xiàng)目代碼,大家都非常頭痛,舊系統(tǒng)經(jīng)常出現(xiàn)一些奇怪的問題,其實(shí)就是不穩(wěn)定引起的。舊系統(tǒng)代碼都是神碼具多,極難維護(hù),誰(shuí)都不愿意接手,后來(lái)經(jīng)過(guò)我們重大討論決策后決定對(duì)舊項(xiàng)目進(jìn)行重構(gòu)。
重構(gòu)說(shuō)起來(lái)簡(jiǎn)單,實(shí)施起來(lái)卻是非常的頭大,畢竟不是簡(jiǎn)單的系統(tǒng),同時(shí)也是公司里面業(yè)務(wù)最重要的業(yè)務(wù)系統(tǒng)(訂單+CRM集團(tuán)業(yè)績(jī)的入口保障),不容許出錯(cuò);而舊代碼庫(kù)非常龐大,規(guī)模接近百萬(wàn)行。代碼質(zhì)量不堪回首,都是地獄級(jí)別。
我們痛定思痛,決定對(duì)它動(dòng)用外科手術(shù),當(dāng)時(shí)頂著巨大壓力說(shuō)服大boss同意,游說(shuō)業(yè)務(wù)、產(chǎn)品、測(cè)試、開發(fā)各方一起協(xié)作。在保證業(yè)務(wù)規(guī)則和邏輯前提,進(jìn)行重大的重構(gòu)設(shè)計(jì),主要也是采用DDD領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)。
這次重構(gòu)經(jīng)歷了近6個(gè)月,頂著各方巨大的壓力。但經(jīng)過(guò)幾次重大升級(jí)發(fā)布,終于徹底改頭換面,神碼級(jí)舊系統(tǒng)完成改造。
1個(gè)java類近萬(wàn)行神碼經(jīng)過(guò)重構(gòu)改造,確切的說(shuō)應(yīng)該重設(shè)計(jì)重新寫代碼結(jié)構(gòu),總結(jié)一下有幾條寶貴經(jīng)驗(yàn):
- 根據(jù)DDD讀寫分離設(shè)計(jì),寫入通過(guò)實(shí)現(xiàn)不同的Command執(zhí)行器,查詢實(shí)現(xiàn)不同的查詢執(zhí)行器。
- 根據(jù)不同業(yè)務(wù)場(chǎng)景增加多個(gè)不同的擴(kuò)展點(diǎn),有效地解耦業(yè)務(wù)。
- 復(fù)雜的業(yè)務(wù)規(guī)則引入規(guī)則引擎,把業(yè)務(wù)規(guī)則抽象成一條條可動(dòng)態(tài)編輯和可維護(hù)規(guī)則,并實(shí)現(xiàn)動(dòng)態(tài)加載和配置,而不是硬代碼。
經(jīng)過(guò)一頓猛烈改造設(shè)計(jì),新版本的代碼清爽多了!
把一萬(wàn)行的代碼直接搞成了360行左右!
舊代碼:
消滅了18層地獄式代碼
改造后新代碼結(jié)構(gòu):
查詢統(tǒng)一執(zhí)行器如下圖所示:
擴(kuò)展點(diǎn)抽象查詢器
擴(kuò)展點(diǎn)執(zhí)行器抽象類
經(jīng)過(guò)一番猛操作改造之后,代碼簡(jiǎn)潔很多,變得可讀可維護(hù),從此世界清新很多,維護(hù)代價(jià)極大的變小!
?
5、問題與總結(jié)
- 對(duì)于簡(jiǎn)單的業(yè)務(wù)場(chǎng)景,使用事務(wù)腳本,其優(yōu)點(diǎn)是簡(jiǎn)單、直觀、易上手;
- 對(duì)于復(fù)雜的業(yè)務(wù)場(chǎng)景,比較有效的治理辦法就是領(lǐng)域建模,在封裝業(yè)務(wù)邏輯的同時(shí),提升了對(duì)象的內(nèi)聚性和重用性,因?yàn)槭褂昧送ㄓ谜Z(yǔ)言,使得隱藏的業(yè)務(wù)邏輯得到顯性化表達(dá),使得復(fù)雜性治理成為可能。
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)結(jié)構(gòu)非常清晰,適合復(fù)雜業(yè)務(wù)場(chǎng)景,代碼結(jié)構(gòu)清晰,代碼維護(hù)代會(huì)低很多。當(dāng)然也需要業(yè)務(wù)建模與抽象能力,與傳統(tǒng)面象數(shù)據(jù)開發(fā)方法有本質(zhì)的不同,需要轉(zhuǎn)變開發(fā)者的思維方法,對(duì)團(tuán)隊(duì)有一定學(xué)習(xí)成本。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-482315.html
- 有開發(fā)卓越軟件的激情和毅力
- 渴望學(xué)習(xí)和進(jìn)步
- 有能力理解軟件模式,并懂得如何應(yīng)用這些模式?
- 有發(fā)掘不同的設(shè)計(jì)方法能力和耐性
- 勇于改變現(xiàn)狀
- 希望編寫出更好的代碼
?
到了這里,關(guān)于領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)DDD實(shí)際項(xiàng)目落地最佳實(shí)踐的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!