聽說過「DCI」嗎?
這里是Z哥的個人公眾號
每周五11:45 按時送達
當然了,也會時不時加個餐~
我的第「229」篇原創(chuàng)敬上
前幾周和團隊里的「DDD」愛好者交流的時候,有人提到了一個概念叫「DCI」。我當時就想我只知道「DI」和「CI」,「DCI」又是什么鬼。后來在網(wǎng)上搜了一下才發(fā)現(xiàn)在 2009 年這個概念就被提出來了,當時的文章是《The DCI Architecture: A New Vision of Object-Oriented Programming》。
DCI 中的 3 個字母分別代表:Data,Context,Interactive。在我看來,這 3 個概念共同配合表達出了某個“角色”做的事情:
「誰/什么東西(Data)」-「在什么場景下(Context)」-「做什么事(Interactive)」比如,當你在家里打掃衛(wèi)生的時候,你的角色其實是“清潔工”;當你在家里燒飯的時候,你的角色是“廚師”;當你在家里運動的時候,你的角色是“運動者”。你看,同樣的一個人在不同場景下所產(chǎn)生的行為都與當時所處的角色有關(guān)。
DCI 的核心就是那個 Interactive,也是“角色”這個概念存在的地方。
func?(p?Person)?Clean()?{
fmt.Println("Clean")
}
func (p Person) CookFood() {
fmt.Println("CookFood")
}
func (p Person) Sport() {
fmt.Println("Sport")
}
type Person struct {
Name string
Age int
}
func MethodA(p Person){
p.Clean()
p.CookFood()
p.Sport()
}
這很明顯與 OO 思想中的核心概念「高內(nèi)聚低耦合」背道而馳,違反了「迪米特法則」。
而函數(shù)式編程的三層架構(gòu)之所以流行了很多年,就是因為它的世界里主要關(guān)注的是顆粒度最小的「方法」應該放到三層中的哪一層,而對「方法」在某一層內(nèi)放到哪個對象之中是沒有明確規(guī)定的。 因此在上面的例子中,如果我們將Clean()、Cook() 和 Exercise() 分別寫在不同的 XXXService 中,并同時接收 Person 作為入?yún)?,那么可以輕松地消除“上帝類”,但與此同時 Person 也成為了一個只有屬性的「貧血模型」。 因此 DCI 的提出就是通過一個新的視角來定義對象,它通過增加一層概念——「角色」,將傳統(tǒng) DDD 中對象上的非通用方法轉(zhuǎn)移到了不同的角色對象中,避免了需要在一個對象上同時表達“是什么”和“能做什么”而可能出現(xiàn)的「上帝類」問題。 同時,通過由多個角色組成的對象也避免了「貧血模型」的發(fā)生。
接下來看看如何使用 DCI 來重新設計上面的代碼。其實很簡單,將 Person 設計成由 3 個角色 Cleaner、Cook、Sporter 組成,在每個角色中分別定義 Clean()、Cook() 和 Exercise()方法。
type Cleaner interface {
Clean()
}
type Cook interface {
CookFood()
}
type Sporter interface {
Sport()
}
type Person struct {
Name string
Age int
}
func (p Person) Clean() {
fmt.Println("Clean")
}
func (p Person) CookFood() {
fmt.Println("CookFood")
}
func (p Person) Sport() {
fmt.Println("Sport")
}
func DoSomeThing(cook Cook) {
fmt.Println("DoSomeThing")
cook.CookFood()
}
func main() {
??var?p?Person
p.Clean()
p.CookFood()
p.Sport()
DoSomeThing(p)
}
如此一來,我們可以將一些使用 Person 作為入?yún)⒌姆椒ㄕ{(diào)整成相應的角色,以達到「迪米特法則」所提倡的效果。
我們再想深入一步,Cook() 和 Clean() 的實現(xiàn)中都需要“拿起東西”,這是一個和角色無關(guān)的行為,那么可以將它直接定義在Person中。
func (p Person) TakeUp(thing string) {
fmt.Println(fmt.Sprintf("%s TakeUp a %s", p.Name, thing))
}
func (p Person) Clean() {
p.TakeUp("掃帚")
fmt.Println("Clean")
}
func (p Person) CookFood() {
p.TakeUp("鍋子")
fmt.Println("CookFood")
}
在 DCI 中,將角色上定義的方法稱作「Role Method」,將對象(Data)上定義的方法稱作「Local Method」。
前者是填充業(yè)務邏輯的地方,而后者更像是對象(Data)自身天然具有的能力,與業(yè)務邏輯無關(guān)。
上面的這整套實現(xiàn)邏輯在 DCI 中被稱作 Methodless Role,與之對應的還有 Methodful Role 的實現(xiàn)邏輯,在這里就不展開了。顧名思義就是在角色的定義上更豐富,將「Local Method」也定義出來。另外,增加一層「角色」的概念后,我們可以發(fā)現(xiàn),任何具有相同行為的對象都可以給他設置同一個角色。比如,機器人也可以打掃,那么這個 Cleaner 的角色也可以定義到 Robot 對象中,而不僅僅是 Person 對象。只不過,Robot.Clean() 的實現(xiàn)不是“拿起掃帚”,而是“制定一個行走路線”,然后它自己會把垃圾吸到自己身體里。
可能你會問,Context 呢?好像一直沒提到它該怎么實現(xiàn)?以 Z 哥目前的理解來看,Context 所做的事情其實和傳統(tǒng) DDD 中的 Applicaion 層做的事情是重合的,只是代碼結(jié)構(gòu)的不同。因此,我認為這部分倒不是重點,你可以按照原先的 Application 層代碼來寫,相當于每一個 Application 層中的方法就是一個 Context。
本質(zhì)上說,DCI 是一種 “角色接口” 設計思想,如果習慣 OO 編程的小伙伴應該是很熟悉這種寫法的。
好了,我們總結(jié)一下。這篇呢,Z哥和你分享了我對 DCI 的了解。它通過引入「角色」的概念,將傳統(tǒng) DDD 建模時賦予「對象」的兩個職責“是什么”和“能做什么”中的后者拆分到「角色」中去定義,避免上帝類問題。同時,因為角色最終還是會作用到「對象」上,所以也不會出現(xiàn)函數(shù)式編程中的貧血模型問題。DCI 中,對定義在「角色」上的方法稱為 Role Method,而直接定義在「對象」上的方法稱作 Local Method。對于「角色」在編碼的實現(xiàn),一般建議使用 interface 的方式來體現(xiàn),因為“角色只定義行為”,具體行為要怎么做,由所在的對象來實現(xiàn)。如此符合「依賴倒置原則」的場景自然適合用 interface 來實現(xiàn)。
最后,Z 哥再分享一個實踐 DCI 的思路給你。首先是什么時候需要用 DCI?當你在實踐 DDD 的過程中,覺得某個對象過大了,有點上帝類的味道,這時候就可以想一下是否可以通過 DCI 來重新設計一下。如何落地DCI?分為以下四步:
-
識別領(lǐng)域場景
-
羅列其中的業(yè)務行為
-
分析這些定位屬于什么角色,定義角色接口
-
確定承擔這些角色的數(shù)據(jù)對象,定義數(shù)據(jù)類以及數(shù)據(jù)類的本地方法
好了,今天就聊這些,希望對你有所啟發(fā)。
推薦閱讀:
原創(chuàng)不易,如果你覺得這篇文章還不錯,就「 點贊 」或者「在看」一下吧,鼓勵我的創(chuàng)作 :)
也可以分享我的公眾號名片給有需要的朋友們。
如果你有關(guān)于軟件架構(gòu)、分布式系統(tǒng)、產(chǎn)品、運營的困惑
可以試試點擊「閱讀原文」
評論
圖片
表情
