前一陣段時間鼠渺,陸陸續(xù)續(xù)給幾個客戶開展了領域驅動設計工作坊遵岩。在這個過程中你辣,遇到了客戶提出的各種各樣的問題,出現(xiàn)頻率比較高的是以下幾個尘执。稍微整理了一下舍哄,也加入了自己的一些思考。
如何區(qū)分實體和值對象誊锭?
這可以說是實施DDD時肯定會遇到的老大難問題表悬。基本上所有客戶都會有這個問題丧靡,實體和值對象如何區(qū)分蟆沫?有沒有區(qū)分原則?坦率說窘行,絕對的區(qū)分原則是沒有的饥追,相對普適的我總結了2條:
1.? 實體具有生命周期并且具有明確的業(yè)務含義的狀態(tài)變更
2.? 值對象沒有生命周期,只關心值本身罐盔,不關心值如何變化
其中但绕,原則1是最關鍵的,也是比較難理解的惶看。難就難在很多人搞不清楚怎么才算是具有生命周期并且具有狀態(tài)變更捏顺。很多人會誤把對數(shù)據(jù)庫的增刪改操作當作對象的生命周期與狀態(tài)變更。這兩者其實是完全不相關的兩個東西纬黎。當我們在識別實體和值對象的時候幅骄,我們還在領域建模階段,這個時候是不需要關注具體實現(xiàn)的本今,甚至我都不知道在持久層是否需要使用數(shù)據(jù)庫技術拆座。所以不能簡單地以是否有數(shù)據(jù)庫記錄的新增與刪除操作作為區(qū)分實體和值對象的原則,增刪不是業(yè)務狀態(tài)冠息。很多人建議以是否具有唯一的ID來區(qū)分二者挪凑,認為具有唯一ID就是實體。我個人認為也不是特別好逛艰,因為很多人并不能輕易區(qū)分這里所說的唯一ID與數(shù)據(jù)庫里面的主鍵躏碳。
那怎么才算有生命周期并且存在狀態(tài)變更呢? 最關鍵的是要緊緊抓住業(yè)務含義這個點散怖。 最典型的實體的例子是銀行賬戶菇绵。每個銀行賬戶都有業(yè)務意義上的狀態(tài)肄渗,例如active、inactive咬最、frozen翎嫡、closed等,并且可以在這些狀態(tài)之間變更切換永乌;同時钝的,在一個交易過程當中,賬戶的實例是一直存活著的铆遭,在交易中途并不能銷毀掉然后以另外一個賬戶實例取而代之,即使是一模一樣的實例也不行沿猜。以業(yè)內的術語來說枚荣,這個賬戶是stateful的。
既然實體是stateful的啼肩,值對象自然而然就是stateless橄妆。最簡單的一個例子就是賬戶里面的余額。余額當然會變化祈坠,但是我們并不關心其變化過程害碾,我們只關注當它變化后的值,在一筆交易的過程中赦拘,余額可以隨著業(yè)務邏輯變化慌随,也可以隨時讀取,只要保證變化后的值是正確就ok躺同。
也就是說阁猜,實體和值對象,二者都是對象蹋艺,不同的是剃袍,對于實體來說,我們關注的是對象本身捎谨;對于值對象來說民效,我們關注的是對象所承載的數(shù)據(jù)。
領域服務是什么涛救?
又是一個比較難理解的名詞畏邢。在復雜的業(yè)務中,有時候會遇到一些概念州叠,感覺既不是實體棵红,又不是值對象,它似乎承載了一些行為咧栗,但好像找不到承載所需的對象逆甜。聽起來比較虛虱肄,不過領域服務有一個很突出的特點,就是事件里面的名詞往往也可以作為一個動詞交煞,然后比較難找到一個合適的動詞來充當事件里面的動作咏窿。最典型的例子就是轉賬。轉賬就是一個典型的領域服務素征。轉賬既可以是名詞集嵌,也可以是動詞。當遇到這種情況時御毅,就需要打個心眼根欧,這個概念會不會是個領域服務。不過我們在使用領域服務的時候端蛆,需要特別小心凤粗,我一般在萬不得已的情況下才把一個對象識別為領域服務。因為領域服務過多今豆,容易導致系統(tǒng)模型貧血嫌拣。
如何識別聚合?
首先呆躲,第一步是要識別出聚合根异逐。因為聚合根本質上也是一個實體,所以一般來說我是在識別出實體后再識別聚合根插掂。那怎么識別聚合根呢灰瞻?我的做法是基于實體生命周期的長短來識別。在做事件風暴的時候燥筷,我們是可以分析對象在一個業(yè)務場景下的生命周期長短的箩祥。生命周期相對最長的實體就可以考慮作為這個場景下的聚合根,生命周期被覆蓋的實體作為這個聚合根的實體肆氓,從而形成一個聚合袍祖。舉一個曾經(jīng)實戰(zhàn)的例子。重大故障作戰(zhàn)室與重大故障作戰(zhàn)視頻會議被識別成為重大故障搶修場景下的兩個實體谢揪,二者都具有生命周期蕉陋。但是,重大故障作戰(zhàn)室的生命周期比重大故障作戰(zhàn)視頻會議長拨扶。先有重大故障作戰(zhàn)室凳鬓,后面才會啟動重大故障作戰(zhàn)視頻會議。并且視頻會議會先于作戰(zhàn)室結束患民。重大故障作戰(zhàn)室必須得在重大故障完全修復之后才能結束缩举。所以,在這個場景下,重大故障作戰(zhàn)室就是聚合根仅孩,重大故障作戰(zhàn)視頻會議就是實體托猩,二者形成一個聚合。
然而辽慕,在真實的業(yè)務場景中京腥,有時候會找不到一個實體的生命周期貫穿始終,或者是生命周期間無法完全重合溅蛉,導致不能比較生命周期長短的情況公浪。這個時候,可以考慮拆分兩個聚合根船侧,形成兩個聚合欠气。
場景只有CRUD怎么辦?
坦白說镜撩,如果只有CRUD晃琳,是不需要DDD建模的。但是一個復雜系統(tǒng)里面難免會存在某些小的業(yè)務場景只有CRUD琐鲁。這個時候,在這個場景下往往只有值對象人灼,沒有實體围段,更不用提聚合。這個時候有兩種處理方法投放,第一種是把值對象上升為實體奈泪,相當于無狀態(tài)實體,然后形成聚合灸芳。這往往是安全的涝桅,但反過來,把實體識別為值對象則會破壞模型烙样。第二種處理方法是把值對象融入別的業(yè)務相關性強的聚合冯遂。也有可能意味著業(yè)務場景的拆分不清晰,或者過細谒获,導致做事件風暴的時候場景粒度太小蛤肌。這個時候就需要從新review一下場景拆分的合理性,把過小的場景合并批狱,然后在做事件風暴剑梳。
子域與限界上下文的關系是什么叮雳?
這也是DDD的老大難問題了。基本上每個客戶都會問骂倘。子域是問題域,限屆上下文是解決方案域,子域的問題通過限界上下文解決,就是這個問題的答案砸王。但是這樣說了,客戶就更迷糊了僵芹。坦率說处硬,對這個問題的答案,以及基于這個答案的實施過程拇派,我個人都不覺得滿意荷辕。這個問題難以解釋清楚的根源在于,問題域與解決方案域有時候難以區(qū)分件豌。不同的人從不同的視角出發(fā)疮方,看到的問題與解決方案往往不一樣。
還是以重大故障的例子來說茧彤,從IT的視角出發(fā)骡显,重大故障作戰(zhàn)室就是IT要解決的問題,但是從業(yè)務的視角出發(fā)曾掂,重大故障作戰(zhàn)室就是業(yè)務賴以快速修復重大故障的解決方案惫谤。這個問題就會處于說不清道不明的尷尬境地。所以很多DDD的引導者珠洗,包括我自己在內溜歪,在實施的過程中,都活多或少的刻意回避這個問題许蓖,在得到了解決方案后再反推其想要解決的問題蝴猪。但是坦白講, 我個人對這種做法是持質疑態(tài)度的膊爪。我也曾經(jīng)嘗試使用名詞動詞等方法在事件風暴前從業(yè)務的角度梳理子域自阱,但由于試驗的次數(shù)不多,效果還是需要進一步驗證米酬。
如何劃分限界上下文沛豌?
劃分限界上下文,很重要的一個點是要識別并去除二義性赃额。那什么是二義性呢琼懊?舉個直觀的例子,女兒這個名詞爬早,在不同的家庭所代表的人是不一樣的哼丈。例如在我家,女兒代表的是我的2歲半的女兒筛严;但是在我岳母家醉旦,女兒代表的就是我妻子。我家和我岳母家,就是兩個典型的限界上下文车胡,當說“女兒”的時候檬输,必須明確告知是在哪個家庭的上下文。這就是二義性匈棘。而在實施DDD時丧慈,我一般會在識別出聚合后,進行聚合細化主卫,說白了就是查漏補缺逃默,把事件風暴中遺漏的對象補全,然后鑒別一下是否存在二義性簇搅,如果沒有的話完域,基本上就可以基于聚合之間的關系劃分限界上下文。
能不能基于聚合劃分微服務瘩将?
當然可以吟税。這樣劃分出來的微服務粒度更小,職責更清晰姿现。不好的地方是這種劃分方法可能會導致最后的微服務數(shù)量比較多肠仪,需要考慮微服務治理的成本。
如何識別API备典?
API有兩部分:處理第三方交互的API與處理服務間調用的API藤韵。第三方交互的API可以通過事件風暴中角色為人或者是第三方系統(tǒng)觸發(fā)的命令轉化而來,服務間調用的API可以通過事件風暴中系統(tǒng)內觸發(fā)的命令轉化而來熊经。但因為并不是所有的系統(tǒng)內觸發(fā)的命令都是服務間調用,所有這一步需要結合聚合來識別欲险。
如何進行類方法的設計镐依?
坦白來說,DDD不會設計到類內行為的設計天试。但是槐壳,事件風暴中系統(tǒng)內觸發(fā)的命令,可以轉化為部分類內行為喜每,也就是方法务唐,但不全,需要結合TDD等微觀方法來驅動代碼級的設計带兜。本人覺得DDD + TDD的方法論在銜接上是比較順暢的枫笛,也是值得一試的,宏觀的架構設計與微觀的代碼設計都覆蓋到了刚照。有興趣的同學可以嘗試一下刑巧。
基本上就是這些了。之后在DDD的實施過程中,如果遇到一些新的問題啊楚,會再寫文章分享吠冤。