- 實現領域驅動設計
- (美)沃恩·弗農
- 5915字
- 2020-09-05 00:21:59
為什么我們需要DDD
事實上,在前面我已經提到了一些應該采用DDD的原因。冒著有悖DRY原則(Don’t repeat yourself,不要做重復的事情)的風險,我重新說說我們需要采用DDD的原因。
? 使領域專家和開發者在一起工作,這樣開發出來的軟件能夠準確地傳達業務規則。當然,對于領域專家和開發者來說,這并不表示單單地包容對方,而是將他們組成一個密切協作的團隊。
? “準確傳達業務規則”的意思是說,此時的軟件就像如果領域專家是編碼人員時所開發出來的一樣。
? 可以幫助業務人員自我提高。沒有任何一個領域專家或者管理者敢說他對業務已經了如指掌了,業務知識也需要一個長期的學習過程。在DDD中,每個人都在學習,同時每個人又是知識的貢獻者。
? 關鍵在于對知識的集中,因為這樣可以確保軟件知識并不只是掌握在少數人手中。
? 在領域專家、開發者和軟件本身之間不存在“翻譯”,意思是當大家都使用相同的語言進行交流時,每人都能聽懂他人所說。
? 設計就是代碼,代碼就是設計。設計是關于軟件如何工作的,最好的編碼設計來自于多次試驗,這得益于敏捷的發現過程。
? DDD同時提供了戰略設計和戰術設計兩種方式。戰略設計幫助我們理解哪些投入是最重要的;哪些既有軟件資產是可以重新拿來使用的;哪些人應該被加到團隊中?戰術設計則幫助我們創建DDD模型中各個部件。
就像其他高回報率的投入一樣,DDD需要我們在時間和精力上都有所投入。但是,考慮到我們在開發軟件的過程中經常遇到的各種問題和挑戰,這樣的投入是值得的。
難以捉摸的業務價值
開發能夠傳遞真正業務價值的軟件和開發普通的軟件是不同的。具有真正業務價值的軟件能夠很好地符合業務戰略,并且可以將競爭優勢融合到解決方案中。此時的軟件并不是關于技術的,而是關于業務的。
業務知識從來就沒有被集中過。開發團隊必須在多方之間權衡各種需求,并確定其中的優先級。同時,團隊成員的技能也是良莠不齊的。在獲得所有的信息之后,團隊所面臨的問題在于:如何確定某種需求確實能夠傳遞真正的業務價值?還有,我們如何去發現并暴露出這些業務價值,如何安排它們之間的優先級,并且如何實現它們?
在開發過程中,最大的鴻溝之一便存在于領域專家和開發者之間。通常來說,領域專家將關注點放在交付業務價值上,而開發者則將注意力放在技術實現上。當然,并不是說開發者的動機是錯誤的,而是說開發者的眼光被自然而然地吸引到了實現層面上。即便讓領域專家和開發者一同工作,他們之間的協作也只是表面的,這時在所開發的軟件中便產生了一種映射:將業務人員所想的映射到開發者所理解的。這樣一來,軟件便不能完全反映出領域專家的思維模型。隨著時間的推移,這種鴻溝將增加軟件的開發成本。而隨著開發者轉到其他項目或者離職,本應該駐留在軟件中的領域知識也就丟失了。
另一個問題發生在當多個領域專家之間存在分歧的時候。這是很有可能發生的,因為每個專家只是熟悉某個或者某些特定的領域。另外,在某個領域里找不到真正的專家也是可能的,此時,有人可能對該領域有所了解,但是他更像一個業務分析員。這些問題將導致相互矛盾的軟件模型。
更糟的是,軟件的技術實現可能錯誤地改變軟件的業務規則。比如,ERP軟件通常需要修改業務操作以滿足某個特定用戶的需求,因此ERP的成本不能單以使用許可和維護費用來計算,對業務規則的修改所產生的成本遠遠大于前兩者。另外一個相似的例子是當開發團隊將業務需求翻譯成軟件功能的時候。這對于業務、用戶和合作方來說都是一筆很大的成本。還有,技術上的翻譯和解釋是沒有必要的,并且在使用適當開發方式的情況下是可以避免的。解決方案才是主要的投入。
DDD如何幫助我們
DDD作為一種軟件開發方法,它主要關注以下三個方面:
1. DDD將領域專家和開發人員聚集到一起,這樣所開發的軟件能夠反映出領域專家的思維模型。這并不意味這我們將精力都花在了對“真實世界”的建模上,而是交付最具業務價值的軟件。有時在實用和理想之間存在沖突,根據它們的互異程度,在DDD中我們將選擇實用性。
領域專家將和開發人員一起創建一套適用于領域建模的通用語言。通用語言必須在全隊范圍之內達成一致;所有成員都使用通用語言進行交流,通用語言也是對軟件模型的直接反映。請注意,雖然團隊中同時包含領域專家和開發人員,但并不是“我們”和“他們”的關系,團隊中只有“我們”的概念。
通用語言也有助于促使原本存在分歧的領域專家們達成一致意見。此外,通過將領域知識傳達給所有的團隊成員,包括開發人員,整個團隊也將更具凝聚力。我們甚至可以認為,這是每個公司都應該有的對于知識型工作者的起碼訓練。
2. DDD關注業務戰略。雖然說戰略(Strategic)設計自然地包含了戰術設計,但是戰略設計關注更多的則是業務的戰略方向。它幫助我們定義不同團隊之間的組織關系,并在這些關系有可能導致項目失敗的時候提供早期預警。DDD的戰略設計用于清楚地界分不同的系統和業務關注點,這樣可以保護每個業務層面的服務。更進一步,這將指引我們如何實現面向服務架構(serviceoriented architecture)或者業務驅動(business-driven architecture)架構。
3. 通過使用戰術設計建模工具,DDD滿足了軟件真正的技術需求。這些戰術設計工具使開發人員能夠按照領域專家的思維模型開發軟件。同時,所開發出來的軟件是可測試的,能夠盡量避免錯誤,能執行服務層面協議(Service-Level Agreement,SLA),具有很好的伸縮性,并且允許分布式計算。DDD的最佳實踐同時包含了高層的架構性實踐和底層設計實踐,關注業務規則和數據不變性,并且可以對業務規則起到保護作用。
通過這種方式開發軟件,你和你的團隊將能成功地交付真正的業務價值。
處理領域復雜性
在使用DDD時,我們首先希望將它應用在最重要的業務場景下。對于那些可以輕易替換的軟件來說,你是不會有所投入的。相反,值得你投入的是那些重要的、復雜的東西,因為這些東西將為你帶來可觀的回報。正因如此,我們將這樣的模型命名為核心域(Core Domain,2),而那些相對次要的稱為支撐子域(Supporting Subdomain,2)。那么現在,我們需要搞明白的是,“復雜”到底是什么意思?
DDD的作用是簡化,而不是復雜化
在使用DDD時,我們應該采用最簡單的方式對復雜領域進行建模,而不是使問題變得更加復雜。
不同的業務領域對于復雜的定義是不一樣的。另外,不同的公司所面臨的挑戰不一樣;成熟度不一樣;軟件開發能力也不一樣。因此,與其去定義什么是復雜的,還不如定義什么是重要的。這時,你的團隊和管理層應該做出決定:你們開發的軟件系統是否值得做出DDD投入。
DDD計分卡:使用表1.1來決定你的項目是否值得做出DDD投入。如果你的項目情況在某行的描述范圍之內,那么請在右邊的列中記上相應的分數,最后將這些分數相加得到總分。如果得分為7分或者以上,那么,你應該考慮使用DDD了。
表1.1 DDD計分卡

