作者 | 林寧
?領(lǐng)域到底是什么?
對領(lǐng)域這個(gè)詞的理解就是 DDD 入門的第一個(gè)難關(guān)。我們有時(shí)會被客戶問到,領(lǐng)域到底是什么?首先要清晰地知道領(lǐng)域是什么,才能劃分核心域、支撐域和通用域。換句話說,構(gòu)成領(lǐng)域的要素是什么呢?
領(lǐng)域是一個(gè)非常抽象的詞匯,我們需要先對其具象化。在英語的語境中,“Domain” 其實(shí)就是業(yè)務(wù),指的是現(xiàn)實(shí)生活中的各種事務(wù)。處理稅務(wù)、記賬、售貨記錄等,這些都是領(lǐng)域。
于是,我們給領(lǐng)域下了一個(gè)定義:
領(lǐng)域(Domain)是業(yè)務(wù)相關(guān)知識的集合。
通俗來說,領(lǐng)域就是業(yè)務(wù)知識。業(yè)務(wù)有一些內(nèi)在規(guī)則,存在專業(yè)性,比如財(cái)務(wù)、CRM、OA、電商等不同領(lǐng)域的業(yè)務(wù)規(guī)則不同。計(jì)算機(jī)只是業(yè)務(wù)規(guī)則的自動化。更加具體來說,構(gòu)成領(lǐng)域的要素就是特定的業(yè)務(wù)場景。
通過對業(yè)務(wù)的場景劃分,再對其分類,就是我們的子域。
- 核心域:那些對業(yè)務(wù)極其重要的場景,內(nèi)容社區(qū)應(yīng)用,就是提問、看帖、回復(fù)。
- 支撐域:那些對重要業(yè)務(wù)支持的場景,比如登錄、找回密碼等場景。
- 通用域:那些已經(jīng)成為相對獨(dú)立的支撐業(yè)務(wù)場景,比如實(shí)名驗(yàn)證、人機(jī)校驗(yàn)(以前是支撐,現(xiàn)在可以是通用)。
領(lǐng)域和上下文是什么關(guān)系?
如果領(lǐng)域的構(gòu)成要件是場景,上下文的構(gòu)成要件是模型,那么領(lǐng)域和上下文之間就沒有包含和被包含關(guān)系。
也不存在一個(gè)領(lǐng)域是否對應(yīng)多個(gè)上下文的關(guān)系。
他們構(gòu)成:上下文支撐領(lǐng)域的關(guān)系,領(lǐng)域?qū)С錾舷挛牡年P(guān)系。
DDD 軟件建模就是業(yè)務(wù)問題和解決方案之間的橋梁。領(lǐng)域是問題,設(shè)計(jì)出來的模型是解的一部分。因此,問題和解形如 x 和 f(x) 的關(guān)系,f = 軟件建模過程。
舉個(gè)例子來說,某個(gè)電商網(wǎng)站有多個(gè)渠道,零售、批發(fā)、企業(yè)采購等多個(gè)場景的業(yè)務(wù),這是他們的領(lǐng)域。對于研發(fā)工程師來說,他們會最終設(shè)計(jì)出訂單、商品等模型上下文,來支持這些領(lǐng)域。
聚合如何持久化?
聚合被賦予了兩個(gè)責(zé)任:
- 負(fù)責(zé)業(yè)務(wù)的一致性。?
- 負(fù)責(zé)數(shù)據(jù)的整體存儲。?
而其持久化是一個(gè)老大難問題。
關(guān)于業(yè)務(wù)的一致性,Eric DDD 給我們描述了一種理想情景。只要把業(yè)務(wù)一致的一組模型從數(shù)據(jù)庫中統(tǒng)一獲取到,對其做業(yè)務(wù)修改,然后再持久化回去,就可以避免業(yè)務(wù)的一致性被破壞。
業(yè)務(wù)的一致性可以這樣理解。我們有訂單和訂單項(xiàng),訂單的總價(jià)由訂單項(xiàng)計(jì)算得來。如果不長眼的程序員把訂單項(xiàng)直接修改了,而不更新訂單,就會帶來 bug。
但是,遺憾的是我們的內(nèi)存不是無限大,而且數(shù)據(jù)會在斷電后丟失。我們必須把數(shù)據(jù)從磁盤中讀取出來,而磁盤的訪問速度很慢。數(shù)據(jù)在磁盤中的組織形式使用了集合+關(guān)聯(lián)的方式存放,這是由于我們?yōu)榱私档蛿?shù)據(jù)冗余和方便查詢而不得已為之。這就是關(guān)系模型和對象模型的差異,而不得不采用一些技術(shù)方法轉(zhuǎn)換(ORM)。
而數(shù)據(jù)的整體存儲,讓聚合的持久化變得困難和性能低下。
一個(gè)簡單的道理是,我們只需要一個(gè)橘子,卻總想把橘子樹搬來搬去,雖然摘橘子需要通過橘子樹。
充血模型為什么不符合編程習(xí)慣?
充血模型已經(jīng)是很多 DDD 實(shí)踐者的潛在認(rèn)知,簡單來說就是把業(yè)務(wù)行為放到模型中。
這種做法看似滿足了面向?qū)ο蟮膶?shí)踐,但是在實(shí)際工作中,它并不方便,甚至有些別扭。在培訓(xùn)中,有學(xué)員找我們說,學(xué)了 DDD 之后不會寫代碼了,甚至忘記之前的代碼該如何編寫。
極端一點(diǎn)的例子,還會有人在聚合根中調(diào)用倉儲來實(shí)現(xiàn)聚合的存儲。這時(shí),他們發(fā)現(xiàn)矛盾在于 JPA 的存儲需要使用實(shí)體的類型信息,這時(shí)候便束手無策了。
在辯證唯物主義認(rèn)識論中,一個(gè)行為構(gòu)成的要件是:主體 + 動詞 + 客體。
在英語學(xué)習(xí)中,主謂賓結(jié)構(gòu)的主體是主語,客體就是賓語。甚至,主系表結(jié)構(gòu)也滿足這個(gè)道理。主語是主體,表語是主體的屬性,也是客體。
“太陽是圓的”。指的是,太陽的形狀是圓的。太陽是主體,“是” 作為邏輯謂詞可以認(rèn)為是動詞,“圓的”是太陽的外觀屬性。
合適的充血模型是給 “主體”充血,給客體貧血。特殊的情況是,當(dāng)一個(gè)模型操作它的屬性的時(shí)候,它也可以是主體。因此,給領(lǐng)域模型的操作能力,應(yīng)該僅限于操作自己的屬性。而領(lǐng)域模型的構(gòu)建、業(yè)務(wù)處理、持久化應(yīng)該交給主體來做。
一個(gè)有意思的悖論就是,不合適的充血模型就像讓一張桌子讓它自己把自己搬到樓上去,我們難描述這種行為。更好的做法不是去找一個(gè)搬運(yùn)工去搬這個(gè)桌子么,這次行為的主體就是搬運(yùn)工,客體就是這個(gè)桌子。
如何清晰的分層?
分層有兩個(gè)原則:
- 分層是有明確目的,沒有目的的分層會帶來額外的問題。?
- 分層需要考慮框架、庫的實(shí)現(xiàn),否則容易帶了 “千層餅架構(gòu)”。?
分層的目的為了隔離差異,沒有差異而進(jìn)行的分層就是浪費(fèi)。由于差異的出現(xiàn),每層所對應(yīng)的客體就發(fā)生了變化。
- 接入層:處理接入?yún)f(xié)議,這個(gè)時(shí)候還不知道領(lǐng)域信息,客體就是數(shù)據(jù)包的不同形式。?
- 應(yīng)用層:處理業(yè)務(wù)場景,比如用戶注冊、添加用戶、導(dǎo)入用戶等,客體就是一些用例對象。?
- 領(lǐng)域?qū)樱禾幚硗ㄓ妙I(lǐng)域能力,比如創(chuàng)建用戶,客體主要就是領(lǐng)域模型。?
- 技術(shù)設(shè)施層:為上層提供技術(shù)實(shí)現(xiàn),并不知道領(lǐng)域?qū)拥男畔?。比?JPA 是一種持久化實(shí)現(xiàn),需要從領(lǐng)域?qū)虞斎雽ο蟮念愋托畔⒑蛿?shù)據(jù)信息,客體就是泛型對象。?
多對多關(guān)系一般怎么處理?
多對多就是客體的含混不清,迷失了中間模型。
一個(gè)訂單可以有多個(gè)商品,實(shí)際上是一個(gè)訂單有多個(gè)訂單項(xiàng)。
- 一個(gè)用戶可以加入多個(gè)文檔協(xié)作編輯,實(shí)際上是一個(gè)用戶可以成為多個(gè)文檔的參與人。?
- 一個(gè)用戶可以加入多個(gè)用戶組,實(shí)際上是一個(gè)用戶可以在多個(gè)用戶組成為成員。?
辨明客體,可以讓代碼變得清晰、簡單、解耦。在現(xiàn)實(shí)中,一個(gè)老板可以有多個(gè)公司,一個(gè)公司也可以由多個(gè)老板投資。他們之間的多對多關(guān)系是通過 “股東” 這個(gè)客體來承載的。
在有限責(zé)任的公司中,股東身份和老板的個(gè)人身份(自然人)相互獨(dú)立,并得到司法支持。?