DDD與Repository
- 2020 年 8 月 19 日
- 筆記
Repository已經不是什麼新鮮概念了。DDD模型自2004年提出,發展至今已經16年了。但是不少企業卻無法實施,其原因也很簡單:DDD是基於需求的,而很多並不理解需求;DDD是容易實現的,而很多設計者並不會編程。這種情況就有一些兩頭不討好,而如果有辦法結合統一的話,則會非常好用。
學習Repository的過程中,最先要進行的是思想的轉變。在過往的編程過程中,大家往往將目光聚焦在CRUD中,導致每個程式設計師首先想的是我如何使用SQL實現我所要的目標。而在DDD過程中,實踐者應當將目光聚焦中功能上,首先將需求分解為若干個功能,然後再將功能進行組合。
舉例而言,某系統擁有組織機構和用戶功能。組織機構樹的每個部門下屬若干個職位,每個職位都有用戶擔任,每個用戶可以出任多個部門的多個職位。這時,系統設計師告訴你這裡要有以下功能:
部門管理(部門的CRUD,部門下職位的CRUD,職位與部門的CRUD)
用戶管理(用戶的CRUD)
講到這裡,很容易看出,這裡其實有四張表,也就是部門,職位,用戶和用戶職位關聯表。表關係也容易理出,部門與職位1對多,職位與用戶多對多。
如果你使用的是DAO思想(或者說分層思想),那麼需求分析做到這一步也就結束了。你可以直接通過上述內容整理出你需要實現的介面,即每張表的CRUD。然後在前端實現一個介面,每個介面調用相關的介面程式也就寫完了。比如,其中一個介面可能是這樣的:
[Route(「~/api/SetPosition」)]
public void SetPosition(Guid userId, Guid positionId);
那麼,現在問題來了。需求發生了一個變更,來了一個全新的需求,客戶說我現在需求每個部門的更改必須通過流程進行。即當部門資訊發生變更時,必須層層審核,最後才通過後,才能在更新數據。這個審核過程甚至包含了一部分關鍵職位的人員變化。
這時,那個坑人的系統設計師又站出來了,給了你一系列功能變化表:
部門修改申請(部門修改申請CRUD,部門申請審核,部門申請同步到部門表,部門申請同步到職位表)
看上去,這個設計很美好「自頂向下逐步細化」分解的也非常舒服。但是,你仔細研究一下就發現這裡有兩個巨大的坑:
1、新建部門修改申請。在部門修改申請時,試問是否要將以前的部門數據複製到這張申請表中?如果你不複製,那了不得了,全部門所有手動數據全部要用戶自行輸入此表,那恐怕最終用戶會和你鬧的不可開交——這什麼垃圾軟體?!而如果你打算實現他,那我告訴你,這張可怕的部門表裡,欄位不多,100個(呵呵)。
2、如果你說功能1其實是必須實現的新功能,和設計關係不大。那麼你再觀察,將部門申請同步到部門這個功能。他絕對可以細分為「修改部門」和「修改職位」兩個子功能,而這兩個子功能其實是之前的介面就實現的。那麼,你是否為之前的介面留下了復用性?仔細看看之前介面的實現程式碼,你就會悲劇的發現,70%的可能性那個介面是無法復用的,因為查詢程式碼其實不太一樣。
那這只是我隨手說的一個需求變更,如果有更多的需求變化呢?那麼雖然程式碼還是能夠復用一部分,設計空間釋放也不會太麻煩。但是,仔細評判你的程式碼和設計,就會發現原來優雅而簡潔的可復用設計的復用性越來越低,原來整齊而易讀的程式碼的可讀性越來越差。這就是人間悲劇。
而這時,Repository的思想從天而降,他也許能夠為你可憐的程式碼帶來一些讓你驚喜的變更。如果使用DDD的思想設計上述內容,首先你需要確定領域。顯而易見的,這裡的領域可以這樣劃分:
用戶領域:添加用戶,刪除用戶,修改用戶,修改用戶的職位,移除用戶的職位
部門領域:添加部門,刪除部門,修改部門,查詢部門下的職位,查詢部門下的用戶
職位領域:添加職位,修改職位,刪除職位,查詢職位下的用戶,將用戶添加到職位中,將用戶從職位中移除
註:這裡,如果是我寫程式碼,我很可能會把「部門領域」和「職位領域」合併。這個並無不可,因為兩者其實沒有那麼明顯的邊界。
在這個設計中,可以看到其實有些功能是重複的,比如說「修改用戶的職位」和「將用戶添加到職位中」。但是,在領域設計中,我卻將其認為是兩個不同的功能,因為他們的主體不一樣。對前者而言,我先查出用戶,函數的參數是「用戶ID」和「職位名稱」,這裡使用出字元串的職位名稱,即意味著對於用戶領域來說,他不需要認識「職位」這個類。對於後者而言,我先查出職位,函數參數是「職位」和「一個或者多個用戶ID」。這意味著,對於職位領域來說,他也不需要認識用戶這個類。
這裡可以看到,領域之間,耦合度很低。其實達到了最小知識原則所要求的內容。但是,實現過程中,可能會有這樣的疑問,將職位添加到用戶過程中,難道你不需要判斷用戶是否存在嗎?當然,判斷還是要判斷的,但是我完全可以不認識用戶這個類。通過將「用戶職位關係表」中的「用戶ID」欄位與用戶表中的「ID」欄位做出外鍵關係,完全可以讓數據幫我保證數據有效性。我只需要做一個簡單的異常處理即可。
另外,耦合度低不等於不能耦合,在這裡查詢一次用戶表,我認為也沒有突破什麼界限,所以完全沒有問題。
在設計完領域後,需要再設計邊界,也就是說由哪些類將這些功能全部暴露給外界。這時可以這麼設計:
部門類:添加職位,修改職位,刪除職位,查詢職位下的用戶,將用戶添加到職位中,將用戶從職位中移除
用戶類:查詢我所在的部門和職位
用戶服務類:用戶的CRUD
部門服務類:部門的CRUD
這裡,實際是將部門當作了職位的聚合。這只是我隨手寫的設計,沒有實踐過也不知道有沒有什麼問題。但我想大致應當是正確的。這時,我就將所有功能都通過這幾個類暴露在外界。在考慮這些內容的情況下,再來上述需求時,問題就明確了,他需要新建一個領域:部門修改申請。
部門修改申請:通過部門新建修改申請,通過舊的修改申請新建修改申請,審核修改申請,將修改申請同步到部門中,將修改申請同步到職位中。
現在再來看之前的兩個大坑。問題1其實是規避不了。因為這個就是新功能,規避的唯一辦法就是加錢,錢到位了功能也就到位了。而問題2確實就簡單了,因為你可以直接調用暴露在「修改職位功能」將申請表中的用戶給到對應職位,也可以通過調用「修改部門功能」直接將部門資訊反向同步,而不需要考慮程式碼是否優雅,因為這裡就是調用一個函數,並不存在優雅與否的問題。
再到以後,如果再有新功能,哪怕你還是需要釋放設計空間。但你在重構的時候,已經整理過的功能就不需要整理第二遍。你只需要交被釋放出的設計空間全部放回領域中,重構的工作量大大減少。而這,就是我所看重的DDD的核心優勢。
針對到實現層面,之前那些亂七八糟的領域功能,其實就是Repository,他的出現自然而又簡單。你所需要的只是簡單的變化一下自己的思想,多寫幾十行程式碼,僅此而已。
最後,稍稍總結一下。完成以上內容的核心和關鍵其實並不是你對DDD了解多少。而真正有效的是你對需求了解多少,你認為需求有多少內容可能發生變化。對需求把握才是軟體設計的核心。任何設計思想,設計模式都基於對需求的理解。我個人對軟體思想的重要理解:
不基於需求任何想法都空談,不理解需求任何程式碼都是胡說,不把握變化任何設計都是假想。
與君共勉。