然而,充血模型并非完美,它也有很多問題,比較典型的是這兩個:
問題一:上帝類
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:{}}}
-
編程語言
+關注
關注
10文章
1939瀏覽量
34607 -
應用程序
+關注
關注
37文章
3245瀏覽量
57614 -
DCI
+關注
關注
0文章
39瀏覽量
6808 -
面向對象編程
+關注
關注
0文章
22瀏覽量
1807
發布評論請先 登錄
相關推薦
評論