續表

通過對以上DDD計分卡打分,我們可以得出以下結論:
當我們在復雜性問題上犯錯時,我們很難輕易地扭轉頹勢。
這意味著我們應該在項目計劃早期便對簡單性和復雜性做出判斷,這將為我們節約很多時間和開銷,并免除很多麻煩。
一旦我們做出了重要的架構決策,并且已經在該架構下進行了深入地開發,通常我們也被綁定在這個架構下了,所以在決定時一定要慎重。
如果你對以上幾點產生了共鳴,表明你已經在認真地思考問題了。
貧血癥和失憶癥
貧血癥嚴重危害著人類健康,并且伴隨有危險的副作用。當貧血領域對象(Anemic Domain Object)[Fowler,Anemic]被首次提出來時,它并不是一個博得贊美的詞匯,它描述的是一個缺少內在行為的領域對象。奇怪的是,人們對于貧血領域對象的態度褒貶不一。問題在于,多數開發者認為這樣的領域對象是正常的,他們并沒有意識到這是一個嚴重的問題。
你是否想知道你所建模型的健康狀況呢?如果你突然患上了技術上的“憂郁癥”,這里你可以做個自我檢查。你可能心情愉悅,也可能無比恐懼。通過表1.2中的步驟開始檢查吧。
表1.2 領域對象病歷

