啰嗦幾句
年前的時候,在和 netfocus 兄,以及對 DDD 感興趣園友的探討過程中,我發現自己有很多不足的地方,對 DDD 的了解也只是皮毛而已,代碼寫的少,DDD 的基本概念也不是很清楚,空有一腔熱愛之情是做不了事的,后來我就多寫技術代碼,也記錄了很多的技術問題,這讓我收獲很多,.NET 開源等等一系列的事件,也讓我們 .NET 技術陣營看到了一絲希望。
后來,在探討的過程中,有很多我不知道的概念被討論,比如 CQRS、六邊形架構、事件溯源等等,我對這些概念是一竅不通的,像六邊形架構,我只知道六邊形有六個邊(莫笑),這讓我意識到,你只了解經典 DDD 架構,會讓你自己陷入一些困境,有時候不是你自己的設計問題,而是你的眼界被遮掩住了,你需要去探尋自己視野之外的東西,這樣才會有所進步。
其實,學習 DDD 最好的方式,就是用最真實的實際案例去運用,在運用的過程中,去發現問題并進行探討學習,這樣雖然會很艱辛,但收獲也是巨大的,除此之外,你還會發現另一個問題,就像在建高樓大廈的時候,雖然樓房的設計是世界最高水平,但是地基打不穩,空有一張設計圖紙又有什么用呢?
讀《實現領域驅動設計》這本書,其實在很早的時候就計劃好了,之前也讀了兩三章,大概是寫《三個問題思考實體和值對象》這篇博文的時候,讀了下實體、值對象和倉儲章節,因為是帶著問題讀的,所以并沒有很深入,只是想可以盡快從書中找到自己的答案。
昨天晚上,我大概讀了第一章《DDD 入門》的前半部分,有很多內容我覺得還是蠻有意思的,我希望可以把這些東西記錄下來,以防備自己的“健忘癥”。
由貧血導致的失憶癥
先來看書中提到的兩個病例測試:
- 你的領域對象中是不是主要是些共有的 getter 和 setter 方法,并且幾乎沒有業務邏輯,或者完全沒有業務邏輯-對象嘛,主要就是用來容納屬性值的?
- 軟件組件經常使用的領域對象是否包含了系統主要的業務邏輯,并且多數情況下你需要調用那些 getter 和 setter?你可能會將這樣的客戶代碼稱為服務層(Service Layer)或者應用層(Application Layer)代碼,也或者,如果這描述的是你的用戶界面,請回答“Yes”,然后好好反省一下,告誡自己一定不要再這么做了。
第一個問題是領域對象的定義,第二個問題是領域對象的調用,你的回答是什么?一個 Yes、一個 No?如果你是這樣的回答,作者給你這樣的分析:你可能是在自欺或者患上了由貧血癥導致的神經系統紊亂。哈哈,作者還蠻調皮的,回歸正題,考慮這兩個問題的時候,你可以和你正在做的項目進行對比考慮,是不是對你產生了一些共鳴呢?有人可能會說:唉呀媽呀,這不是我“萬能”三層架構里面的 Model 層和 BLL 層嘛?如果你這么想的話,對你的最終確認結果是:先生,你患上了貧血癥,而且還“貧”的不輕呢。
上面是從富有行為對象到貧血對象的時間線,凡事都有存在的理由,像貧血對象也是,它也是由多種因素導致并演化而來的,在作者敘述的這一部分內容中,我覺得主要概括為兩個因素:Microsoft Visual Basic 開發方式和早期 ORM 暴露共有屬性,ORM 暴露共有屬性這個我不是很懂,但是 Microsoft Visual Basic 開發方式對我還是蠻有影響的,記得在上大學的時候,老師講 Web Forms 和 Windows Forms 的課程,都是一拖一個控件,然后再設置控件的屬性,這樣一個項目基本就完成了,從那時候開始,“屬性”的概念就慢慢培養起來了,做一個項目之前,會先把一系列的 Model 屬性設計好,按照需求下面就是對這些屬性值的修改,最后就是把這些 Model 保存的數據庫中,過程就是這么個過程,有錯嗎?沒有,但是呢,好像建設一棟摩天大樓的設計不應該這么簡單吧?我們看下面的代碼(PDF 文件,不能復制,只能純手打):
public void saveCustomer(
String customerId,
String customerFirstName, String customerLastName,
String streetAddress1, String streetAddress2,
String city, String stateOrProvince,
String postalCode, String country,
String homePhone, String mobilePhone,
String primaryEmailAddress, String secondaryEmailAddress) {
Customer customer = customerDao.readCustomer(customerId);
if (customer == null) {
customer = new Customer();
customer.setCustomerId(customerId);
}
customer.setCustomerFirstName(customerFirstName);
customer.setCustomerLastName(customerLastName);
customer.setStreetAddress1(streetAddress1);
customer.setStreetAddress2(streetAddress2);
customer.setCity(city);
customer.setStateOrProvince(stateOrProvince);
customer.setPostalCode(postalCode);
customer.setCountry(country);
customer.setHomePhone(homePhone);
customer.setMobilePhone(mobilePhone);
customer.setPrimaryEmailAddress(primaryEmailAddress);
customer.setSecondaryEmailAddress (secondaryEmailAddress);
customerDao.saveCustomer(customer);
}
當時,看到這個 saveCustomer 方法中的代碼,我哈哈大笑了三聲,笑的不是別人,而是我自己,因為我之前寫過比這個 saveCustomer 方法還多的代碼,那個看起來更加臃腫,之前開發的是快遞業務系統,一個表多的話有近上百個字段,那修改這個表的屬性,就是像上面的代碼一樣,不同的是,我的比這個更多,一坨一坨的。比如上面,不管是地址變了沒變,你都是使用的 saveCustomer,那這個方法到底是什么含義呢?你也說不清楚,因為它看上去是那么的“萬能”,不過,也確實如此。因為你說不清一個方法的具體作用,這樣導致的結果就是失憶癥,原因是由貧血模型產生。
舉個例子,有一天,業務人員告訴 DBA(業務實際掌握人),要去掉 Customer 中的一個屬性,然后 DBA 就在 Customer 表中,把這個屬性對應的字段去掉了,但是 DBA 并沒有告知你,因為他覺得沒必要(你又不懂業務),但是,你發現項目突然報錯了,然后你就各種排查,最后發現是 saveCustomer 方法里面拋出的異常,然后你就開始一個一個比較 Customer 模型屬性和 saveCustomer 表字段,發現原來是少了一個字段,然后,你就和 DBA 干了起來。。。
以上純屬虛構,如有雷同,那就雷同吧,針對 saveCustomer 出現的問題,作者簡要總結了下:
- saveCustomer() 業務意圖不明確。
- 方法的實現本身增加了潛在的復雜性。
- Customer 領域對象根本就不是對象,而只一個數據持有器(data holder)。
以上的三大問題,就是導致“失憶癥”發生的根本原因。
Ubiquitous Language-通用語言
在領域驅動設計中,通用語言是非常重要的一個概念,在書中的第一章節中,作者也反復提到這個概念,并進行了詳細解釋,我之前認為通用語言就是代碼,領域專家和開發人員都可以看懂的代碼,但這種理解是片面的,領域專家是業務專家,他又不是開發人員,怎么能看懂代碼呢?來看幾個對話:
- 很明顯,通用語言是一種業務語言。
抱歉,不是。 - 通用語言必須采用工業標準術語。
不完全是。. - 通用語言是領域專家專用的。
對不起,不是。 - 通用語言是團隊自己創建的公用語言,團隊中同時包含領域專家和軟件開發人員。
對了。
在理解通用語言之間,還有個問題也容易混淆,至少我是這樣的,那就是設計和實現的區別,有人就說了:很簡單啊,這有什么好混淆的,設計就是我們畫的業務流程圖或者是 UML,實現就是代碼。仔細一想,好像也確實是這樣,但是在領域驅動設計中,領域模型的設計是通過與領域專家進行討論確定的,畫的各種設計圖,并不是領域驅動的設計,而只是我們建設討論的一種方式而已,那設計是什么?設計其實就是代碼,代碼就是設計,所以,在領域模型的設計中,不要把設計和實現的概念區分開,他們其實是一個概念而已。
在上面對話的第四點中,通用語言是團隊自己創建的公用語言,什么意思呢?公用的意思,就是所表達的內容領域專家和開發人員都懂,語言其實不是說話的語言,中文?英文?都不是,也不是代碼語言,它其實是溝通的一種方式,大家都可以理解的一種方式,一個團隊有一個屬于自己的公用語言,范圍是僅限于團隊內容,可能這個公用語言在其他團隊就不適用了,也就說,它是團隊成員自己創建的,當然也不是一下就可以創建出來的,是一步一步進行完善,需要每一個領域專家和開發人員的參與。
不管怎么理解,公用語言概念中,有一點是非常重要的,那就是溝通,可能說多了不好理解,作者就舉了一個示例:
你會發現,業務描述的不同,最后實現的代碼就會千差萬別,也就是說開發人員和領域專家的溝通很重要,當然開發人員的理解能力也很重要,很多的方方面面,就組成了通用語言的概念。
如果你不知道怎么理解通用語言,你可以嘗試用一個最小業務用例去實現并理解,比如,修改客戶名稱業務用例,你可以先把這個業務用例中所涉及的概念抽離出來,比如客戶、客戶名稱,修改客戶名稱,需要首先找到這個具體的客戶,當然,可能會有一些限制操作,但不管怎樣,“修改客戶名稱”這個業務所表達的結果,就是這個客戶的名稱要被修改,所以你要實現修改客戶名稱這個操作,可能實現的代碼就是下面這樣:
public void changeCustomerPersonalName(
String customerId,
String customerName) {
Customer customer = customerRepository.customerOfId(customerId);
if (customer == null) {
throw new IllegalStateException("Customer does not exist.");
}
customer.changePersonalName(customerName);
}
上面的實現代碼和之前的 saveCustomer 代碼,很明顯的區別,首先,你修改客戶名稱,如果你使用的是 saveCustomer,你需要在方法參數中,傳遞一大堆的 null,而且整個方法內部充滿了一些沒必要的操作,而且你把 saveCustomer 方法描述給領域專家聽,我想他們肯定也會不知所云,相反,changeCustomerPersonalName 就是通用語言的一種表達方式。
一個業務用例,從一開始的討論,到最后的實現,整個過程中所涉及的方方面面,其實都可以理解為通用語言的表現,關于通用語言的界定問題,作者還提到幾點:
- 通用語言在團隊范圍內使用,并且只表達一個單一的領域模型。
- 只有當團隊工作在一個獨立的限界上下文中時,通用語言才是“通用”的。
- “通用語言”并不表示全企業、全公司或者全球性的萬能的領域語言。
- 每個限界上下文都有自己的通用語言,而有時語言間的術語可能有重疊的地方。
- 。。。
以上幾點警示你,通用語言有一定的界定,并不是所有團隊,也不是一個項目,而是一個單一的領域模型或者一個獨立的界定上下文,你可以把它理解為一個領域模型或者一個獨立界定上下文的具體表現,或者稱之為過程體現。
以上只是簡單的概念整理,并沒有一些實際意義,具體的體會只能在實踐中更加深刻,就記錄到這!
文章列表