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