如果你對以上兩個問題的回答都是“No”,表明你的領域對象是健康的。
如果都是“Yes”,表明你的領域對象已經病得不輕了,這便是貧血對象。好消息是,你是可以獲得幫助的,繼續往下讀吧。
如果你對其中一個回答“Yes”,而另一個回答“No”,你可能是在自欺或者患上了由貧血癥導致的神經系統紊亂,此時你應該怎么辦呢?回到第一個問題重新來一遍,不要著急,要確保對兩個問題都回答“Yes”。
正如[Fowler,Anemic]所說,貧血領域對象是不好的,因為你花了很大的成本來開發領域對象,但是從中卻獲益甚少。比如,由于存在對象-關系阻抗失配(Object-Relational Impedance),開發者需要將很多時間花在對象和數據存儲之間的映射上。這樣的代價太大,而收益太小。我得說,你所說的領域對象根本就不是領域對象,而只是將關系型數據庫中的模型映射到了對象上而已。這樣的領域對象更像是活動記錄(Active Record)[Fowler,P of EAA],此時你可以對架構做個簡化,然后使用事務腳本(Transaction Script)[Fowler,P of EAA]進行開發。
為什么會有貧血領域對象
如果說貧血領域對象是由設計不當造成的,為什么還有如此多的人認為他們的領域對象是健康的呢?其中一個原因是:貧血領域對象反映了一種自然的過程式的編程風格,但我并不認為這是首要原因。軟件業中有很多開發者都是學著示例代碼做開發的,這并不是什么壞事,只要示例代碼本身是好的。然而,通常情況是,示例代碼只是用盡可能簡單的方式來展示某個特定的概念或者API特性,而并不強調要遵循多好的設計原則。一些極度簡化的示例代碼總是包含了大量的getter和setter,于是這些getter和setter隨著示例代碼每天被程序員們原封不動地來回復制。
還有歷史的影響。Microsoft的Visual Basic對我們現在的軟件開發產生了很大的影響。我并不是說Visual Basic是門不好的語言和集成開發環境(IDE),因為它的確是種高效的開發方式,并且在某些方面對軟件開發產生過正面的影響。當然,有些人可能會拒絕Visual Basic的直接影響,但是最終它卻間接地影響著每一個程序員。請注意表1.3中的時間線。
表1.3 從富有行為的對象到貧血對象的時間線

這里,我想談及的是對象屬性和屬性列表帶來的影響。對象屬性和屬性列表都得益于getter和setter的支持,而Visual Basic的窗體設計器將getter和setter變得過于流行了。你需要做的只是將自定義控件拖到窗體上,然后編輯控件的屬性列表,大功告成,一個功能完備的窗體程序開發完畢。如果直接采用C語言的Windows API來開發相同的窗體,可能需要幾天時間,而采用Visual Basic只是幾分鐘的事情。
那這和貧血領域對象有什么關系呢?JavaBean標準最早是用來輔助Java的可視化設計工具的,旨在將Microsoft的Active X開發方式帶到Java平臺。Java此舉希望開創一個第三方自定義控件市場,就像Visual Basic一樣。此后不久,幾乎所有的框架和類庫都涌入到了JavaBean潮流中,其中包括Java本身的SDK/JDK和第三方類庫,比如Hibernate。在.NET平臺推出之后,這樣的趨勢還在繼續。
有趣的是,在早期的Hibernate版本中,所有需要持久化的領域對象都必須暴露公有的getter和setter,不管是對于簡單類型的屬性,還是對復雜類型皆如此。這意味著,即便你希望將自己的POJO(Plain Old Java Object)設計成富含行為的對象,你都必須將對象的內部暴露給Hibernate以保存或重建對象。誠然,你可以隱藏公有的JavaBean接口,但是多數開發者都懶得這樣做,或者甚至都不知道為什么應該這么做。
我應該考慮在DDD中使用對象-關系映射(Object-Relational Mapping,ORM)嗎?
前面主要是從歷史的角度對Hibernate進行了批評。現在,Hibernate已經不需要對象暴露getter和setter了,它甚至可以對對象屬性進行直接操作。我將在后面的章節中講到在使用Hibernate或其他持久化機制時如何避免貧血對象。
此外,多數的Web框架依然只支持JavaBean規范。如果你想將一個Java對象顯示在網頁上,該Java對象最好是支持JavaBean規范的。如果你想將HTML表單中的數據傳到一個Java對象中,該Java對象也最好是支持JavaBean規范的。
市場上的幾乎每種框架都要求對象暴露公有屬性。這樣一來,多數開發者只能被動地接受那些貧血對象。于是我們便到了“到處都是貧血對象”的地步。
看看貧血對象都對你的模型做了些什么
好吧,我們同意這已經是煩人的即成事實。但是這些無處不在的貧血對象和失憶癥又有什么關系呢?當你在閱讀一個貧血領域對象的示例代碼時,比如應用服務(4,14)中的事務腳本,你通常會看到類似如下的代碼片段:


