1. 角色
對象的背后,需要表達、體現(xiàn)出兩種概念:“它是什么”和“它能做什么”。
如果以一種『非面向?qū)ο蟮乃季S』使用 Java<small>(或其它面向?qū)ο缶幊陶Z言)</small>,那么你會發(fā)現(xiàn)你的系統(tǒng)中會充斥著大量的『貧血模型』。
注意,你『使用面向?qū)ο缶幊陶Z言』進行開發(fā),并不意味著你在進行『面向?qū)ο缶幊獭弧?/p>
貧血模型意味著這個對象只表達了『它是什么』,而沒有表達出『它能做什么』。
有人提出通過『角色』的概念去提煉對象『能做什么』。
在本質(zhì)上,『角色』體現(xiàn)的是一般化的、抽象的算法。角色沒有血肉,并不能做實際的事情,歸根結(jié)底工作還是落在對象的頭上,而對象本身還擔(dān)負著體現(xiàn)領(lǐng)域模型的責(zé)任。
『對象』這個統(tǒng)一的整體有兩種不同的模型,即『系統(tǒng)是什么』和『系統(tǒng)做什么』。
正因為最終用戶能把兩種視角合為一體,類的對象除了支持所屬類的成員函數(shù),還可以執(zhí)行所扮演角色的成員函數(shù),就好像那些函數(shù)屬于對象本身一樣。
Object 由多個 Role 組合而成,在不同的場景下扮演不同的角色,所以 Object 就是多角色對象。
Java 語言可以通過接口來實現(xiàn)對象的角色。而且,從 Java 8 開始接口可以提供默認。
舉個生活中的例子,人是多角色對象,由多個角色組合而成,不同的角色履行的職責(zé)不同:
- 作為父母:我們要給孩子講故事,陪他們玩游戲,哄它們睡覺;
- 作為子女:我們要孝敬父母,聽取他們的人生建議;
- 作為下屬:我們要服從上司的工作安排,并高質(zhì)量完成任務(wù);
- 作為上司:我們要安排下屬的工作,并進行培養(yǎng)和激勵;
- ...
人在特定的場景下,只能扮演特定的角色:
- 在孩子面前,我們是父母;
- 在父母面前,我們是子女;
- 在上司面前,我們是下屬;
- 在下屬面前,我們是上司;
- ...
2. 練習(xí)
-
有兩個人,一個人是富人 RichMan ,另一個人是窮人 PoorMan ,每個人都有一個唯一身份標識,基本數(shù)據(jù)有姓名、性別,電話號碼和啟動資金。
富人是老板 Boss ,家里有事時向公司請假,在公司時主要工作是分配任務(wù); 富人是父母 Parent ,要按時參加孩子的家長會,但參加之前要先向公司請假;富人是投資者 Investor ,經(jīng)常炒股;窮人是員工 Employee ,家里有事時向公司請假,周末外場有故障時要在公司加班;窮人是父母 Parent ,要按時參加孩子的家長會,但參加之前要先向公司請假;窮人是廚師 Cooker ,會炒菜。
-
每個人都應(yīng)該申請一個賬戶 Account ,然后持有 AccountId ,可以通過 AccountId 訪問賬戶完成轉(zhuǎn)賬或查詢余額等業(yè)務(wù)。
人與人之間的轉(zhuǎn)賬轉(zhuǎn)變?yōu)橘~戶之間的轉(zhuǎn)賬;在轉(zhuǎn)賬時,需要兩個賬戶,其中一個賬戶作為源賬戶 Monkey Source ,另一個賬戶作為目標賬戶 Money Destination ;轉(zhuǎn)賬時,源賬戶可能余額不足; 轉(zhuǎn)賬成功后,雙方在手機上都收到了轉(zhuǎn)賬成功的信息。
在 2020 年新年來臨之際,RichMan 慷慨的給 PoorMan 轉(zhuǎn)賬 2000 元紅包。
3. 場景驅(qū)動設(shè)計
對象是強調(diào)『行為協(xié)作』的,但對象自身卻是對『概念』的描述。一旦我們將現(xiàn)實世界映射為對象,由于行為需要正確地分配給各個對象,于是行為就被打散了,缺少了領(lǐng)域場景的連續(xù)性。
『場景驅(qū)動設(shè)計』引入『分解任務(wù)』的方法,一方面通過分而治之的思想降低了領(lǐng)域邏輯的復(fù)雜度,另一方面也建立了一系列連續(xù)的行為去表現(xiàn)領(lǐng)域場景,使得整個領(lǐng)域場景被分解的同時還能保證完整性。
『時序圖』體現(xiàn)了領(lǐng)域場景下行為的動態(tài)協(xié)作過程,并反向驅(qū)動出角色構(gòu)造型來承擔(dān)各自的職責(zé),就能使得對象的設(shè)計變得更加合理。
分解任務(wù)之所以能夠承擔(dān)此重任,一個關(guān)鍵原因在于它匹配了軟件開發(fā)人員的思維模式。在將業(yè)務(wù)需求轉(zhuǎn)換為軟件設(shè)計的過程中,要找到一種既具有業(yè)務(wù)視角又具有設(shè)計視角的思維模式,并非易事。
任務(wù)分解采用面向過程的思維模式,以業(yè)務(wù)視角對領(lǐng)域場景進行觀察和剖析;然后再采用面向?qū)ο蟮乃季S模式,以設(shè)計視角結(jié)合職責(zé)與角色構(gòu)造型,形成對職責(zé)的角色分配。這兩種視角的切換是自然的,它同時降低了需求理解和設(shè)計建模的難度。
軟件設(shè)計終究是由人做出的決策,在提出一種設(shè)計方法時,若能從人的思維模式著手,就容易『找到現(xiàn)實世界與模型世界的結(jié)合點』。如果我們將領(lǐng)域場景視為電影或劇本中的場景,它反映了我們需要解決的代表問題域的現(xiàn)實世界??柧S諾在 《看不見的城市》 一書中描繪了這樣的場景:
梅拉尼亞的人口生生不息:對話者一個個相繼死去,而接替他們對話的人又一個個出生,分別扮演對話中的角色。當(dāng)有人轉(zhuǎn)換角色,或者永遠離開或者初次進入廣場時,就會引起連鎖式變化,直至所有角色都重新分配妥當(dāng)為止。
這個場景描述了一座奇幻的城市,這種城市的居民會聚集在廣場中發(fā)生一場一場的對話,對話持續(xù)不斷地繼續(xù)下去,但是參與對話的角色卻如幻影一般發(fā)生變化。這一幕小說情節(jié)很好地闡釋了 DCI 模式,它開啟了另外一種投影現(xiàn)實世界到對象世界的思維模式。
4. DCI 架構(gòu)模式
Reenskaug 、Trygve 和 James O. Coplien 在 2009 年發(fā)表了一篇論文 《A New Vision of Object-Oriented Programming1》 ,標志著 DCI 架構(gòu)模式的誕生。
值得注意的是 James O. Coplien 曾在年輕時創(chuàng)造了 MVC 架構(gòu)模式,所以在 DCI 的論文中也有對 MVC 架構(gòu)模式的反思。
DCI 模式認為,在現(xiàn)實世界到對象世界的映射中,構(gòu)成元素只有三個:
數(shù)據(jù) <small>Data</small>
上下文 <small>Context</small>
交互 <small>Interaction</small>
在梅拉尼亞這座城市,城市的廣場是上下文,城市的居民是數(shù)據(jù),他們扮演了不同的角色進行不同的對話,這種對話就是交互。

