實現 DCI 架構
前言
在面向對象編程的理念裏,應用程序是對現實世界的抽象,我們經常會將現實中的事物建模爲編程語言中的類 / 對象(“是什麼”),而事物的行爲則建模爲方法(“做什麼”)。面向對象編程有三大基本特性(封裝、繼承 / 組合、多態)和五大基本原則(單一職責原則、開放封閉原則、里氏替換原則、依賴倒置原則、接口分離原則),但知道這些還並不足以讓我們設計出好的程序,於是很多方法論就湧現了出來。
近來最火的當屬領域驅動設計(DDD),其中戰術建模提出的實體、值對象、聚合等建模方法,能夠很好的指導我們設計出符合現實世界的領域模型。但 DDD 也不是萬能的,在某些應用場景下,按照傳統的戰術建模 / 面向對象方法設計出來的程序,也會存在可維護性差、違反單一職責原則等問題。
本文介紹的 DCI 建模方法可以看成是戰術建模的一種輔助,在某些場景下,它可以很好的彌補 DDD 戰術建模的一些缺點。接下來,我們將會通過一個案例來介紹 DCI 是如何解決 DDD 戰術建模的這些缺點的。
本文涉及的代碼歸檔在 github 項目:https://github.com/ruanrunxue/DCI-Architecture-Implementation
案例
考慮一個普通人的生活日常,他會在學校上課,也會趁着暑假去公司工作,在工作之餘去公園遊玩,也會像普通人一樣在家喫喝玩樂。當然,一個人的生活還遠不止這些,爲了講解方便,本文只針對這幾個典型的場景進行建模示例。
使用 DDD 建模
按照 DDD 戰術建模的思路,首先,我們會列出該案例的通用語言:
人、身份證、銀行卡、家、喫飯、睡覺、玩遊戲、學校、學生卡、學習、考試、公司、工卡、上班、下班、公園、購票、遊玩
接着,我們使用戰術建模技術(值對象、實體、聚合、領域服務、資源庫)對通用語言進行領域建模。
DDD 建模後的代碼目錄結構如下:
- aggregate: 聚合
- company.go
- home.go
- park.go
- school.go
- entity: 實體
- people.go
- vo: 值對象
- account.go
- identity_card.go
- student_card.go
- work_card.go
我們將身份證、學生卡、工卡、銀行卡這幾個概念,建模爲值對象(Value Object):
package vo
// 身份證
type IdentityCard struct {
Id uint32
Name string
}
// 學生卡
type StudentCard struct {
Id uint32
Name string
School string
}
// 工卡
type WorkCard struct {
Id uint32
Name string
Company string
}
// 銀行卡
type Account struct {
Id uint32
Balance int
}
...
接着我們將人建模成實體(Entity),他包含了身份證、學生卡等值對象,也具備喫飯、睡覺等行爲:
package entity
// 人
type People struct {
vo.IdentityCard
vo.StudentCard
vo.WorkCard
vo.Account
}
// 學習
func (p *People) Study() {
fmt.Printf("Student %+v studying\n", p.StudentCard)
}
// 考試
func (p *People) Exam() {
fmt.Printf("Student %+v examing\n", p.StudentCard)
}
// 喫飯
func (p *People) Eat() {
fmt.Printf("%+v eating\n", p.IdentityCard)
p.Account.Balance--
}
// 睡覺
func (p *People) Sleep() {
fmt.Printf("%+v sleeping\n", p.IdentityCard)
}
// 玩遊戲
func (p *People) PlayGame() {
fmt.Printf("%+v playing game\n", p.IdentityCard)
}
// 上班
func (p *People) Work() {
fmt.Printf("%+v working\n", p.WorkCard)
p.Account.Balance++
}
// 下班
func (p *People) OffWork() {
fmt.Printf("%+v getting off work\n", p.WorkCard)
}
// 購票
func (p *People) BuyTicket() {
fmt.Printf("%+v buying a ticket\n", p.IdentityCard)
p.Account.Balance--
}
// 遊玩
func (p *People) Enjoy() {
fmt.Printf("%+v enjoying park scenery\n", p.IdentityCard)
}
最後,我們將學校、公司、公園、家建模成聚合(Aggregate),聚合由一個或多個實體、值對象組合而成,組織它們完成具體的業務邏輯:
package aggregate
// 家
type Home struct {
me *entity.People
}
func (h *Home) ComeBack(p *entity.People) {
fmt.Printf("%+v come back home\n", p.IdentityCard)
h.me = p
}
// 執行Home的業務邏輯
func (h *Home) Run() {
h.me.Eat()
h.me.PlayGame()
h.me.Sleep()
}
// 學校
type School struct {
Name string
students []*entity.People
}
func (s *School) Receive(student *entity.People) {
student.StudentCard = vo.StudentCard{
Id: rand.Uint32(),
Name: student.IdentityCard.Name,
School: s.Name,
}
s.students = append(s.students, student)
fmt.Printf("%s Receive stduent %+v\n", s.Name, student.StudentCard)
}
// 執行School的業務邏輯
func (s *School) Run() {
fmt.Printf("%s start class\n", s.Name)
for _, student := range s.students {
student.Study()
}
fmt.Println("students start to eating")
for _, student := range s.students {
student.Eat()
}
fmt.Println("students start to exam")
for _, student := range s.students {
student.Exam()
}
fmt.Printf("%s finish class\n", s.Name)
}
// 公司
type Company struct {
Name string
workers []*entity.People
}
func (c *Company) Employ(worker *entity.People) {
worker.WorkCard = vo.WorkCard{
Id: rand.Uint32(),
Name: worker.IdentityCard.Name,
Company: c.Name,
}
c.workers = append(c.workers, worker)
fmt.Printf("%s Employ worker %s\n", c.Name, worker.WorkCard.Name)
}
// 執行Company的業務邏輯
func (c *Company) Run() {
fmt.Printf("%s start work\n", c.Name)
for _, worker := range c.workers {
worker.Work()
}
fmt.Println("worker start to eating")
for _, worker := range c.workers {
worker.Eat()
}
fmt.Println("worker get off work")
for _, worker := range c.workers {
worker.OffWork()
}
fmt.Printf("%s finish work\n", c.Name)
}
// 公園
type Park struct {
Name string
enjoyers []*entity.People
}
func (p *Park) Welcome(enjoyer *entity.People) {
fmt.Printf("%+v come to park %s\n", enjoyer.IdentityCard, p.Name)
p.enjoyers = append(p.enjoyers, enjoyer)
}
// 執行Park的業務邏輯
func (p *Park) Run() {
fmt.Printf("%s start to sell tickets\n", p.Name)
for _, enjoyer := range p.enjoyers {
enjoyer.BuyTicket()
}
fmt.Printf("%s start a show\n", p.Name)
for _, enjoyer := range p.enjoyers {
enjoyer.Enjoy()
}
fmt.Printf("show finish\n")
}
那麼,根據上述方法建模出來的模型是這樣的:
模型的運行方法如下:
paul := entity.NewPeople("Paul")
mit := aggregate.NewSchool("MIT")
google := aggregate.NewCompany("Google")
home := aggregate.NewHome()
summerPalace := aggregate.NewPark("Summer Palace")
// 上學
mit.Receive(paul)
mit.Run()
// 回家
home.ComeBack(paul)
home.Run()
// 工作
google.Employ(paul)
google.Run()
// 公園遊玩
summerPalace.Welcome(paul)
summerPalace.Run()
貧血模型 VS 充血模型(工程派 VS 學院派)
上一節中,我們使用 DDD 的戰術建模完成了該案例領域模型。模型的核心是People
實體,它有IdentityCard
、StudentCard
等數據屬性,也有Eat()
、Study()
、Work()
等業務行爲 ,非常符合現實世界中定義。這也是學院派所倡導的,同時擁有數據屬性和業務行爲的充血模型。
然而,充血模型並非完美,它也有很多問題,比較典型的是這兩個:
問題一:上帝類
People
這個實體包含了太多的職責,導致它變成了一個名副其實的上帝類。試想,這裏還是裁剪了很多 “人” 所包含的屬性和行爲,如果要建模一個完整的模型,其屬性和方法之多,無法想象。上帝類違反了單一職責原則,會導致代碼的可維護性變得極差。
問題二:模塊間耦合
School
與Company
本應該是相互獨立的,School
不必關注上班與否,Company
也不必關注考試與否。但是現在因爲它們都依賴了People
這個實體,School
可以調用與Company
相關的Work()
和OffWork()
方法,反之亦然。這導致模塊間產生了不必要的耦合,違反了接口隔離原則。
這些問題都是工程派不能接受的,從軟件工程的角度,它們會使得代碼難以維護。解決這類問題的方法,比較常見的是對實體進行拆分,比如將實體的行爲建模成領域服務,像這樣:
type People struct {
vo.IdentityCard
vo.StudentCard
vo.WorkCard
vo.Account
}
type StudentService struct{}
func (s *StudentService) Study(p *entity.People) {
fmt.Printf("Student %+v studying\n", p.StudentCard)
}
func (s *StudentService) Exam(p *entity.People) {
fmt.Printf("Student %+v examing\n", p.StudentCard)
}
type WorkerService struct{}
func (w *WorkerService) Work(p *entity.People) {
fmt.Printf("%+v working\n", p.WorkCard)
p.Account.Balance++
}
func (w *WorkerService) OffWOrk(p *entity.People) {
fmt.Printf("%+v getting off work\n", p.WorkCard)
}
// ...
這種建模方法,解決了上述兩個問題,但也變成了所謂的貧血模型:People
變成了一個純粹的數據類,沒有任何業務行爲。在人的心理上,這樣的模型並不能在建立起對現實世界的對應關係,不容易讓人理解,因此被學院派所抵制。
到目前爲止,貧血模型和充血模型都有各有優缺點,工程派和學院派誰都無法說服對方。接下來,輪到本文的主角出場了。
DCI 架構
DCI(Data,Context,Interactive)架構是一種面向對象的軟件架構模式,在《The DCI Architecture: A New Vision of Object-Oriented Programming》一文中被首次提出。與傳統的面向對象相比,DCI 能更好地對數據和行爲之間的關係進行建模,從而更容易被人理解。
-
Data,也即數據 / 領域對象,用來描述系統 “是什麼”,通常採用 DDD 中的戰術建模來識別當前模型的領域對象,等同於 DDD 分層架構中的領域層。
-
Context,也即場景,可理解爲是系統的 Use Case,代表了系統的業務處理流程,等同於 DDD 分層架構中的應用層。
-
Interactive,也即交互,是 DCI 相對於傳統面向對象的最大發展,它認爲我們應該顯式地對領域對象(Object)在每個業務場景(Context)中扮演(Cast)的角色(Role)進行建模。Role 代表了領域對象在業務場景中的業務行爲(“做什麼”),Role 之間通過交互完成完整的義務流程。
這種角色扮演的模型我們並不陌生,在現實的世界裏也是隨處可見,比如,一個演員可以在這部電影裏扮演英雄的角色,也可以在另一部電影裏扮演反派的角色。
DCI 認爲,對 Role 的建模應該是面向 Context 的,因爲特定的業務行爲只有在特定的業務場景下才會有意義。通過對 Role 的建模,我們就能夠將領域對象的方法拆分出去,從而避免了上帝類的出現。最後,領域對象通過組合或繼承的方式將 Role 集成起來,從而具備了扮演角色的能力。
DCI 架構一方面通過角色扮演模型使得領域模型易於理解,另一方面通過 “小類大對象” 的手法避免了上帝類的問題,從而較好地解決了貧血模型和充血模型之爭。另外,將領域對象的行爲根據 Role 拆分之後,模塊更加的高內聚、低耦合了。
使用 DCI 建模
回到前面的案例,使用 DCI 的建模思路,我們可以將 “人” 的幾種行爲按照不同的角色進行劃分。喫完、睡覺、玩遊戲,是作爲人類角色的行爲;學習、考試,是作爲學生角色的行爲;上班、下班,是作爲員工角色的行爲;購票、遊玩,則是作爲遊玩者角色的行爲。“人” 在家這個場景中,充當的是人類的角色;在學校這個場景中,充當的是學生的角色;在公司這個場景中,充當的是員工的角色;在公園這個場景中,充當的是遊玩者的角色。
需要注意的是,學生、員工、遊玩者,這些角色都應該具備人類角色的行爲,比如在學校裏,學生也需要喫飯。
最後,根據 DCI 建模出來的模型,應該是這樣的:
在 DCI 模型中,People
不再是一個包含衆多屬性和方法的 “上帝類”,這些屬性和方法被拆分到多個 Role 中實現,而People
由這些 Role 組合而成。
另外,School
與Company
也不再耦合,School
只引用了Student
,不能調用與Company
相關的Worker
的Work()
和OffWorker()
方法。
代碼實現 DCI 模型
DCI 建模後的代碼目錄結構如下;
- context: 場景
- company.go
- home.go
- park.go
- school.go
- object: 對象
- people.go
- data: 數據
- account.go
- identity_card.go
- student_card.go
- work_card.go
- role: 角色
- enjoyer.go
- human.go
- student.go
- worker.go
從代碼目錄結構上看,DDD 和 DCI 架構相差並不大,aggregate
目錄演變成了context
目錄;vo
目錄演變成了data
目錄;entity
目錄則演變成了object
和role
目錄。
首先,我們實現基礎角色Human
,Student
、Worker
、Enjoyer
都需要組合它:
package role
// 人類角色
type Human struct {
data.IdentityCard
data.Account
}
func (h *Human) Eat() {
fmt.Printf("%+v eating\n", h.IdentityCard)
h.Account.Balance--
}
func (h *Human) Sleep() {
fmt.Printf("%+v sleeping\n", h.IdentityCard)
}
func (h *Human) PlayGame() {
fmt.Printf("%+v playing game\n", h.IdentityCard)
}
接着,我們再實現其他角色,需要注意的是,Student
、Worker
、Enjoyer
不能直接組合Human
,否則People
對象將會有 4 個Human
子對象,與模型不符:
// 錯誤的實現
type Worker struct {
Human
}
func (w *Worker) Work() {
fmt.Printf("%+v working\n", w.WorkCard)
w.Balance++
}
...
type People struct {
Human
Student
Worker
Enjoyer
}
func main() {
people := People{}
fmt.Printf("People: %+v", people)
}
// 結果輸出, People中有4個Human:
// People: {Human:{} Student:{Human:{}} Worker:{Human:{}} Enjoyer:{Human:{}}}
爲解決該問題,我們引入了xxxTrait
接口:
// 人類角色特徵
type HumanTrait interface {
CastHuman() *Human
}
// 學生角色特徵
type StudentTrait interface {
CastStudent() *Student
}
// 員工角色特徵
type WorkerTrait interface {
CastWorker() *Worker
}
// 遊玩者角色特徵
type EnjoyerTrait interface {
CastEnjoyer() *Enjoyer
}
Student
、Worker
、Enjoyer
組合HumanTrait
,並通過Compose(HumanTrait)
方法進行特徵注入,只要在注入的時候保證Human
是同一個,就可以解決該問題了。
// 學生角色
type Student struct {
// Student同時也是個普通人,因此組合了Human角色
HumanTrait
data.StudentCard
}
// 注入人類角色特徵
func (s *Student) Compose(trait HumanTrait) {
s.HumanTrait = trait
}
func (s *Student) Study() {
fmt.Printf("Student %+v studying\n", s.StudentCard)
}
func (s *Student) Exam() {
fmt.Printf("Student %+v examing\n", s.StudentCard)
}
// 員工角色
type Worker struct {
// Worker同時也是個普通人,因此組合了Human角色
HumanTrait
data.WorkCard
}
// 注入人類角色特徵
func (w *Worker) Compose(trait HumanTrait) {
w.HumanTrait = trait
}
func (w *Worker) Work() {
fmt.Printf("%+v working\n", w.WorkCard)
w.CastHuman().Balance++
}
func (w *Worker) OffWork() {
fmt.Printf("%+v getting off work\n", w.WorkCard)
}
// 遊玩者角色
type Enjoyer struct {
// Enjoyer同時也是個普通人,因此組合了Human角色
HumanTrait
}
// 注入人類角色特徵
func (e *Enjoyer) Compose(trait HumanTrait) {
e.HumanTrait = trait
}
func (e *Enjoyer) BuyTicket() {
fmt.Printf("%+v buying a ticket\n", e.CastHuman().IdentityCard)
e.CastHuman().Balance--
}
func (e *Enjoyer) Enjoy() {
fmt.Printf("%+v enjoying scenery\n", e.CastHuman().IdentityCard)
}
最後,實現People
這一領域對象:
package object
type People struct {
// People對象扮演的角色
role.Human
role.Student
role.Worker
role.Enjoyer
}
// People實現了HumanTrait、StudentTrait、WorkerTrait、EnjoyerTrait等特徵接口
func (p *People) CastHuman() *role.Human {
return &p.Human
}
func (p *People) CastStudent() *role.Student {
return &p.Student
}
func (p *People) CastWorker() *role.Worker {
return &p.Worker
}
func (p *People) CastEnjoyer() *role.Enjoyer {
return &p.Enjoyer
}
// People在初始化時,完成對角色特徵的注入
func NewPeople(name string) *People {
// 一些初始化的邏輯...
people.Student.Compose(people)
people.Worker.Compose(people)
people.Enjoyer.Compose(people)
return people
}
進行角色拆分之後,在實現Home
、School
、Company
、Park
等場景時,只需依賴相應的角色即可,不再需要依賴People
這一領域對象:
// 家
type Home struct {
me *role.Human
}
func (h *Home) ComeBack(human *role.Human) {
fmt.Printf("%+v come back home\n", human.IdentityCard)
h.me = human
}
// 執行Home的業務邏輯
func (h *Home) Run() {
h.me.Eat()
h.me.PlayGame()
h.me.Sleep()
}
// 學校
type School struct {
Name string
students []*role.Student
}
func (s *School) Receive(student *role.Student) {
// 初始化StduentCard邏輯 ...
s.students = append(s.students, student)
fmt.Printf("%s Receive stduent %+v\n", s.Name, student.StudentCard)
}
// 執行School的業務邏輯
func (s *School) Run() {
fmt.Printf("%s start class\n", s.Name)
for _, student := range s.students {
student.Study()
}
fmt.Println("students start to eating")
for _, student := range s.students {
student.CastHuman().Eat()
}
fmt.Println("students start to exam")
for _, student := range s.students {
student.Exam()
}
fmt.Printf("%s finish class\n", s.Name)
}
// 公司
type Company struct {
Name string
workers []*role.Worker
}
func (c *Company) Employ(worker *role.Worker) {
// 初始化WorkCard邏輯 ...
c.workers = append(c.workers, worker)
fmt.Printf("%s Employ worker %s\n", c.Name, worker.WorkCard.Name)
}
// 執行Company的業務邏輯
func (c *Company) Run() {
fmt.Printf("%s start work\n", c.Name)
for _, worker := range c.workers {
worker.Work()
}
fmt.Println("worker start to eating")
for _, worker := range c.workers {
worker.CastHuman().Eat()
}
fmt.Println("worker get off work")
for _, worker := range c.workers {
worker.OffWork()
}
fmt.Printf("%s finish work\n", c.Name)
}
// 公園
type Park struct {
Name string
enjoyers []*role.Enjoyer
}
func (p *Park) Welcome(enjoyer *role.Enjoyer) {
fmt.Printf("%+v come park %s\n", enjoyer.CastHuman().IdentityCard, p.Name)
p.enjoyers = append(p.enjoyers, enjoyer)
}
// 執行Park的業務邏輯
func (p *Park) Run() {
fmt.Printf("%s start to sell tickets\n", p.Name)
for _, enjoyer := range p.enjoyers {
enjoyer.BuyTicket()
}
fmt.Printf("%s start a show\n", p.Name)
for _, enjoyer := range p.enjoyers {
enjoyer.Enjoy()
}
fmt.Printf("show finish\n")
}
模型的運行方法如下:
paul := object.NewPeople("Paul")
mit := context.NewSchool("MIT")
google := context.NewCompany("Google")
home := context.NewHome()
summerPalace := context.NewPark("Summer Palace")
// 上學
mit.Receive(paul.CastStudent())
mit.Run()
// 回家
home.ComeBack(paul.CastHuman())
home.Run()
// 工作
google.Employ(paul.CastWorker())
google.Run()
// 公園遊玩
summerPalace.Welcome(paul.CastEnjoyer())
summerPalace.Run()
寫在最後
從前文所描述的場景中,我們可以發現傳統的 DDD / 面向對象設計方法在對行爲進行建模方面存在着不足,進而導致了所謂的貧血模型和充血模型之爭。
DCI 架構的出現很好的彌補了這一點,它通過引入角色扮演的思想,巧妙地解決了充血模型中上帝類和模塊間耦合問題,而且不影響模型的正確性。當然,DCI 架構也不是萬能的,在行爲較少的業務模型中,使用 DCI 來建模並不合適。
最後,將 DCI 架構總結成一句話就是:領域對象(Object)在不同的場景(Context)中扮演(Cast)不同的角色(Role),角色之間通過交互(Interactive)來完成具體的業務邏輯。
參考
1、The DCI Architecture: A New Vision of Object-Oriented Programming, Trygve Reenskaug & James O. Coplien
2、軟件設計的演變過程, 張曉龍
3、Implement Domain Object in Golang, 張曉龍
4、DCI: 代碼的可理解性, chelsea
5、DCI in C++, MagicBowen
更多文章請關注微信公衆號:元閏子的邀請
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/AD1-0htaGqnxZcUCxw7mNA