刻意保持例子的簡單
必須得承認,以上代碼并不表示一個有趣的領域,但是卻幫助我們看到了一個欠妥的設計,我們可以將其重構成更好的模型。這里我們關注的并不是如何保存Customer數據,而是如何向模型中添加業務價值,即便就這個例子本身來說意義并不大。
以上代碼完成了什么功能呢?事實上,以上代碼的功能是相當強大的。不管一個Customer是新建的還是先前存在的;不管是Customer的名字變了還是他搬進了新家;不管是他的家用電話號碼變了還是他有了新的移動電話;也不管他是改用Gmail還是有了新的E-mail地址,這段代碼都會保存這個Customer。哇,好厲害的方法啊!
情況真是這樣的嗎?其實,我們并不知道saveCustomer()方法的業務場景。為什么一開始會創建這個方法?有人知道它的本來意圖嗎,還是它原本就是用來滿足不同業務需求的?幾周或幾個月之后,我們便將這些忘得一干二凈了。你不相信?那請看看該方法的下一個版本:


我得說,以上方法還算不上糟糕到了極點。很多時候數據-映射(datamapping)代碼將變得非常復雜,此時大量的業務邏輯便不能反映在代碼里了。
現在,除了customerId之外,所有的參數都是可選的,我們至少可以在某些業務場景下使用該方法。但是,我們就能說這是好的代碼嗎?我們如何測試這段代碼以保證在錯誤的業務場景下該段代碼不應該保存一個Customer呢?
都不用討論過多的細節我們便知道,在很多情況下該方法是不能正常工作的。可能數據庫約束會防止對非法狀態的保存,但你是不是又得去查看數據庫啦?你會在Java對象屬性和數據庫表的列名之間輾轉反側,然后可能發現你缺少數據庫約束或者約束并不完全。
你可能會查看很多客戶代碼,然后比較代碼歷史,找出saveCustomer()的來龍去脈。你會發現,沒有人能夠解釋這個方法為什么會成為現在這個樣子,也沒有人知道究竟有多少客戶代碼在正確地使用saveCustomer()方法。要自己去搞明白這里面的緣由,你得花上幾個小時甚至幾天的時間。
牛仔的邏輯
AJ:“這哥們兒疑惑了,他不知道是應該吃土豆呢,還是吃牛肉?”

這個時候,領域專家是幫不上忙的,因為他們看不懂代碼。即便領域專家能夠看懂代碼,他可能也會被這段代碼搞得一頭霧水。我們難道就不能用另外一種方式來改善這段代碼嗎?如果可以,怎么修改?
上面的saveCustomer()至少存在三大問題:
1. saveCustomer()業務意圖不明確。
2. 方法的實現本身增加了潛在的復雜性。
3. Customer領域對象根本就不是對象,而只一個數據持有器(data holder)。
我們將這種情況稱為“由貧血癥導致的失憶癥。”在實際項目中,這種癥狀發生得太多了。
等等!
這時你可能在想,“我們的設計都是在白板上進行的啊。我們會繪制設計框圖,只有大家都達成一致時,我們才開始編碼實現。”
如果情況是這樣,那么請不要將設計和實現分開。記住,在實施DDD時,設計就是代碼,代碼就是設計。換句話說,白板圖并不是設計,而只是我們討論模型的一種方式。
現在你可能有些擔心,“我要如何才能做到更好的設計呢?”用不著擔心,你會成功的,繼續讀下去。
- MySQL高可用解決方案:從主從復制到InnoDB Cluster架構
- 企業數字化創新引擎:企業級PaaS平臺HZERO
- 從零開始學Hadoop大數據分析(視頻教學版)
- 工業大數據分析算法實戰
- MySQL從入門到精通(第3版)
- 醫療大數據挖掘與可視化
- 深入淺出Greenplum分布式數據庫:原理、架構和代碼分析
- SQL應用及誤區分析
- 新手學會計(2013-2014實戰升級版)
- Cognitive Computing with IBM Watson
- 數據產品經理寶典:大數據時代如何創造卓越產品
- Applying Math with Python
- 2D 計算機視覺:原理、算法及應用
- 圖解LeetCode初級算法(Python版)
- Building Multicopter Video Drones