學(xué)習(xí)資源來自Gitchat上張逸的《領(lǐng)域驅(qū)動設(shè)計實踐》
不管是因為規(guī)模與結(jié)構(gòu)制造的理解力障礙,還是因為變化帶來的預(yù)測能力問題冬耿,最終的決定因素還是因為需求闲擦。需求分為業(yè)務(wù)需求與質(zhì)量屬性需求焕济,因而需求引起的復(fù)雜度可以分為兩個方面:
質(zhì)量屬性需求帶來的技術(shù)復(fù)雜度
諸如安全纷妆、高性能、高并發(fā)晴弃、高可用性等需求掩幢,為軟件設(shè)計帶來了極大的挑戰(zhàn),讓人痛苦的是這些因素彼此之間可能又互相矛盾上鞠、互相影響际邻。例如為了滿足系統(tǒng)的高并發(fā)訪問,我們需要對應(yīng)用服務(wù)進行物理分解芍阎,通過橫向增加更多的機器來分散訪問負載世曾;同時,還可以將一個同步的訪問請求拆分為多級步驟的異步請求谴咸,再通過引入消息中間件對這些請求進行整合和分散處理轮听。這種分離一方面增加了系統(tǒng)架構(gòu)的復(fù)雜性,另一方面也因為引入了更多的資源岭佳,使得系統(tǒng)的高可用面臨挑戰(zhàn)血巍,并增加了維護數(shù)據(jù)一致性的難度。業(yè)務(wù)需求帶來的業(yè)務(wù)復(fù)雜度
這種復(fù)雜度往往會隨著需求規(guī)模的增大而增加驼唱。由于需求不可能做到完全獨立藻茂,一旦規(guī)模擴大到一定程度,不僅產(chǎn)生了功能數(shù)量的增加玫恳,還會因為功能互相之間的依賴與影響使得這種復(fù)雜度產(chǎn)生疊加辨赐,進而影響到整個系統(tǒng)的質(zhì)量屬性,比如系統(tǒng)的可維護性與可擴展性京办。另外還會因為溝通不暢掀序、客戶需求不清晰等多種局外因素而帶來的需求變更和修改。如果不能很好地控制這種變更惭婿,則可能會因為多次修改而導(dǎo)致業(yè)務(wù)邏輯糾纏不清不恭,系統(tǒng)可能開始慢慢腐爛而變得不可維護,最終形成一個“大泥球”系統(tǒng)财饥。
領(lǐng)域驅(qū)動設(shè)計通過分層架構(gòu)與六邊形架構(gòu)來確保業(yè)務(wù)邏輯與技術(shù)實現(xiàn)的隔離换吧。
分層架構(gòu)
分層架構(gòu)依照“關(guān)注點分離”原則,將屬于業(yè)務(wù)邏輯的關(guān)注點放到領(lǐng)域?qū)樱―omain Layer)中钥星,而將支撐業(yè)務(wù)邏輯的技術(shù)實現(xiàn)放到基礎(chǔ)設(shè)施層(Infrastructure Layer)中沾瓦。同時,領(lǐng)域驅(qū)動設(shè)計又頗具創(chuàng)見的引入了應(yīng)用層(Application Layer)谦炒,應(yīng)用層扮演了雙重角色贯莺。一方面它作為業(yè)務(wù)邏輯的外觀(Facade),暴露了能夠體現(xiàn)業(yè)務(wù)用例的應(yīng)用服務(wù)接口宁改;另一方面它又是業(yè)務(wù)邏輯與技術(shù)實現(xiàn)的粘合劑缕探,實現(xiàn)二者之間的協(xié)作。
六邊形架構(gòu)
六邊形架構(gòu)則以“內(nèi)外分離”的方式还蹲,更加清晰地勾勒出了業(yè)務(wù)邏輯與技術(shù)實現(xiàn)的邊界爹耗,且將業(yè)務(wù)邏輯放在了架構(gòu)的核心位置。
體現(xiàn)業(yè)務(wù)邏輯的應(yīng)用層與領(lǐng)域?qū)犹幱诹呅渭軜?gòu)的內(nèi)核谜喊,并通過內(nèi)部的六邊形邊界與基礎(chǔ)設(shè)施的模塊隔離開鲸沮。當我們在進行軟件開發(fā)時,只要恪守架構(gòu)上的六邊形邊界锅论,則不會讓技術(shù)實現(xiàn)的復(fù)雜度污染到業(yè)務(wù)邏輯讼溺,保證了領(lǐng)域的整潔。邊界還隔離了變化產(chǎn)生的影響最易。如果我們在領(lǐng)域?qū)踊驊?yīng)用層抽象了技術(shù)實現(xiàn)的接口怒坯,再通過依賴注入將控制的方向倒轉(zhuǎn),業(yè)務(wù)內(nèi)核就會變得更加的穩(wěn)定藻懒,不會因為技術(shù)選型或其他決策的變化而導(dǎo)致領(lǐng)域代碼的修改剔猿。
不了解六邊形架構(gòu)可以看看這篇
案例:隔離數(shù)據(jù)庫與緩存的訪問
領(lǐng)域驅(qū)動設(shè)計建議我們在領(lǐng)域?qū)咏①Y源庫(Repository)的抽象,它的實現(xiàn)則被放在基礎(chǔ)設(shè)施層嬉荆,然后采用依賴注入在運行時為業(yè)務(wù)邏輯注入具體的資源庫實現(xiàn)归敬。那么,對于處于內(nèi)核之外的 Repositories 模塊而言,即使選擇從 MyBatis 遷移到 Spring Data汪茧,領(lǐng)域代碼都不會受到牽連:
package practiceddd.ecommerce.ordercontext.application;
@Transaction
public class OrderAppService {
@Service
private PlaceOrderService placeOrder;
public void placeOrder(Identity buyerId, List<OrderItem> items, ShippingAddress shipping, BillingAddress billing) {
try {
palceOrder.execute(buyerId, items, shipping, billing);
} catch (OrderRepositoryException | InvalidOrderException | Exception ex) {
ex.printStackTrace();
logger.error(ex.getMessage());
}
}
}
package practiceddd.ecommerce.ordercontext.domain;
public interface OrderRepository {
List<Order> forBuyerId(Identity buyerId);
void add(Order order);
}
public class PlaceOrderService {
@Repository
private OrderRepository orderRepository;
@Service
private OrderValidator orderValidator;
public void execute(Identity buyerId, List<OrderItem> items, ShippingAddress shipping, BillingAddress billing) {
Order order = Order.create(buyerId, items, shipping, billing);
if (orderValidator.isValid(order)) {
orderRepository.add(order);
} else {
throw new InvalidOrderException(String.format("the order which placed by buyer with %s is invalid.", buyerId));
}
}
}
package practiceddd.ecommerce.ordercontext.infrastructure.db;
public class OrderMybatisRepository implements OrderRepository {}
public class OrderSprintDataRepository implements OrderRepository {}
對緩存的處理可以如法炮制椅亚,但它與資源庫稍有不同之處。資源庫作為訪問領(lǐng)域模型對象的入口舱污,其本身提供的增刪改查功能呀舔,在抽象層面上是對領(lǐng)域資源的訪問。因此在領(lǐng)域驅(qū)動設(shè)計中扩灯,我們通常將資源庫的抽象歸屬到領(lǐng)域?qū)用睦怠彺娴脑L問則不相同,它的邏輯就是對 key 和 value 的操作珠插,與具體的領(lǐng)域無關(guān)惧磺。倘若要為緩存的訪問方法定義抽象接口,在分層的歸屬上應(yīng)該屬于應(yīng)用層捻撑,至于實現(xiàn)則屬于技術(shù)范疇磨隘,應(yīng)該放在基礎(chǔ)設(shè)施層:
package practiceddd.ecommerce.ordercontext.application;
@Transaction
public class OrderAppService {
@Repository
private OrderRepository orderRepository;
@Service
private CacheClient<List<Order>> cacheClient;
public List<Order> findBy(Identity buyerId) {
Optional<List<Order>> cachedOrders = cacheClient.get(buyerId.value());
if (cachedOrders.isPresent()) {
return orders.get();
}
List<Order> orders = orderRepository.forBuyerId(buyerId);
if (!orders.isEmpty()) {
cacheClient.put(buyerId.value(), orders);
}
return orders;
}
}
package practiceddd.ecommerce.ordercontext.application.cache;
public interface CacheClient<T> {
Optional<T> get(String key);
void put(String key, T value);
}
package practiceddd.ecommerce.ordercontext.infrastructure.cache;
public class RedisCacheClient<T> implements CacheClient<T> {}
限界上下文的分而治之
上面分析緩存訪問接口的歸屬時,我們將接口放在了系統(tǒng)的應(yīng)用層布讹。從層次的職責(zé)來看琳拭,這樣的設(shè)計是合理的,但它卻使得系統(tǒng)的應(yīng)用層變得更加臃腫描验,職責(zé)也變得不夠單一了白嘁。這是分層架構(gòu)與六邊形架構(gòu)的局限所在,因為這兩種架構(gòu)模式僅僅體現(xiàn)了軟件系統(tǒng)的邏輯劃分膘流。倘若我們將一個軟件系統(tǒng)視為一個縱橫交錯的魔方絮缅,前述的邏輯劃分僅僅是一種水平方向的劃分;至于垂直方向的劃分呼股,則是面向垂直業(yè)務(wù)的切割耕魄。這種方式更利于控制軟件系統(tǒng)的規(guī)模,將一個龐大的軟件系統(tǒng)劃分為松散耦合的多個小系統(tǒng)的組合彭谁。
針對前述案例吸奴,我們可以將緩存視為一個獨立的子系統(tǒng),它同樣擁有自己的業(yè)務(wù)邏輯和技術(shù)實現(xiàn)缠局,因而也可以為其建立屬于緩存領(lǐng)域的分層架構(gòu)则奥。在架構(gòu)的宏觀視角,這個緩存子系統(tǒng)與訂單子系統(tǒng)處于同一個抽象層次狭园。這一概念在領(lǐng)域驅(qū)動設(shè)計中读处,被稱之為限界上下文(Bounded Context)。
針對龐大而復(fù)雜的問題域唱矛,限界上下文采用了“分而治之”的思想對問題域進行了分解罚舱,有效地控制了問題域的規(guī)模井辜,進而控制了整個系統(tǒng)的規(guī)模。一旦規(guī)模減小管闷,無論業(yè)務(wù)復(fù)雜度還是技術(shù)復(fù)雜度粥脚,都會得到顯著的降低,在對領(lǐng)域進行分析以及建模時渐北,也能變得更加容易阿逃。限界上下文對整個系統(tǒng)進行了劃分铭拧,在將一個大系統(tǒng)拆分為一個個小系統(tǒng)后赃蛛,我們再利用分層架構(gòu)與六邊形架構(gòu)思想對其進行邏輯分層,以確保業(yè)務(wù)邏輯與技術(shù)實現(xiàn)的隔離搀菩,其設(shè)計會變得更易于把控呕臂,系統(tǒng)的架構(gòu)也會變得更加清晰。
案例:限界上下文幫助架構(gòu)的演進
國際報稅系統(tǒng)是為跨國公司的駐外出差雇員(系統(tǒng)中被稱之為 Assignee)提供方便一體化的稅收信息填報平臺肪跋∑缃客戶是一家會計師事務(wù)所,該事務(wù)所的專員(Admin)通過該平臺可以收集雇員提交的報稅信息州既,然后對這些信息進行稅務(wù)評審谜洽。如果 Admin 評審出信息有問題,則返回給 Assignee 重新修改和填報吴叶。一旦信息確認無誤阐虚,則進行稅收分析和計算,并獲得最終的稅務(wù)報告提交給當?shù)卣约肮蛦T本人蚌卤。
系統(tǒng)主要涉及的功能包括:
- 駐外出差雇員的薪酬與福利
- 稅收計劃與合規(guī)評審
- 對稅收評審的分配管理
- 稅收策略設(shè)計與評審
- 對駐外出差雇員的稅收合規(guī)評審
- 全球的 Visa 服務(wù)
主要涉及的用戶角色包括:
- Assignee:駐外出差雇員
- Admin:稅務(wù)專員
- Client:出差雇員的雇主
在早期的架構(gòu)設(shè)計時实束,架構(gòu)師并沒有對整個系統(tǒng)的問題域進行拆分,而是基于用戶角色對系統(tǒng)進行了簡單粗暴的劃分逊彭,分為了兩個相對獨立的子系統(tǒng):Frond End 與 Office End咸灿,這兩個子系統(tǒng)單獨部署,分別面向 Assignee 與 Admin侮叮。系統(tǒng)之間的集成則通過消息和 Web Service 進行通信避矢。兩個子系統(tǒng)的開發(fā)分屬不同的團隊,F(xiàn)rond End 由美國的團隊負責(zé)開發(fā)與維護囊榜,而 Office End 則由印度的團隊負責(zé)审胸。整個架構(gòu)如下圖所示:
采用這種架構(gòu)面臨的問題如下:
- 龐大的代碼庫:整個 Front End 和 Office End 都沒有做物理分解,隨著需求的增多锦聊,代碼庫會變得格外龐大歹嘹。
- 分散的邏輯:系統(tǒng)分解的邊界是不合理的,沒有按照業(yè)務(wù)分解孔庭,而是按照用戶的角色進行分解尺上,因而導(dǎo)致大量相似的邏輯分散在兩個不同的子系統(tǒng)中材蛛。
- 重復(fù)的數(shù)據(jù):兩個子系統(tǒng)中存在業(yè)務(wù)重疊,因而也導(dǎo)致了部分數(shù)據(jù)的重復(fù)怎抛。
- 復(fù)雜的集成:Front End 與 Office End 因為某些相關(guān)的業(yè)務(wù)需要彼此通信卑吭,這種集成關(guān)系是雙向的,且由兩個不同的團隊開發(fā)马绝,導(dǎo)致集成的接口混亂豆赏,消息協(xié)議多樣化。
- 知識未形成共享:兩個團隊完全獨立開發(fā)富稻,沒有掌握端對端的整體流程掷邦,團隊之間沒有形成知識的共享。
- 無法應(yīng)對需求變化:新增需求包括對國際旅游椭赋、Visa 的支持抚岗,現(xiàn)有系統(tǒng)的架構(gòu)無法很好地支持這些變化。
采用領(lǐng)域驅(qū)動設(shè)計哪怔,我們將架構(gòu)的主要關(guān)注點放在了“領(lǐng)域”宣蔚,與客戶進行了充分的需求溝通和交流。通過分析已有系統(tǒng)的問題域认境,結(jié)合客戶提出的新需求胚委,對整個問題域進行了梳理,并利用限界上下文對問題域進行了分解叉信,獲得了如下限界上下文:
- Account Management:管理用戶的身份與配置信息亩冬;
- Calendar Management:管理用戶的日程與旅行足跡。
之后茉盏,客戶希望能改進需求鉴未,做到全球范圍內(nèi)的工作指派與管理,目的在于提高公司的運營效率鸠姨。通過對領(lǐng)域的分析铜秆,我們又識別出兩個限界上下文。在原有的系統(tǒng)架構(gòu)中讶迁,這兩個限界上下文同時處于 Front End 與 Office End 之中连茧,屬于重復(fù)開發(fā)的業(yè)務(wù)邏輯:
- Work Record Management:實現(xiàn)工作的分配與任務(wù)的跟蹤;
- File Sharing:目的是實現(xiàn)客戶與會計師事務(wù)所之間的文件交換巍糯。
隨著我們對領(lǐng)域知識的逐漸深入理解與分析啸驯,又隨之識別出如下限界上下文:
- Consent:管理合法的遵守法規(guī)的狀態(tài);
- Notification:管理系統(tǒng)與客戶之間的交流祟峦;
- Questionnaire:對問卷調(diào)查的數(shù)據(jù)收集罚斗。
Questionnaire:對問卷調(diào)查的數(shù)據(jù)收集。
這個領(lǐng)域分析的過程實際上就是通過對領(lǐng)域的分析而引入限界上下文對問題域進行分解宅楞,通過降低規(guī)模的方式來降低問題域的復(fù)雜度针姿;同時袱吆,通過為模型確定清晰的邊界,使得系統(tǒng)的結(jié)構(gòu)變得更加的清晰距淫,從而保證了領(lǐng)域邏輯的一致性绞绒。一旦確定了清晰的領(lǐng)域模型,就能夠幫助我們更加容易地發(fā)現(xiàn)系統(tǒng)的可重用點與可擴展點榕暇,并遵循“高內(nèi)聚蓬衡、松耦合”的原則對系統(tǒng)職責(zé)進行合理分配,再輔以分層架構(gòu)以劃分邏輯邊界彤枢,如下圖所示:
我們將識別出來的限界上下文定義為微服務(wù)狰晚,并對外公開 REST 服務(wù)接口。UI Applications 是一個薄薄的展現(xiàn)層堂污,它會調(diào)用后端的 RESTful 服務(wù)家肯,也使得服務(wù)在保證接口不變的前提下能夠單獨演化龄砰。每個服務(wù)都是獨立的盟猖,可以單獨部署,因而可以針對服務(wù)建立單獨的代碼庫和對應(yīng)的特性團隊(Feature Team)换棚。服務(wù)的重用性和可擴展性也有了更好的保障式镐,服務(wù)與 UI 之間的集成變得更簡單,整個架構(gòu)會更加清晰固蚤。
個人寄語:學(xué)到現(xiàn)階段娘汞,用自己的白話做點總結(jié):領(lǐng)域驅(qū)動設(shè)計的初衷是解決軟件復(fù)雜度問題,復(fù)雜度主要由規(guī)模夕玩、結(jié)構(gòu)你弦、變化造成,其中變化是無處不在的是整個開發(fā)過程中都要注意并尋求一個成本與好處的平衡燎孟;識別限界上下文分拆分微服務(wù)分而治之可以解決規(guī)模問題禽作;分層架構(gòu)+六邊形架構(gòu)解決拆分以后各自的結(jié)構(gòu)問題,分層的意思就是領(lǐng)域?qū)用嫦驑I(yè)務(wù)揩页,基礎(chǔ)設(shè)施層面向技術(shù)旷偿,應(yīng)用層粘合二者并提供接口。六邊形架構(gòu)主要強調(diào)通過接口與適配器的方式恪守對領(lǐng)域與基礎(chǔ)設(shè)施的隔離爆侣。
但是對第一篇描述的領(lǐng)域驅(qū)動設(shè)計的過程還不是很理解透徹萍程,強調(diào)建模的時候建立統(tǒng)一語言,然后識別限界上下午進行拆分我都能理解兔仰,但是分層架構(gòu)和六邊形架構(gòu)怎么會用在戰(zhàn)略階段呢茫负?不應(yīng)該主要是戰(zhàn)術(shù)階段嗎?