摘要:但是從客觀上而言,業務代碼本身由于包含了業務領域的知識,復雜可以說是先天的屬性。原來業務代碼也可以這么簡潔而優雅。因為此內部業務框架做的事情很多,篇幅有限,這里僅對最具借鑒意義的領域建模思考作介紹。其實也是很典型的一種業務代碼編寫方式。
本文主要作為筆者閱讀Eric Evans的《Domain-Driven Design領域驅動設計》一書,同時拜讀了我司大神針對業務代碼封裝的一套業務框架后,對于如何編寫復雜業務代碼的一點粗淺理解和思考。
ps,如有錯誤及疏漏,歡迎探討,知道自己錯了才好成長么,我是這么認為的,哈哈~
背景介紹忘記在哪里看到的句子了,有 “看花是花,看花不是花,看花還是花” 三種境界。這三個句子恰好代表了我從初入公司到現在,對于公司代碼的看法的三重心路歷程。
學習動機 “看花是花”得益于我司十幾年前一幫大神數據庫表模型設計的優異,一開始剛進公司的時候,很是驚嘆。通過客戶端配配屬性,一個查詢頁面和一個資源實體的屬性控件頁面就生成好了。
框架本身負責管理頁面屬性和查詢頁面的顯示內容,以及右鍵菜單與 js 函數的綁定關系,同時當其他頁面需要調用查詢及屬性頁面時,將頁面的實現和調用者頁面做了隔離,僅通過預留的簡單的調用模式及參數進行調用。這樣復雜功能的實現則不受框架限制與影響,留給業務開發人員自己去實現,客觀上滿足了日常的開發需求。
我司將繁雜而同質化的查詢及屬性頁面開發簡化,確實客觀上減輕了業務開發人員的工作壓力,使其留出了更多精力進行業務代碼的研究及開發工作。
這套開發機制的發現,對我來說收獲是巨大的,具體的實現思路,與本文無關,這里就不作過多贅述了。
這種新奇感和驚嘆感,就是剛開始說的 “看花是花” 的境界吧。
那么 “看花不是花” 又該從何說起呢?前面說了,框架完美的簡化了大量重復基礎頁面的開發工作,同時,框架本身又十分的克制,并不干涉業務代碼的開發工作。
但是從客觀上而言,業務代碼本身由于包含了業務領域的知識,復雜可以說是先天的屬性。隨著自己工作所負責業務的深入,接觸更多的業務必然也不再是框架所能涵蓋的資源查詢與屬性編輯頁面。
同時考慮到業務編寫人員本身相對于框架人員技術上的弱勢,以及業務領域本身具有的復雜性的提升,我一開始所面對的,就是各種的長達幾百行的函數,隨處可見的判斷語句,參差不齊的錯誤提示流程,混亂的數據庫訪問語句。在這個階段,對業務代碼開始感到失望,這也就是之后的 “看花不是花” 的境界吧
“看花還是花”有一天,我突然發現面對紛繁而又雜亂的業務代碼,總還有一個模塊 “濯清漣而不妖”,其中規范了常用的常量對象,封裝了前后端交互機制,約定了異常處理流程,統一了數據庫訪問方式,更重要的是,思考并實現了一套代碼層面的業務模型,并最終實現了業務代碼基本上都是幾十行內解決戰斗,常年沒有 bug,即便有,也是改動三兩行內就基本解決的神一般效果(有所夸張,酌情理解:P)。
這是一個寶庫。原來業務代碼也可以這么簡潔而優雅。
唯一麻煩的就是該模塊的業務復雜,相應的代碼層面的業務模型的類層次結構也復雜,一開始看不太懂,直到我看了 Eric Evans的《Domain-Driven Design領域驅動設計》才逐漸有所理解。
因為此內部業務框架做的事情很多,篇幅有限,這里僅對最具借鑒意義的領域建模思考作介紹。
業務場景我主要負責傳輸網資源管理中的傳輸模塊管理。這個部分涉及相對來說比較復雜的關聯關系,所以如果代碼組織不夠嚴謹的話,極易繞暈和出錯,下面以一張簡單的概念圖來描述一下部分實體對象間的關聯關系。
如圖,簡單來說時隙和通道是一對多的父子關系,同時業務電路和多段通道間的下級時隙,存在更復雜的一對多承載關系。
所以這個關系中復雜的地方在哪里呢?理解上經常會繞混,電路創建要選多段通道時隙,通道本身要管理多個時隙。這樣時隙和通道以及電路同時存在了一對多的聯系,如何去準確的理解和區分這種聯系,并將之有效的梳理在代碼層面就很需要一定技巧。
稍微拓展一下,改改資源類型,把業務電路換為傳輸通道,把傳輸通道換為傳輸段,這套關系同樣成立。
另外,真實的業務場景中,業務電路的下層路由,不僅支持高階通道,還支持段時隙,端口等資源。
整體設計從業務場景中我們可以看到,資源實體間的模型關系其實有很多相類似的地方。比如大體上總是分為路由關系,和層級關系這么兩種,那么如何才能高效的對這兩種關系進行代碼層面的建模以高效的進行復用,同時又保留有每個資源足夠的拓展空間呢?
傳統思路我們先來考慮一下,按照傳統的貧血模型去處理傳輸通道這個資源,針對傳輸通道的需求,它是如何處理的呢?
最粗陋的常規模型,其實就是依據不同類型的資源對需求進行簡單分流,然后按照管理劃分 Controller 層,Service 層,Dao 層。各層之間的交互,搞得好一點的會通過抽象出的一個薄薄的 domain 域對象,搞的不好的直接就是 List,Map,Object 對象的粗陋組合。
代碼示例/** * 刪除通道 不調用ejb了 * 業務邏輯: * 判斷是否被用,被用不能刪除 * 判斷是否是高階通道且已被拆分,被拆不能刪除 * 邏輯刪除通道路由表 * 清空關聯時隙、波道的channel_id字段 * 將端口的狀態置為空閑 * 邏輯刪除通道表 * @param paramLists 被刪除通道,必填屬性:channelID * @return 是否刪除成功 * @throws BOException 業務邏輯判斷不滿足刪除條件引發的刪除失敗
* 通道非空閑狀態
* 高階通道已被拆分
* 刪除通道數據庫操作失敗
* 刪除HDSL系統失敗
* @return 成功與否 */ public String deleteChannel(String channelId){ String returnResult = "true:刪除成功"; Mapcondition = new HashMap (); condition.put("channelID",channelId); condition.put("min_index","0"); condition.put("max_index","1"); boolean flag=true; List
上面這些代碼,是我司n年前的一段已廢棄代碼。其實也是很典型的一種業務代碼編寫方式。
可以看到,比較關鍵的幾個流程是 :
空閑不能刪除(狀態驗證)—>路由刪除->端口置為空閑(路由資源置為空閑)->資源實體刪除
其中各個步驟的具體實現,基本上都是通過調用 dao 層的方法,同時配合若干行service層代碼來實現的。這就帶來了第一個弊端,方法實現和 dao層實現過于緊密,而dao層的實現又是和各個資源所屬的表緊密耦合的。因此即便電路的刪除邏輯和通道的刪除邏輯有很相似的邏輯,也必然不可能進行代碼復用了。
如果非要將不同資源刪除方法統一起來,那也必然是充斥著各種的 if/else 語句的硬性判斷,總代碼量卻甚至沒有減少反而增加了,得不償失。
拓展思考筆者曾經看過前人寫的一段傳輸資源的保存方法的代碼。
方法目的是支持傳輸通道/段/電路三個資源的保存,方法參數是一些復雜的 List,Map 結構組合。由于一次支持了三種資源,每種資源又有自己獨特的業務判斷規則,多情況組合以后復雜度直接爆炸,再外原本方法的編寫人員沒有定期重構的習慣,所以到了筆者接手的時候,是一個長達500多行的方法,其間充斥著各式各樣的 if 跳轉,循環處理,以及業務邏輯驗證。
解決辦法面對如此棘手的情況,筆者先是參考《重構·改善既有代碼設計》一書中的一些簡單套路,拆解重構了部分代碼。將原本的 500 行變成了十來個幾十行左右的小方法,重新組合。
方案局限重構難度及時間成本巨大。
有大量的 if/else 跳轉根本沒法縮減,因為代碼直接調用 dao 層方法,必然要有一些 if/else 方法用來驗證資源類型然后調用不同的 dao 方法
也因為上一點,重構僅是小修小補,化簡了一些輔助性代碼的調用(參數提取,錯誤處理等),對于業務邏輯調用的核心代碼卻無法進行簡化。service層代碼量還是爆炸
小結站在分層的角度思考下,上述流程按照技術特點將需求處理邏輯分為了三個層次,可是為什么只有 Service 層會出現上述復雜度爆炸的情況呢?
看到這樣的代碼,不由讓我想到了小學時候老師教寫文章,講文章要鳳頭,豬肚,豹尾。還真是貼切呢 :-)
換做學生時代的我,可能也就接受了,但是見識過高手的代碼后,才發現寫代碼并不應該是簡單的行數堆砌。
業務情景再分析對于一個具體的傳輸通道A的對象而言,其內部都要管理哪些數據呢?
資源對象層面
自身屬性信息
路由層面
下級路由對象列表
層次關系層面
上級資源對象
下級資源對象列表
可以看到,所有這些數據其實分為了三個層面:
作為普通資源,傳輸通道需要管理自身的屬性信息,比如速率,兩端網元,兩端端口,通道類型等。
作為帶有路由的資源,傳輸通道需要管理關聯的路由信息,比如承載自己的下層傳輸段,下層傳輸通道等。
作為帶有層次關系的資源,傳輸通道需要管理關聯的上下級資源信息,比如自己拆分出來的時隙列表。
更進一步,將傳輸通道的這幾種職責的適用范圍關系進行全業務對象級別匯總整理,如下所示:
各種職責對應的業務對象范圍如下:
同時具有路由和層次關系的實體:
傳輸時隙、傳輸通道、傳輸段、傳輸電路
具有路由關系的實體:
文本路由
具有層次結構關系的對象:
設備、機房、端口
僅作為資源的實體:
傳輸網管、傳輸子網、傳輸系統
拓展思考 微觀層面以傳輸通道這樣一個具體的業務對象來看,傳統的貧血模型基本不會考慮到傳輸通道本身的這三個層次的職責。但是對象的職責并不設計者沒意識到而變得不存在。如前所述的保存方法,因為要兼顧對象屬性的保存,對象路由數據的保存,對象層次結構數據的保存,再乘上通道,段,電路三種資源,很容易導致復雜度的飆升和耦合的嚴重。
因此,500行的函數出現某種程度上也是一種必然。因為原本業務的領域知識就是如此復雜,將這種復雜性簡單映射在 Service 層中必然導致邏輯的復雜和代碼維護成本的上升。
宏觀層面以各個資源的職責分類來看,具備路由或層次關系的資源并不在少數。也就是說,貧血模型中,承擔類似路由管理職責的代碼總是平均的分散在通道,段,電路的相關 Service 層中。
每種資源都不同程度的實現了一遍,而并沒有有效的進行抽象。這是在業務對象的代碼模型角度來說,是個敗筆。
在這種情況下就算使用重構的小技巧,所能做的也只是對于各資源的部分重復代碼進行抽取,很難自然而然的在路由的業務層面進行概念抽象。
既然傳統的貧血模型沒法應對復雜的業務邏輯,那么我們又該怎么辦呢?
新的架構 代碼示例@Transactional public int deleteResRoute(ResIdentify operationRes) { int result = ResCommConst.ZERO; //1:獲得需要保存對象的Entity OperationRouteResEntity resEntity = context.getResEntity(operationRes,OperationRouteResEntity.class); //2:獲得路由對象 ListentityRoutes = resEntity.loadRouteData(); //3:刪除路由 result = resEntity.delRoute(); //4:釋放刪除的路由資源狀態為空閑 this.updateEntitysOprState(entityRoutes, ResDictValueConst.OPR_STATE_FREE); //日志記錄 resEntity.loadPropertys(); String resName = resEntity.getResName(); String resNo = resEntity.getResCode(); String eport = "刪除[" + ResSpecConst.getResSpecName(operationRes.getResSpcId()) + ": " + resNo + "]路由成功!"; ResEntityUtil.recordOprateLog(operationRes, resName, resNo, ResEntityUtil.LOGTYPE_DELETE, eport); return result; }
上述代碼是我們傳輸業務模塊的刪除功能的service層代碼片段,可以看到相較先前介紹的代碼示例而言,最大的不同,就是多出來了個 entity 對象,路由資源的獲取是通過這個對象,路由資源的刪除也是通過這個對象。所有操作都只需要一行代碼即可完成。對電路如此,對通道也是如此。
當然,別的service層代碼也可以很方便的獲取這個entity對象,調用相關的方法組合實現自己的業務邏輯以實現復用。
那么這種效果又是如何實現的呢?
概念揭示首先我們得思考一下,作為一個類而言,最重要的本質是什么?
答案是數據和行為。
照這個思路,對于一個業務對象,比如傳輸通道而言,進行分析:
在數據層面,每個通道記錄了自身屬性信息及其關聯的傳輸時隙、傳輸段、傳輸電路等信息數據。
在行為層面,每個通道都應該有增刪改查自身屬性、路由、下級資源、綁定/解綁上級資源等行為。
那么在具體的業務建模時又該如何理解這兩點呢?
答案就是這張圖:
可以看到大體分為了三種類型的元素,
Context(上下文容器):
程序啟動時,開始持有各個 DataOperation 對象
程序運行時,負責創建 Entity 對象,并將相應的 DataOperation 對象裝配進 Entity 對象實例中
Entity(實體對象):每個用到的資源對象都生成一個 Entity 實例,以存放這個對象特有的實例數據。
DataOperation(數據操作對象):不同于 Entity,每類用到的資源對象對應一個相應的 DataOperation 子類型,用以封裝該類對象特有的數據操作行為
ps,雖然我這里畫的 Entity&DataOperation 對象只是一個方框,但實際上 Entity&DataOperation 都有屬于他們自己的 N 多個適用與不同場景的接口和模板類
數據管理筆者是個宅男,因為并木有女朋友,又不喜歡逛街,所以買東西都是網購。這就產生了一個很有意思的影響——隔三差五就要取快遞。
可是快遞點大媽不認識我,我也不是每天出門帶身份證。這就很尷尬,因為我每次總是需要和大媽圍繞 “Thehope 就是我” 解釋半天。
所以每次解釋的時候,我都在想,如果我帶了身份證或者其他類似的證件,該有多方便。
什么是 Entity我們一般認為,一個人有一個標識,這個標識會陪伴他走完一生(甚至死后)。這個人的物理屬性會發生變化,最后消失。他的名字可能改變,財務關系也會發生變化,沒有哪個屬性是一生不變的。然而,標識卻是永久的。我跟我5歲時是同一個人嗎?這種聽上去像是純哲學的問題在探索有效的領域模型時非常重要。
稍微變換一下問題的角度:應用程序的用戶是否關心現在的我和5歲的我是不是同一個人?
—— Eric Evans《領域驅動設計》
簡單的取快遞或許使你覺得帶有標識的對象概念并沒有什么了不起。但是我們把場景拓展下,你不光要完成取快遞的場景,如果你需要買火車票呢?如果還要去考試呢?
伴隨著業務場景的復雜化,你會越來越發現,有個統一而清晰的標識概念的對象是多么的方便。
再來看看 Eric Evans 在《領域驅動設計》如何介紹 Entity 這個概念的:
確定標識一些對象主要不是由它們的屬性定義的。它們實際上表示了一條“標識線”(A Thread of Identity),這條線經過了一個時間跨度,而且對象在這條線上通常經歷了多種不同的表示。
這種主要由標識定義的對象被稱作 Entity。它們的類定義、職責、屬性和關聯必須圍繞標識來變化,而不會隨著特殊屬性來變化。即使對于哪些不發生根本變化或者生命周期不太復雜的 Entity ,也應該在語義上把它們作為 Entity 來對待,這樣可以得到更清晰的模型和更健壯的實現。
得益于我司數據庫模型管理的細致,對于每條資源數據都可以通過他的規格類型id,以及數據庫主鍵id,獲得一個唯一確定標識特征。
如圖:
這里舉出的 Entity 的屬性及方法僅僅是最簡單的一個示例,實際業務代碼中的 Entity,還包括許多具備各種能力的子接口。
引入Entity如圖:
可以看到 entity 對象實際上分為了兩個主要的接口,RouteEntity 和 HierarchyEntity。
其中 RouteEntity 主要規定要實現的方法是 addRoute(), 即添加路由方法
其中 HierarchyEntity 主要規定要實現的方法是 addLowerRes() 與 setUpperRes() ,即添加子資源對象和設置父資源兩種方法。
那么這兩個接口是如何抽象建模得到的呢?
確定功能的邊界從微觀的實例對象層面來看,因為每個實例都可能擁有完全不一樣的路由和層級關系,所以我們建模時候,用抽象出的 Entity 概念,表示哪些每個需要維護自己的屬性/路由/層次關聯數據的對象實例。
從高一層的類的層次去分析,我們可以發現,對路由的管理,對層次結構的管理,貫穿了傳輸電路,傳輸通道,傳輸段,傳輸時隙等很多業務類型。所以這個時候就需要我們在接口層面,根據業務特征,抽象出管理不同類型數據的 Entity 類型,以實現內在關聯關系的復用。
因此我們對 Entity 接口進行細化而建立了的 RouteEntity 和 HierarchyEntity 兩個子接口,比如
Entity 需要維護自己的 id 標識,屬性信息。
RouteEntity 就需要內部維護一個路由數據列表。
HierarchyEntity 就需要維護一個父對象和子資源列表。
這樣通過對不同的 Entity 管理的數據的職責與類型的進一步明確,保證在不同場景下,做到使用不同的 Entity 就可以滿足相應需求。。。。的數據前提 :P
拓展思考既然 Entity 概念的引入是為了解決各資源對象具體實例的實例數據的存儲問題。那么各個資源對象特有的行為操作怎么滿足呢?比如傳輸通道和傳輸電路都有自己的表,起碼在dao層的操作就肯定不一樣,再加上各個資源自己獨特的增刪改查驗證邏輯,如果這些行為都放在 Entity 中。。。妥妥的類型爆炸啊~
另外,將數據與行為的職責耦合在一起,從領域建模的思想上就是一個容易混淆而不明智的決定。
站在微觀角度來說,每個 Entity 實例所管理的實例數據是不同的,而同一類資源的行為操作(它的方法)卻是無狀態的。
站在宏觀角度來說,具有路由特征的或者具有層次特征一簇實體,有抽象共性的價值(比如都需要管理路由列表或者父子對象信息),而涉及具體的行為實現,每種具體的資源又天然不完全相同。
這里我們可以再思考下前文貼的兩段代碼,當我們沒有 Entity 對象時,許多應該由 Entity 進行存儲和管理的數據,就不得不通過 map/list 去實現,比如上文的第一段代碼。這就帶來第一個弊端,不到運行時,你根本不知道這個容器內存放的是哪種業務規格的資源。
第二個弊端就是,當你使用 map/list 來代替本應存在的 Entity 對象時,你也拒絕了將對象的行為和數據整合在一起的可能(即不可能寫出resEntity.loadRouteData() 這樣清晰的代碼,實現類似的邏輯只能是放在 Service 層中去實現,不過放在 Service 又增加了與具體資源邏輯的耦合)
所以,以數據和行為分離的視角,將業務對象以策略模式進行解耦,抽離成專職數據管理的 Entity 對象,以及專職行為實現的 DataOperation 簇對象,就顯得非常有價值了。
行為管理 引入 DataOperation接下來有請出我們的 DataOperation 元素登場~
以傳輸通道為例,對于傳輸通道的所屬路由而言,常用的功能無非就是的增刪改查這幾個動作。
確定變化的邊界還是從微觀的實例對象層面先進行分析
業務行為邏輯會因為操作的實體數據是傳輸通道A,或者傳輸通道B 而變得不同嗎?答案是不會。
正如數據庫行記錄的變化不引起表結構的變化一樣,本質上一類資源所擁有的行為和對象實例的關系,應該是一對多的。
所以只要都是傳輸通道,那么其路由增刪改查的行為邏輯總是一致的。
結合某一著名設計原則:
找出應用中可能需要變化之處,把它們獨立出來,不要和那些不需要變化的代碼混在一起
所以我們應該將資源不變的行為邏輯抽離出來,以保證 Entity 可以專注于自己對數據的管理義務,達到更高級的一種復用。
這也就是為什么需要抽象 DataOperation 概念的原因之一。
進一步從類的層次去分析
不同種類的資源,其具體的數據操作行為必然是存在差別的(比如與數據庫交互時,不同資源對應的表就不同)。
所以不同種類的業務對象都必然會有自己的 DataOperation 子類,比如 TrsChannelDataOperation、TrsSegDataOperation 等,以確保每類業務對象獨特的數據操作邏輯的靈活性。
再進一步去分析
在更高的層級上去分析,靈活性我們因為實現類的細化已經具備了,那么復用的需求又該怎么去滿足呢?
與 Entity 對象一樣,我們可以在具體的 TrsChannelDataOperation、TrsSegDataOperation 等實體類之上,抽象出 RouteResDataOperation、HierarchyResDataOperation 等接口,規定好應該具備的方法。
Entity 對象面對需要調用 DataOperation 的場景,就以這些接口作為引用,從而使路由或者層次處理的業務代碼從細節的實現中解放出來。
拓展思考這里可以仔細思考一下,Entity 和 DataOperation 應該在什么時候建立好二者之間的聯系呢?
小結我們已經分析好了對象的數據和行為該如何建模,那么,我們又該如何將這二者統一起來呢?
有請我們的第三類元素,Context 登場~
組裝先來看看這樣一個例子:
汽車發動機是一種復雜的機械裝置,它由數十個零件共同協作來侶行發動機的職責 — 使軸轉動。我們可以試著設計一種發動機組,讓它自己抓取一組活塞并塞到氣缸中,火花塞也可以自己找到插孔并把自己擰進去。但這樣組裝的復雜機器可能沒有我們常見的發動機那樣可靠或高效。相反,我們用其他東西來裝配發動機。或許是一個機械師,或者是一個工業機器人。無論是機器還是人,實際上都比二者要裝配的發動機復雜。裝配零件的工作與使軸旋轉的工作完全無關。裝配者的功能只是在生產汽車時才需要,我們駕駛時并不需要機器人或機械師。由于汽車的裝配和駕駛永遠不會同事發生。因此將這兩種功能合并到同一個機制中是毫無意義的。同理,裝配復雜的復合對象的工作也最好與對象要執行的工作分開。
——Eric Evans《領域驅動設計》
與發動機小栗子相類似,代碼中我們當然可以通過構造器的方式用到哪個對象再組裝哪個對象。不過比較一下這樣兩段代碼:
沒有 Context 元素的代碼:
@Transactional public int deleteResRoute(ResIdentify operationRes, boolean protectFlag) { ... //1.獲取需要保存對象的Entity OperationRouteResEntity resEntity = new TrsChannelResEntity(); if(ResSpecConst.isChannelEntity(operationRes.getResSpcId())){ ComponentsDefined component = new TrsChannelDataOperation(); resEntity.initResEntityComponent(conponent); } ... }
有了 Context 元素以后
@Transactional public int deleteResRoute(ResIdentify operationRes, boolean protectFlag) { ... //1.獲取需要保存對象的Entity OperationRouteResEntity resEntity = context.getResEntity(operationRes,OperationRouteResEntity.class); ... }
是不是立竿見影的效果!
為什么需要 Context事實上前文對 Entity 和 DataOperation 只是領域建模的第一步,只是個雛形。而這里的 context 對象,才是畫龍點睛的那一筆。
為什么這么說呢?在此之前,我也見過公司內許多其他的模塊對業務邏輯做過的復雜抽象,但是因為調用的時候需要調用者親自調用構造器生成實例,導致使用成本太高。尤其是人員流動比較大的模塊,新人天然不懂復雜的業務對象關系,這就使的業務開發人員很難持續使用業務模型對象,最終導致代碼模型形同虛設。
對象的功能主要體現在其復雜的內部配置以及關聯方面。我們應該一直對一個對象進行提煉,直到所有與其意義或在交互中的角色無關的內容已經完全被剔除為止,一個對象在它的生命周期中要承擔大量的職責。如果再讓復雜對象負責其自身的創建,那么職責的過載將會導致問題產生。——Eric Evans《領域驅動設計》
為了避免這樣的問題,我們有必要對 Entity 等實體對象的裝配與運行進行解耦實現,這也即是我們的 Context 元素的主要職責之一。比如上述兩段代碼,在其他元素不做改變的情況下,僅僅出于對職責的明確而引入的 Context 元素,對業務代碼編寫卻有了質的提升。
但實際上,正如一開始的小栗子說的,“無論是裝配機還是裝配師,都比要裝配的發動機要復雜”,我們的 context 所執行的邏輯其實也是相當復雜的,但只要對客戶(這里的客戶指的是使用 context 的業務代碼,下文同)幫助更大,即便再復雜也構不成不去做的理由,下面我們就來聊聊這個實質上的復雜工廠是如何運作的。
引入 ContextOperationResEntity resEntity = context.getResEntity(resIdentify);
在 Context 中,加載操作僅有一行,看起來是不是非常清晰,非常簡單?可惜背后所需思考的問題可一點都不簡單:)
首先,我們先來思考下 Context 在這行代碼中都完成了哪些事情吧:
public OperationResEntity getResEntity(ResIdentify identify) { OperationResEntity entity = getResEntity(identify.getResSpcId()); entity.setIdentify(identify); return entity; } public OperationResEntity getResEntity(String resSpcId) { ResEntity entity = factory.getResEntity(resSpcId); if(entity instanceof OperationResEntity){ ResComponentHolder holder = componentSource.getComponent(resSpcId); if(entity instanceof ContextComponentInit){ ((ContextComponentInit)entity).initResEntityComponent(holder); } }else{ throw new ResEntityContextException("資源規格:"+resSpcId+",實體類:"+entity.getClass().getName()+"未實現OperationResEntity接口"); } return (OperationResEntity)entity; }
上面就是 context 在獲取目標 entity 對象時所做的一些具體操作,可以看到,主要完成了這么三件事:
獲取 Entity 實例
獲取 DataOpeartion 實例(持有于上述方法中的 hoder 對象中)
將 Entity 和 DataOperation 裝配起來
那接下來我們就仔細分析下這三個步驟應該怎么實現吧~
獲取 Entity在本節的一開始,我們就舉了兩個例子,對比了有 context 幫我們封裝 Entity 與 DataOperation 組合關系,與缺少 context 幫我們封裝組合關系時的區別。具體來說,優勢在與這么兩點:
簡化了業務開發人員使用 Entity 對象的成本,使其天然傾向于調用框架模型,便于保證后期業務領域模型的統一性
減少了客戶代碼(service)中類似 new TrsChannelDataOperation() 這樣的硬編碼,客觀上便于 service 層構建更為通用而健壯的實現
轉過頭來再思考下,我們的 Entity 對象與 DataOperation 對象又是否天然存在一種非常復雜多變的動態組合關系呢?
通常,我們在實際運行時才能確定 service 中某個 Enity 的具體規格及其應該持有的 DataOperation對象。如果由業務代碼開發人員在調用處手動初始化,未免太過復雜,也不可避免的需要通過許多 If/ELSE 判斷來調整運行分支,這樣看代碼復雜度還是居高不下,那我們前面洋洋灑灑分析那么多又還有什么意義呢。
實際上,類似 Entity 與 DataOperation 之類的動態(調用處才知道具體的組合關系)組合關系在很多優秀的代碼中都有應用,比如我們熟知的 Spring。
或許我們也需要借鑒一波 Spring 的處理思路 ^_^
從 Spring 延伸開來我們都知道 Spring 最著名的一個賣點就是 IOC,也就是我們俗稱的控制反轉/依賴注入。
它將 Bean 對象中域的聲明和實例化過程解耦,將對象域實例的管理與注入責任,從開發人員移交至 Spring 容器。也正因如此,這種設計從源頭上即減少了開發人員在域實例化過程中的硬編碼,為對象間的組合提供了更為清晰便捷的實現。 ——TheHope:P
先明確了 IOC 的最大功用之一就是將對象間如何組合的責任從開發者肩上卸下,我們再繼續分析這個過程的實現中的兩個要點。
首先容器必須具有創建各個對象的能力
其次容器必須知道各個對象間的關聯關系是怎樣的
來,我們看看 Spring 加載 Bean 的步驟:
bean工廠初始化時期: 加載配置文件 --> 初始化 bean 工廠 --> 初始化 bean 關聯關系解析器 --> 加載并緩存 beanDefinition
bean工廠初始化完成之后: 獲取 beanDefinition --> 根據 beanDefinition 生成相應的 bean 實例 --> 初始化 bean 實例中的屬性信息 --> 調用 bean 的生命周期函數
可以看到 bean 工廠初始化時,便解析好了所有 bean 的 beanDefinition ,同時維護好了一個 beanName 與 beanDefinition 的 map 映射關系,而 beanDefinition 內部存儲好了 bean 對象實例化所需的所有信息。同時也解析好了 bean 之間的注入關系。
因此,當 beanFacory 初始化完備的時候,實際上,Spring 就已經具備獲取任意一個的 Bean 實例對象的所有基礎信息了。
拓展思考看到這里你有沒有發現,Spring 加載 bean 的第二步操作,根據某種標識獲取目標對象實例的過程,不就是常規情況下一個工廠的目標作用嗎,那 Spring 在流程上要加一步初始化工廠的操作呢?Spring 的工廠與普通的工廠模式又有什么異同呢?
為了屏蔽代碼中 new 一個構造器之類的硬編碼,我們都學習過工廠模式,當類型變化不是很多的時候,可以使用工廠模式進行封裝,當變化再多些的時候我們可以借助抽象類,用個抽象工廠進行封裝,將這種組合變換關系的復雜度分散到組合關系與繼承關系中去。
只是 Spring 中的 bean 比較特別,其屬性信息變化的情況實在是太多了,甚至 bean 之間的組合關系都是不固定的,很有可能出現 A 關聯了 B ,B 又關聯了 C,C 又...這時候如果還使用抽象工廠,業務上為了支持自己需要的組合情況,每多一層組合關系,那就需要我們動態繼承抽象類,相比XML,這顯然太過復雜了。
所以 Spring 為了支持這樣的變化,也是為了職責的清晰,將 BeanFactory 生成一個具體的 bean 時所需的信息專門抽象出來,用 XML 去由框架使用者自行維護,XML 內的信息在 Spring 內部即轉化為一簇 BeanDefinition 對象來管理,BeanFactory 的職責,則圍繞 BeanDefinition 劃分為了兩個階段:
讀取并緩存 beanDefinition 信息的 beanFactory 初始化階段
使用 beanDefinition 信息動態生成 bean 實例的后 beanFactory 初始化階段
小結如此這般,ABC問題中最為靈活且復雜的關聯關系,即由工廠/抽象工廠中預先設計轉化為了框架使用者自行維護。嘿,好一招騰籠換鳥~
組裝Entity一不小心就講多了,我們的 Entity 與 DataOperation 的關聯關系遠沒有那么復雜,不過我們可以仿照 Spring 建立 BeanName 與 BeanDefinition 映射關系的思想,在容器啟動時將我們的 Entity 與 DataOperation 組合關系加載好,實現后續使用時,獲取確定的 Entity 同時容器自己幫我們注入好需要的 DataOperation。
待續
小結待續。。。
參考資料Eric Evans的《Domain-Driven Design領域驅動設計》
聯系作者zhihu.com
segmentfault.com
oschina.net
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/67400.html
摘要:年加入有贊作為兼聯合創始人,目前在有贊管理著多人的技術團隊,帶領團隊致力于打造中國領域最好的開店軟件解決方案。訪談內容如下,還請大家多提建議和反饋,大不了繼續去騷擾崔玉松老師。 前不久,獲悉有贊科技發布了個有贊云,據說開發者隨便搞搞,分分鐘便可以上線一個商城,略有不明覺厲之感。好不容易抓到了正在度假的有贊 CTO 兼聯合創始人崔玉松老師,就毫不專業地用微信發了一堆問題列表過去。好在玉松...
摘要:前言本文給大家分享的題目是基于微服務以及的高可用架構探索與實現。比如說年大地震的時候我正好在東京,當時在做一個金融系統的相關工作。那次大地震導致很多很多的問題,雖然大地震不是在東京發生,但是還是給我們的系統造成了影響。 前言 本文給大家分享的題目是《基于DevOps、微服務以及K8S的高可用架構探索與實現》。整個企業的高可用架構面臨很多的挑戰,面向微服務、容器化以及敏態交付,是我們現在...
摘要:最近發現文章老是被竊取,有些平臺舉報了還沒有用。最后不了了之,產品很配合,但是內驅力不強。為什么內驅力不強,因為給他帶來的收益不夠。所以在千個團隊中實行可能有千套不同的方案。最近發現文章老是被竊取,有些平臺舉報了還沒有用。請識別我的id方丈的寺院。 摘要 DDD領域驅動設計,起源于2004年著名建模專家Eric Evans發表的他最具影響力的著名書籍:Domain-Driven Design...
摘要:但周自恒輕描淡寫地說,這是理性分析之后的結果,談不上多艱難。到今年月,是他做全職爸爸的周年。對此,周自恒建議老爸們雖然無法天天陪孩子學習,但是得了解自己孩子思維的發育特點,在哪方面比較敏感,在孩子的培養方向和計劃上更多地參與進來。 showImg(https://segmentfault.com/img/bVbtYNo); 哥哥:爸爸我問你,有一種鯊魚,它的頭像錘子,是海底的雜食動物,...
閱讀 1217·2023-04-25 20:31
閱讀 3723·2021-10-14 09:42
閱讀 1494·2021-09-22 16:06
閱讀 2660·2021-09-10 10:50
閱讀 3529·2021-09-07 10:19
閱讀 1778·2019-08-30 15:53
閱讀 1176·2019-08-29 15:13
閱讀 2823·2019-08-29 13:20