如今的軟件開發(fā)是前后端分離的,所以 View 我們暫不考慮。
Model 分為業(yè)務(wù)數(shù)據(jù)和業(yè)務(wù)邏輯:
業(yè)務(wù)數(shù)據(jù) <====> DCI 中的 Objects : 系統(tǒng)是什么
業(yè)務(wù)邏輯 <====> DCI 中的角色關(guān)聯(lián) : 系統(tǒng)做什么
Controller <====> DCI 中的上下文 Context : 系統(tǒng)的用例場景
Methodless Roles 指的是抽象的角色,而 Methodful Roles 指的是具體的角色,Classes 是組合了多個角色的類,Objects 就是多角色類的實例化。
5. DCI 架構(gòu)模式
-
Data 層
描述系統(tǒng)有哪些領(lǐng)域概念及其之間的關(guān)系,該層專注于領(lǐng)域?qū)ο蟮拇_立和這些對象的生命周期管理及關(guān)系,讓程序員站在對象的角度思考系統(tǒng),從而讓『系統(tǒng)是什么』更容易被理解。
-
Context 層
是盡可能薄的一層。Context 往往被實現(xiàn)得無狀態(tài),只是找到合適的 Role ,讓 Role 交互起來完成業(yè)務(wù)邏輯即可。但是簡單并不代表不重要,顯示化 Context 層正是為開發(fā)者理解軟件業(yè)務(wù)流程提供切入點和主線。
在典型的實現(xiàn)里,每個『用例』都有其對應(yīng)的一個 Context 對象,而用例涉及到的每個角色在對應(yīng)的 Context 里也都有一個標識符。Context 要做的只是將角色標識符與正確的對象綁定到一起,然后我們只要觸發(fā) Context 里的『開場』角色,代碼就會運行下去。
-
Interactive 層
主要體現(xiàn)在對 Role 的建模,Role 是每個 Context 中復(fù)雜的業(yè)務(wù)邏輯的真正執(zhí)行者,體現(xiàn)『系統(tǒng)做什么』。Role 所做的是對行為進行建模,它聯(lián)接了 Context 和領(lǐng)域?qū)ο?。由于系統(tǒng)的行為是復(fù)雜且多變的,Role 使得系統(tǒng)將穩(wěn)定的領(lǐng)域模型層和多變的系統(tǒng)行為層進行了分離,由 Role 專注于對系統(tǒng)行為進行建模。該層往往關(guān)注于系統(tǒng)的可擴展性,更加貼近于軟件工程實踐,在面向?qū)ο笾懈嗟氖且灶惖囊暯沁M行思考設(shè)計。
換句話說,我們希望把角色的邏輯注入到對象,讓這些邏輯成為對象的一部分,而其地位卻絲毫不弱于對象初始化時從類所得到的方法。
和場景驅(qū)動設(shè)計相同,DCI 模式需要從業(yè)務(wù)需求表現(xiàn)的現(xiàn)實世界中截取一幕場景作為設(shè)計的上下文。上下文將參與交互的數(shù)據(jù)『框定』起來,根據(jù)場景要達成的業(yè)務(wù)目標確定對象要扮演的角色,以及角色之間的交互行為。每個數(shù)據(jù)對象在扮演各自角色時,只能做出符合自己角色身份的行為,這些行為在 DCI 模式中被稱之為『角色方法』<small>(Role Method)</small>,它們反映了數(shù)據(jù)的目的;數(shù)據(jù)對象自身還擁有一些固定的行為,稱之為『本地方法』<small>(Local Method)</small>,它們反映了數(shù)據(jù)的特征。數(shù)據(jù)對象通過角色方法參與到上下文的交互,通過本地方法訪問和操作自身擁有的數(shù)據(jù),然后采用某種形式將角色綁定到對象之上:

現(xiàn)實世界有很多這樣的例子。一個人在上下文中會扮演一種特定的角色,他與別的角色展開不同的交互行為。這時,人作為數(shù)據(jù)對象,具備 talk() 、walk() 、write() 等本地行為,這些本地行為與角色無關(guān),屬于人的固有行為 。
當(dāng)一個人處于課堂學(xué)習(xí)上下文時,若扮演了教師角色,就會擁有角色行為 teach() ,與之交互的角色為學(xué)生,角色行為是 learn() 。teach() 與 learn() 這樣的角色方法由 talk() 、write() 等本地方法實現(xiàn),本地方法不會隨著上下文的變化而變化,因此屬于數(shù)據(jù)對象最為穩(wěn)定的領(lǐng)域邏輯。
一個數(shù)據(jù)對象可以同時承擔(dān)多個角色,例如一個人既可以是教師,也可以是學(xué)生,回到家,面對不同的角色,他也在不斷變換著角色:父親、兒子、丈夫、…… 。顯然,角色代表了一種身份或者一種能力,更像是一種接口行為。正如上圖所示,當(dāng)一個數(shù)據(jù)對象參與到上下文的交互中時,就需要將角色綁定到對象上,使得對象擁有角色行為。
3. 轉(zhuǎn)賬業(yè)務(wù)的 DCI 實現(xiàn)
以銀行的轉(zhuǎn)賬業(yè)務(wù)為例,它的上下文就是 TransferingContext ,儲蓄賬戶 SavingAccount 作為體現(xiàn)了領(lǐng)域概念的數(shù)據(jù)對象參與到轉(zhuǎn)賬上下文中。按照 DCI 的思維模式,我們需要對上下文中的數(shù)據(jù)提出兩個問題:
它是什么?數(shù)據(jù)代表了上下文的領(lǐng)域概念;
它做了什么?角色代表了數(shù)據(jù)在上下文中的身份。
雖然轉(zhuǎn)賬上下文牽涉到兩個不同的儲蓄賬戶對象,但各自扮演的角色卻不相同。一個賬戶扮演了轉(zhuǎn)出方 TransferSource ,另一個賬戶扮演了轉(zhuǎn)入方 TransferTarget ,對應(yīng)的角色方法就是 transferOut() 與 transferIn() 。儲蓄賬戶擁有余額數(shù)據(jù),增加和減少余額值都是儲蓄賬戶這個數(shù)據(jù)對象的固有特征,相當(dāng)于針對余額數(shù)據(jù)進行的數(shù)學(xué)運算,對應(yīng)的本地方法為 decrease() 與 increase() 。
很明顯,通過『本地方法』,數(shù)據(jù)回答了『它是什么』這個問題,體現(xiàn)了數(shù)據(jù)的本質(zhì)特征,這樣的行為通常不會發(fā)生變化;角色方法回答了『它做了什么』這一問題,操作了數(shù)據(jù)的業(yè)務(wù)規(guī)則,因此可能會頻繁發(fā)生改變。一個穩(wěn)定不變,一個頻繁變化,自然就需要隔離它們,這就是角色的價值。最后,由『上下文』來指定角色,并管理角色之間的交互行為。
轉(zhuǎn)出方的角色接口:
public interface TransferSource {
// 本地方法到角色方法的映射
Amount getBalance();
void decrease(Amount amount);
// 角色方法
default void transferOut(Amount amount) {
if (getBalance().lessThan(amount)) {
throw new NotEnoughBalanceException();
}
decrease(amount);
}
}
同時,數(shù)據(jù)類 SavingAccount 還必須顯式實現(xiàn)這兩個接口:
public class SavingAccount implements TransferSource, TransferTarget {
...
}
上下文類的實現(xiàn):
public class TransferContext {
private NotificationService notification;
public void transfer(TransferSource source, TranserTarget, Amount amount) {
source.transferOut(amount);
target.transferIn(amount);
notification.sendMessage();
}
}
一個數(shù)據(jù)對象可以同時承擔(dān)多個角色,反過來,一個角色也可能被多個不同的數(shù)據(jù)對象扮演。還是以轉(zhuǎn)賬業(yè)務(wù)為例,可能不僅是 SavingAccount 才能參與轉(zhuǎn)賬,例如通過銀行的儲蓄賬戶將錢轉(zhuǎn)入到支付寶,就是由 AlipayAccount 擔(dān)任轉(zhuǎn)入方角色。如果 AlipayAccount 與 SavingAccount 之間沒有任何關(guān)系,根據(jù)前面的實現(xiàn),就無法將其傳遞給 TransferTarget 角色接口;同理,將支付寶的錢轉(zhuǎn)入到儲蓄賬戶,也受到了數(shù)據(jù)類型的限制。
如果將角色方法的實現(xiàn)留給數(shù)據(jù)類來實現(xiàn),角色接口僅提供抽象的定義,就可以為各種不同的數(shù)據(jù)類戴上“角色”這頂帽子。站在上下文的角度看,它僅關(guān)心參與交互的角色方法,而不在意數(shù)據(jù)對象到底是什么。例如,在課堂學(xué)習(xí)上下文中,可以是一個人擔(dān)任教師的角色,以 teach() 角色行為與學(xué)生交互,但也可以是一個 AI 機器人擔(dān)任教師角色,只要它的授課能夠滿足學(xué)生的需要即可。
顯然,上下文從抽象角度看待參與交互的角色,這就將角色分成了抽象和實現(xiàn)兩個層次。這兩個層次在 DCI 模式中分別稱為 Methodful Role 與 Methodless Role。Methodful Role 組成了數(shù)據(jù)類,數(shù)據(jù)類對象則通過 Methodless Role 對外提供服務(wù),參與到上下文中。仍以轉(zhuǎn)賬上下文為例,Methodless Role 的定義如下:
public interface TransferSource {
void transferOut(Amount amount);
}
public interface TransferTarget {
void transferIn(Amount amount);
}
這樣的角色接口沒有任何實現(xiàn),僅僅規(guī)定了角色參與上下文交互的契約。數(shù)據(jù)類由本地方法和角色方法共同組成,其中它實現(xiàn)的角色方法代表了它是 Methodful Role:
public class SavingAccount implements TransferSource, TransferTarget {
private Amount balance;
// Methodful Role 的角色方法
@Override
public void transferOut(Amount amount) {
if (balance.lessThan(amount))
throw new NotEnoughBalanceException();
decrease(amount);
}
// Methodful Role 的角色方法
@Override
public void transferIn(Amount amount) {
increase(amount);
}
// 本地方法
private void decrease(Amount amount) {
balance.substract(amount);
}
private void increase(Amount amount) {
balance.add(amount);
}
}
public class AlipayAccount implements TransferSource, TransferTarget {
}
Methodful Role 與 Methodless Role 的分離不會影響角色的定義,因為上下文的交互是面向角色的,與數(shù)據(jù)類無關(guān),不受數(shù)據(jù)類類型變化的任何影響,故而 TransferContext 的實現(xiàn)與前面的代碼完全一樣。
我認為,DCI 模式將角色的承擔(dān)者命名為數(shù)據(jù)類是一種糟糕的命名,因為數(shù)據(jù)這一說法極容易誤導(dǎo)設(shè)計者,誤以為該類僅僅為上下文提供交互行為所需的數(shù)據(jù)。
若產(chǎn)生這種誤解,就有可能將數(shù)據(jù)類定義為貧血對象,設(shè)計出貧血模型。
實際上,數(shù)據(jù)類更像是實體,在定義了數(shù)據(jù)屬性之外,還需要定義屬于自己的方法,即本地方法。這些方法同樣表達了領(lǐng)域邏輯,只是該領(lǐng)域邏輯是與數(shù)據(jù)類強內(nèi)聚的行為,如 SavingAccount 的
increase()與decrease()方法。
DCI 模式與場景驅(qū)動設(shè)計
毫無疑問,DCI 模式通過數(shù)據(jù)類、數(shù)據(jù)對象、角色、角色交互和上下文等設(shè)計元素共同實現(xiàn)了現(xiàn)實世界到對象世界的映射。這種思維模式的起點仍然是領(lǐng)域場景,上下文相當(dāng)于是搭建領(lǐng)域場景的舞臺。在這個舞臺上,進行的并非冷靜而細化的過程分解,而是從角色出發(fā),推斷和指導(dǎo)參與領(lǐng)域場景的各個演員之間的互動。因此,我們也可以將 DCI 模式結(jié)合到場景驅(qū)動設(shè)計中。
對比場景驅(qū)動設(shè)計的角色構(gòu)造型,DCI 模式的上下文相當(dāng)于領(lǐng)域服務(wù),數(shù)據(jù)類相當(dāng)于聚合。在定義上下文時,DCI 模式通過觀察不同角色之間的交互來滿足領(lǐng)域場景的業(yè)務(wù)需求。角色方法的定義體現(xiàn)了面向?qū)ο蟆敖涌诟綦x原則”與“面向接口設(shè)計”的設(shè)計思想,而角色之間的交互模式又體現(xiàn)了對象之間良好的行為協(xié)作,這在一定程度上保證了領(lǐng)域設(shè)計模型的質(zhì)量,滿足可重用性與可擴展性。在上下文之上,是體現(xiàn)了業(yè)務(wù)價值的領(lǐng)域場景,仍然由應(yīng)用服務(wù)來實現(xiàn)對外業(yè)務(wù)接口的包裝,在內(nèi)部的實現(xiàn)中,則糅合諸如事務(wù)、認證授權(quán)、系統(tǒng)日志等橫切關(guān)注點。至于數(shù)據(jù)對象的獲得,仍然交給資源庫。不同之處在于資源庫的注入由應(yīng)用服務(wù)來完成,這是因為作為領(lǐng)域服務(wù)的上下文協(xié)調(diào)的是角色之間的交互,即領(lǐng)域服務(wù)依賴于角色,而非數(shù)據(jù)對象的 ID 。
結(jié)合 DCI 模式的場景驅(qū)動設(shè)計過程為:
- 識別領(lǐng)域場景,并由對應(yīng)的應(yīng)用服務(wù)承擔(dān)
- 領(lǐng)域場景對應(yīng)的業(yè)務(wù)行為由上下文領(lǐng)域服務(wù)執(zhí)行
- 為了完成該領(lǐng)域場景,明確有哪些角色參與了行為的交互
- 為這些角色定義角色接口,角色方法實現(xiàn)為默認方法,或者分為抽象與實現(xiàn)
- 確定承擔(dān)這些角色的數(shù)據(jù)對象,定義數(shù)據(jù)類以及數(shù)據(jù)類的本地方法
即使不遵循 DCI 模式,我們也應(yīng)盡量遵循 “角色接口” 的設(shè)計思想。角色、職責(zé)、協(xié)作本身就是場景驅(qū)動設(shè)計分配職責(zé)過程的三要素。區(qū)別在于二者對角色的定義不同。場景驅(qū)動設(shè)計的角色構(gòu)造型屬于設(shè)計角度的角色定義,它來自于職責(zé)驅(qū)動設(shè)計對角色的分類,也參考了領(lǐng)域驅(qū)動設(shè)計的設(shè)計模式。不同的角色構(gòu)造型承擔(dān)不同的職責(zé),但并不包含任何業(yè)務(wù)含義。DCI 模式的角色是直接參與領(lǐng)域場景的對象,如 Martin Fowler 對角色接口的闡述,他認為是從供應(yīng)者與消費者之間協(xié)作的角度來定義的接口,代表了業(yè)務(wù)場景中與其他類型協(xié)作的角色。
在場景驅(qū)動設(shè)計過程中,當(dāng)我們將職責(zé)分配給聚合時,可以借鑒 DCI 模式,從領(lǐng)域服務(wù)的角度去思考抽象的角色交互,引入的角色接口可以在重用性和擴展性方面改進領(lǐng)域設(shè)計模型。當(dāng)然,這在一定程度上要考究面向?qū)ο蟮脑O(shè)計能力,沒有足夠的抽象與概括能力,可能難以識別出正確的角色。例如,在薪資管理系統(tǒng)的支付薪資場景中,該為計算薪資上下文引入什么樣的角色呢?與轉(zhuǎn)賬上下文不同,計算薪資上下文并沒有兩個不同的角色參與交互,這時的角色就應(yīng)該體現(xiàn)為 數(shù)據(jù)類在上下文中的能力 ,故而可以獲得 PayrollCalculable 角色。數(shù)據(jù)類 Employee 只有實現(xiàn)了該角色接口,才有“能力”被上下文計算薪資。