什麼是DDD
產品:Domain Model,Software Design
Domain:一個會實際存在,被使用的系統。不同的系統有不同的運作邏輯、流程、資料。也是軟體要幫助的對象。Ex:銀行、書籍管理、進銷貨、股票交易,電信,這些是原來就有系統存在,軟體可以幫助這些系統運行。而像是遊戲、作業系統、這些原來沒有存在的系統,軟體就是把系統行為建構起來的基本組件。
Model:把實際存在,運作的事物抽象化就是Model,Ex:經濟模型、數學模型、物理模型...等,Model不是一個特定的圖,Model是一個idea,Model不只是Domain專家的知識,Model是一個嚴格組織與挑選過的,對於Domain專家的知識的抽象化。Model可以用圖來表示,可以用code來表示,可以用文字來描述。對於開發人員而言,Model就代表Domain。
- Software Develop花太多的時間在code上,他們認為軟體就只是Object與Method。
- 建立Model的最主要目的,就是要拿來溝通。
- Software Design可以說是整間房子的藍圖,Code Design算是實際蓋房子的方法。
- Design Pattern就是Code design的一種方法
- Waterfall的缺點就是作到最後沒人敢動
- Agile的方式的缺點就是一開始沒有作周詳的設計
- To be or not to be?
- Domain專家講的一定都對?No,Domain專家的語言不見得可以直接拿來做軟體,所以要問清楚,再作Modeling,Model也會不斷做修改。
- DDD的目標就是透過與Domain專家溝通,作對應Domain的Mode,再根據Model,來作出Software Design,讓Design可以與Domain對應,而且讓參與製作產品的所有人,都可以透過Model這個共通的語言來溝通
The Ubiquitous Language
- Software developers腦袋裡面裝的就是Classes,Methods,演算法,Patterns,而且有一種要把現實世界所遇到的問題配合這些觀念的傾向,所以作出來的Design只有搞軟體的才會懂。這就是我們一直以來作事的方法。
- 所以與Domain專家討論時,Software developer傾向於講libraries,frameworks,Persistence,Database,這些純技術名詞,Domain專家無法了解。
- 作出符合需求的軟體->與Domain專家與其他人員更好的溝通->The Ubiquitous Language
- The Ubiquitous Language是由雙方討論出來的,比較偏Domain expert的行話。
- UML可以表達關係,一個一個的個體,但是沒辦法詳細表達每一個個體的意義,或是每一個個體的功能。
- 用UML圖表現個體,文字詳述每個個體代表的概念,限制,或是互動。一次描述Domain的一個部份。這似乎可以用在Function Spec裡面。
- 畫出代表整個系統的單一UML意義不大,不如根據不同概念畫出不同部分的UML。
- Code不能完全代表Model,不可能給Domain Expert看,因為裡面太多實作細節,例如用了Design Pattern,特殊演算法...等
Model-Driven Design
- Domain專家,系統分析師,Deveoper最好可以一起Design
- 最後可以把Model作成可以用code表達的方式,如果設計一個無法實作的Mode,再怎麼完全貼近Domain也沒用。
- 設計model也要能符合軟體架構,軟體才有機會按照model作出來。
- OOP比較適合用來實作Model
- 函式導向比較不適合,但是可以作不是拿來給人使用,而是內部自行運作控制的系統,或是科學運算,Ex:OS,嵌入式系統
Entities,Value Objects,Services,Repositories,Factories,Aggregates,Layered Architecture,每一個都是一個Pattern,Model-Driven-Design就是作出一個Model,可以用這些Pattern實作的設計方式。
Layered Architecture
作業系統有分層,網路堆疊有分層,連生物的構造都有分層,為什麼軟體不該分層?我自己寫有分功能就不錯了,分層就距離很遠。在Layered Architecture中,只有上層能呼叫下層。同層之間的Module可以相互呼叫。
- User Interface:負責與使用者互動,GUI,CLI
- Application:薄薄的一層,用來協調Domain層不同原件的互動,此層不包含Business logic,也不會存任何Business data,可以存放user request的進度。等同MVC的C,表現User Stories的地方。
- Domain:表達Model的一層,也是軟體最有價值的地方,放Business logic,Business data。
- Infrastructure:底層系統功能,資料庫,系統功能,繪圖,網路。
Entities
代表在Model中的一個資料個體,在程式執行中具有唯一性,有自己的狀態,有自己的行為,應該要在Model開始建立的時候就要設計出來。Ex:Account in banking system,每個Account有ID,每個Accout之間就算資料相同,還是不一樣的Account。
Value Objects
代表在Model中不需唯一性的資料個體,理論上,應該把不需唯一性的資料個體全部定義為Value Object,最好是能設計成Immutable,可以大大減少系統複雜度與執行效率。有一個規則是,如果Value Objects會被不同Class間分享,那就應該定義為Immutable。Ex: Customer包含Address,Adress包含Street,City,State。Customer是一個Entity,Address沒有唯一性,所以是Value Object。
Services
不屬於任何Entities與Value Objects的行為,或是跨越好幾個Entities與Value Objects的行為,把多個類似的行為集合,定義為Service,Service不能有內部狀態,只有提供方法讓外界呼叫執行,而後得到結果。好處就是可以降低Object之間的耦兒,而且把行為的概念萃取出來,Service有以下三個特徵:
- 在Service裡面的操作不屬於任何Entity或是Value Object
- 在Service裡面的操作參考到Domain裡其他Objects
- 在Service裡面的操作沒有狀態
Ex:Banking system裡面的轉帳,從A帳戶轉到B帳戶,就可以是一個Service。而轉帳狀態也會有一個Entity來負責記錄。
Application Layer可以有Service,Domain Layer可以有Service,Infrastructure Layer也可以有Service,在建立Model是要把所屬的Layer分清楚。
Service的名稱要是The Ubiquitous Language,大家看得懂。
Modules
Layer是縱向,Modules則是橫向,包含一群相關的Entities,Value Objects,Services,對外提供定義好的接口供其他Module呼叫。有兩種聚合Module的方式:
- communicational cohesion:都在傳同類的資料
- functional cohesion:一起完成一系列相關的動作
Module也應該作Refactor,搬來搬去
Module的名稱也要是The Ubiquitous Language,大家看得懂。
Aggregates
一群相關的Enities與Value Objects集合在一起成為一個聚合體,對外提供服務,從外面看到的只有這個Aggregates,外界可以使用的也只有這個Aggregate,而Aggregate本身也是一個Entity,因為聚合其他Objects,所以是Root。當Aggregate裡面的某一個Objects改變的時候,Aggregate內相關的資料也會一起改變。而Aggregate外面的Object不會有任何變化。
- Aggregate可以包含Aggregate。所以可以有各式各樣的包含變化。
- Aggregate包含一群Objects。這些Objects資料互相關連,Aggregate負責保證這些Object的資料合理正確。
- 這一群Objects可以互相參考,但是不能參考外界的Objects。把複雜度局限在Aggregate裡。
- 外界只能參考Aggregate,不能直接參考裡面的Objects。外界不能破壞Aggregate內部資料運行的邏輯。
- 外界要改變Aggregate包含的Object內容,只能透過呼叫Aggregate的方法。所以外界修改都在Aggregate的保護之下。
- Aggregate把object丟出去只能丟copied value object。
由以上幾點,可以達成資料完整性與不變條件。Aggregates可以保證修改與刪除都不會出包,而新增就由Factory處理。
Ex:Customer是一個Aggregate,包含Address,ContactInfo。呼叫Customer.getAddress()會回傳一個Clone出來的Address,是一個Value Object。使用者修改成無效的Address,也不會影響Customer。當要修改Customer內的Address的時候,要呼叫Customer.setAddress(),內部會檢查Address是否有效。有效的話就Clone一份放到Customer裡面。
Factories
用來封裝Objects建立的流程與邏輯,所以建立Objects的知識,就是Domain的知識,全部都應該放在Factories裡面。因此透過Factories建立出來的Objects都是可用的。
為什麼要有Factorys?當Entities與Aggregates太大太複雜,使用者想要產生一個Entities或Aggregates時,必須要有足夠的知識才能產生,大大的增加使用的難度,而且產生出來的Entities或Aggregates,內容可能有問題,使用者還要想辦法把它設定正確,才能夠順利儲存。就算使用者將Entities或Aggregates設定正確了,這些使用的方式,邏輯,都會寫在外部,讓外部的邏輯更加複雜,並且破壞了封裝,還造成沒有重用性以及重複的程式碼。
Aggregate,Value Objects,Entities都可以用Factories來產生。
可以使用的方法如下:
- Factory Method->From Design Pattern
- Abstract Factory->From Design Pattern
- Constructor->只在Create Objects很簡單的狀況下使用
Ex:生成一個User,ID必須用查表的方式獲得,而且系統會有最大User數量限制,User是一個Interface,有三個實作分別是NormalUser,VIPUser,LimitedUser。每一種實作分別有不同的預設值。如果用Factories,則會是
- UserFactory.Create(UserType.NormalUser);
- UserFactory.Create(UserType.VIPUser);
- UserFactory.Create(UserType.LimitedUser);
所有需要創建User的知識就放在UserFactory裡面,外部使用者只需呼叫UserFactory.Create就可以得到一個可用的User。
Repositories
包裝所有儲存,取出Objects的邏輯,提供儲存Objects的地方,也就是Aggregates,Entities,Value Objects都可以儲存,使用者不需知道底層的儲存方式為何,可以是記憶體,可以是檔案,可以是SQL資料庫,可以是NoSQL資料庫,也可以是OOO。
- 因為隨時都可以透過Repository拿Objects,使用者(包含Domain內部),可以儘量不持有Objects的References。有需要再透過Repositories來拿。
- 提供單一的查詢機制,因此使用者也不需在意底層儲存媒介提供的查詢機制,當然也可以作快取。
- 因為與儲存媒介溝通,看起來很像處在Infrastructure Layer,其實不然,Repositories是在Domain Layer,也需提供Application Layer服務。
- Repositories與Factories有何不同?Factories負責Create Objects,而Repositories負責照顧已經存在的Objects,所謂已經存在,可能在記憶體,也可能在其他儲存媒介,只要經過Repository.save之後就算存在。
- Factories算是純Domain,而Repositories包含與Infrastructure的連結。
Refactoring Toward Deeper Insight
- 設計需要,洞察力、經驗、深思、才能。那我看這本幹嘛
- Domain model不是一次就可以做好,也需要透過迭代的方式來慢慢培育
- 所以Design不是作完就不動了,而是要持續Refine。
- 要怎麼Refine Design?把Domain的關鍵概念挖出來,呈現在陽光之下,讓Design有所突破:
- 與Domain專家討論,聽他講系統的角色,角色的行為,資料的流程,獲得的結果,找出之前沒有找到的概念。
- 深入了解Domain 專家所用的行話,與他/她的對話,字字句句都要弄懂。
- 在design中有不得不作但不合適的設計時,肯定有什麼隱含概念沒被包含在model中。這句真的很硬
- 參考Domain領域的書籍,航空、財務、流體力學、遊戲設計,因為Domain專家不是萬能,有參考書更好!
- Constraint,Processes,Specification三個作法可以協助把關鍵概念找出來
- Constraint:表達不變量的方式,例如一間教室只能有40人,這就是Constraint。實作的時候為了兼顧集中管理與Design,可以把Contraint放在對應的Class裡面,對應的不變量集中放置在某個檔案裡面。
- AirPlane包含很多零件,Wing、Engine,Missile,各自的Constraint可以寫WingConstraint,EngineConstraint,MissileConstraint,然後裡面用到的不變量,都集中放在AirPlaneConst裡面。如此可以一眼就看出AirPlane所有的不變量,各自的Contraint可以與各自的Class放一起。
- Processes:某件事情運作的程序,例如公文處理,主辦人先簽名,給直屬主管,再給其他部門主管,再給總經理。最適合作成Service的形式,如果與Domain專家討論出來的The Ubiquitous Language有明確講出這個Process,那就要明確的把它用Service作出來。
- Specification:Business Rule的限制,跨Entity的限制,用來測試一個Object是否滿足某些標準。例如一個三年級學生要修90學分才能升上四年級。這就是一個Specification。
- 簡單的Specification可以寫在Entity裡面;會牽扯到好幾個Service與Entities的複雜Specification,就應該抽取出來變成一個Object,將邏輯集合起來,不會四散在各地。
- 而這個Specification不一定要跟著Entity走,可以單獨拿出來用,Service內可用,Repository內可用,Applicaiton Layer也可用,寫起來麻煩可以用AOP包裝。
Preserving Model Integrity
了解如何把Model找出來,如何用Model作Design,接下來就用這個Design來實作,每一個迭代就Refine Model與Design,從此王子與公主就過著幸福快樂的生活?
- 最簡單、理想的狀況是保持單一、設計良好的Model
- 當有多個團隊在作同一個Project
- 大家平行寫Code
- 每個團隊負責Project的一部份
- 每一部份大部分都不是獨立,多少都有相關
- 分布在世界各地,只能用英文溝通!
- 不見得大家可以同時把資源用在完成自己的一部份上
- 現實世界就是,在大型專案中,保持單一Model很困難,也不是一定要。
- 多團隊開發不需勉強保持單一Model,可以分成多個Model,並遵守一定規則,就可以保持系統完整性。
Model Integration Pattern
Bounded Context
既然要切分Model,那Model要切的多小?最好切到可以指派給一個Team,方便作最有效率的溝通。什麼是Model Context?在DDD就是指Model所要被實作的外在環境與內在狀態,團隊有幾個人、誰是老大、用什麼開發流程、這個Model實作出來會被誰使用、使用語言、Codebase、Database Schema、開發環境,Coding Standard,Release方式等等。清楚的劃出一個界限,然後在開發期間不要越過這個界限,也不要讓外面的人或事進入這個界限。小專案小團隊就不必這麼複雜,但人一多,花在溝通的成本很高,如果在美國、中國、德國、日本的同事在同一天改同一個Class...這時候有Bounded Context才是明智的選擇。
範例:要作一個e-commerce的web application,提供註冊帳號、記錄使用者資訊、使用者登入、瀏覽商品、下訂單、把訂單送給出貨中心、產生各種販售報告、分析報告。
- 一開始的Model就是作一個具備所有功能的Web Application
- 發現賣東西與產生報告,似乎沒有太大關係,只有讀寫同一份資料。
- 建議分成兩個Model,一個是e-shop,一個是report,兩個Model可以分別開發,甚至可以作成兩個不同的Application。
- 需要有一個Messaging System來通知出貨中心,e-commerce只需送一個包含Value Objects的message到出貨中心。
可以看到以上設計流程是先規劃好Web與Report二者的Model,再來看有沒有需要分成兩個Application。Model先,實作方式後。
在4.的Value Objects不是訂單,訂單該存去資料庫。只是一個Notification,送出後就不會改變,也無需進資料庫,所以是Value Object。
Continuous Integration
- 不只是一般Code層級的CI,包含Model本身的CI,Model建立之後,會被改變,需要有作為來保持Model的完整性
- 當多人在同一個Modle上作業時,Model就會有破碎的傾向
- 如何維持在Bounded Context內的Model完整性
- 新概念整合進Model的流程
- Code Merge的流程,小Team每日Merge
- 自動build
- 自動測試
- 每一個Bounded Context都需要作Continuous Integration,以日為單位。
Context Map
- 大型的的產品會有多個Model,多個Bounded Context,一個Bounded Context一個Team。
- Context是一份文件,描繪出不同的Bounded Context的關係,可以是文字敘述,可以有流程圖。
- Common practice of design
- 定義Model
- 做出Design
- 劃分Bounded Context
- Create modules
- 不同Bounded Context間的關係接下來描述
Shared Kernel
- 兩個Team或多個Team之間分享部份的Domain Model,Code,Database Design。
- 實作速度最快,同樣的事情只要作一次,要拿資料直接撈。
- 相依性最高,在Shared Kernel隨便改個東西都會影響到別人。
- 要改變這些有共享的Kernel,都必須作團隊之間的溝通。
- 也要作Continuous Integration,不過頻率會比單一Bounded Context內部來的少,以周為單位。
Customer-Supplier
- 應用在一個A子系統單向依賴另一個B子系統,此時A是Customer,B是Supplier。
- 以上列e-commerce的範例,分為兩個model,一是e-shop,一是report,report可以是Customer,e-shop可以是Supplier
- Customer提供需求,Supplier接收需求,排定計畫實作。
- 需要有一個Conformity test suite把提供給Customer的Interface寫成Test放到Supplier的Test Suite裡面,這個Test就是Acceptance Test,由Supplier與Customer一起決定。
- Supplier有這些Acceptance Test,就可以放心改功能。
Conformist
- 當C、S兩個Team有Customer-Supplier的關係時,由於利害關係不同,時程不同與其他因素,S Team可能沒辦法提供完全符合C Team需求的內容。
- 很多人都是好人,很多人都是想作事情的人,但是每一個人又有自己的難處。
- S Team也許有幾個好人,願意提供C Team界面與資料,但C Team如果根據這幾個好人的一念之仁下去作設計,反而風險很大。
- 如果Supplier不給足夠的support,Customer就自己搞,用Separateed Way,這也是一種實務做法。
- 如果S Team提供的內容有限,但還是勉強可以用,可以用Anticorruption Layer包裝起來,隔離S Team不良的設計。
- 如果S Team提供的介面與資料完整,那就是Conformist出場的時候,C Team可以完全使用S Team的Model來作設計,這有點像Shared Kernel,不一樣的地方在於,Conformist的情況下,C Team是完全不能更動S Team的Model。
Anticorruption Layer
- 在界接舊的應用程式,或是界接其他團隊、其他公司的產品時,拿出來使用。
- 舊的應用程式,Model可能與新的完全不同。
- 其他公司產品的開發者,根本沒機會溝通,只能看看文件。
- 界接方式:
- 透過網路通訊協定
- 資料庫,超危險,因為放在資料庫的資料沒有任何Model可言
- Anticorruption Layer形成一道防護罩,用來作內部Model與外部Model的轉譯。
- 用Service Pattern來設計Anticorruption Layer,就是內部沒有狀態,只有提供方法
- 用Facade與Adapter來實作
- one Service, one Facade, one or many Adapter. 對於其中來往的Object,可以有Translator把要送到外部的Object包裝成外部系統看得懂的形式,反之亦然。
- 不應該有Business Logic,只有轉換邏輯。
Separate Ways
- 多個團隊在實作多個子系統,需要花很多時間在整合Code,整合測試,或是互相實作其他團隊需要的介面。當這些交互合作、整合太過於花時間,複雜度太高時,就可以考慮Separate Ways。
- 先決條件當然是多個子系統的Model可以作出沒有相關的設計。
- Model不同、程式語言、執行平台都可以不同。
- 可以有共通的使用者介面,例如一個Web Page,不同的連結一個導向Tomcat的Java Web Application,另一個連結導向Node.js的Web Application。
- 使用了Separate Ways後,分出去的子系統基本上是回不來的,所以要作這個設計決策時要三思。
Open Host Service
- 當很多subsystem都需要存取外部System,每個Subsystem都有一個Translator來處理資料與流程的對應,很容易就產生一堆重複且複雜的Code。
- 對於這個外部System,使用一個Open Host Service來對應內部Subsystem的需求。把Protocol設計的儘量通用,Desgin當然是用Service Pattern來設計(也就是內部沒有狀態,只有Operation)。
Distillation
- 目標就是蒸餾出Core Domain
- 大型系統就會有大型Domain,大型Domain就會有大型Model,就算是作了多次Refactor仍然保持巨大體積與複雜度。
- Distillation就是蒸餾出Core Domain,與附產品的Sub Domain
- 把通用的流成與資料分離成Sub Domain
- 以之前介紹過的Air Traffic Controller為例,用來作尋徑的Simple Routing System雖然有用,但是不是Air Traffic Controller這個系統的目的。所以在Air traffic controller裡面就是一個Sub Domain。
- Air traffic Controller真正的Core Domain就是飛行路線預測,避免飛機相撞。
- 找出Core Domain有什麼用?花最多的心力設計與實作,作出有彈性可以滿足現在對應未來的系統。
- Developer覺得Domain做完就沒用了,最新的技術才是Developer感興趣的東西。
- Core Domain對產品而言才是最重要。這與追求技術相互矛盾,但是成功的專案都有找到平衡點。
- Core Domain不是一次就可以找出來的,要透過不斷的Refine與Refactor。
- 分離出Core Domain與Sub Domain,還有一個用處,可以考慮用簡單的方式來實作Subdomain:
- 外面有在賣或是Open source的Solution,要考慮學習曲線、可維護性、導入難易度。
- 以外包的方式,還是會比內部開發的子系統難整合,要把與外包子系統溝通的介面定義完整。
- 使用Existing Model,真要自己作Sub Domain的話,設計部分可參考已經存在的Model,Design Pattern,Pattern-Oriented Software Architecture,Patterns of Enterprise Application Architecture,Enterprise Integration Patterns等等都可參考
- In-House Implementation,自己做最好整合,不過就是要時間,也要花費精力維護。
作者訪談使用DDD的重點
- 保持親自動手,Modelers也是要寫Code的。
- 專注在真實的運作情節,抽象化定要根據在真實會發生的Case來設計。
- 別想把DDD套用在所有地方,畫出Context Map來決定要把DDD套用在系統的哪個部分。也不用擔心越界,再重構回來就好,只要有對應措施,軟體可以是軟的。
- 多作實驗,預期會犯錯,Modeling是一種創造性的程序。