怎樣寫好業務代碼——那些年領域建模教會個人東西

本文主要做爲筆者閱讀Eric Evans的《Domain-Driven Design領域驅動設計》一書,同時拜讀了我司大神針對業務代碼封裝的一套業務框架後,對於如何編寫複雜業務代碼的一點粗淺理解和思考。html

ps,若有錯誤及疏漏,歡迎探討,知道本身錯了纔好成長麼,我是這麼認爲的,哈哈~java

背景介紹

忘記在哪裏看到的句子了,有 「看花是花,看花不是花,看花仍是花」 三種境界。這三個句子剛好表明了我從初入公司到如今,對於公司代碼的見解的三重心路歷程。數據庫

學習動機

「看花是花」

得益於我司十幾年前一幫大神數據庫表模型設計的優異,一開始剛進公司的時候,非常驚歎。經過客戶端配配屬性,一個查詢頁面和一個資源實體的屬性控件頁面就生成好了。segmentfault

框架自己負責管理頁面屬性和查詢頁面的顯示內容,以及右鍵菜單與 js 函數的綁定關係,同時當其餘頁面須要調用查詢及屬性頁面時,將頁面的實現和調用者頁面作了隔離,僅經過預留的簡單的調用模式及參數進行調用。這樣複雜功能的實現則不受框架限制與影響,留給業務開發人員本身去實現,客觀上知足了平常的開發需求。後端

我司將繁雜而同質化的查詢及屬性頁面開發簡化,確實客觀上減輕了業務開發人員的工做壓力,使其留出了更多精力進行業務代碼的研究及開發工做。緩存

這套開發機制的發現,對我來講收穫是巨大的,具體的實現思路,與本文無關,這裏就不做過多贅述了。
這種新奇感和驚歎感,就是剛開始說的 「看花是花」 的境界吧。架構

「看花不是花」

那麼 「看花不是花」 又該從何提及呢?前面說了,框架完美的簡化了大量重複基礎頁面的開發工做,同時,框架自己又十分的剋制,並不干涉業務代碼的開發工做。框架

可是從客觀上而言,業務代碼自己因爲包含了業務領域的知識,複雜能夠說是先天的屬性。隨着本身工做所負責業務的深刻,接觸更多的業務必然也再也不是框架所能涵蓋的資源查詢與屬性編輯頁面。dom

同時考慮到業務編寫人員自己相對於框架人員技術上的弱勢,以及業務領域自己具備的複雜性的提高,我一開始所面對的,就是各類的長達幾百行的函數,隨處可見的判斷語句,良莠不齊的錯誤提示流程,混亂的數據庫訪問語句。在這個階段,對業務代碼開始感到失望,這也就是以後的 「看花不是花」 的境界吧ide

「看花仍是花」

有一天,我忽然發現面對紛繁而又雜亂的業務代碼,總還有一個模塊 「濯清漣而不妖」,其中規範了經常使用的常量對象,封裝了先後端交互機制,約定了異常處理流程,統一了數據庫訪問方式,更重要的是,思考並實現了一套代碼層面的業務模型,並最終實現了業務代碼基本上都是幾十行內解決戰鬥,常年沒有 bug,即使有,也是改動三兩行內就基本解決的神通常效果(有所誇張,酌情理解:P)

這是一個寶庫。原來業務代碼也能夠這麼簡潔而優雅。

惟一麻煩的就是該模塊的業務複雜,相應的代碼層面的業務模型的類層次結構也複雜,一開始看不太懂,直到我看了 Eric Evans的《Domain-Driven Design領域驅動設計》才逐漸有所理解。

由於此內部業務框架作的事情不少,篇幅有限,這裏僅對最具借鑑意義的領域建模思考做介紹。

業務場景

我主要負責傳輸網資源管理中的傳輸模塊管理。這個部分涉及相對來講比較複雜的關聯關係,因此若是代碼組織不夠嚴謹的話,極易繞暈和出錯,下面以一張簡單的概念圖來描述一下部分實體對象間的關聯關係。

clipboard.png

如圖,簡單來講時隙和通道是一對多的父子關係,同時業務電路和多段通道間的下級時隙,存在更復雜的一對多承載關係。

因此這個關係中複雜的地方在哪裏呢?理解上常常會繞混,電路建立要選多段通道時隙,通道自己要管理多個時隙。這樣時隙和通道以及電路同時存在了一對多的聯繫,如何去準確的理解和區分這種聯繫,並將之有效的梳理在代碼層面就很須要必定技巧。

稍微拓展一下,改改資源類型,把業務電路換爲傳輸通道,把傳輸通道換爲傳輸段,這套關係一樣成立。

另外,真實的業務場景中,業務電路的下層路由,不只支持高階通道,還支持段時隙,端口等資源。

總體設計

從業務場景中咱們能夠看到,資源實體間的模型關係其實有不少相相似的地方。好比大致上老是分爲路由關係,和層級關係這麼兩種,那麼如何才能高效的對這兩種關係進行代碼層面的建模以高效的進行復用,同時又保留有每一個資源足夠的拓展空間呢?

傳統思路

咱們先來考慮一下,按照傳統的貧血模型去處理傳輸通道這個資源,針對傳輸通道的需求,它是如何處理的呢?
圖片描述

最粗陋的常規模型,其實就是依據不一樣類型的資源對需求進行簡單分流,而後按照管理劃分 Controller 層,Service 層,Dao 層。各層之間的交互,搞得好一點的會經過抽象出的一個薄薄的 domain 域對象,搞的很差的直接就是 List,Map,Object 對象的粗陋組合。

代碼示例

/**
  * 刪除通道 不調用ejb了
  *   業務邏輯:
  *       判斷是否被用,被用不能刪除
  *       判斷是不是高階通道且已被拆分,被拆不能刪除
  *       邏輯刪除通道路由表
  *       清空關聯時隙、波道的channel_id字段
  *          將端口的狀態置爲空閒   
  *       邏輯刪除通道表
  * @param paramLists 被刪除通道,<b>必填屬性:channelID</b>
  * @return 是否刪除成功
  * @throws BOException 業務邏輯判斷不知足刪除條件引起的刪除失敗<br>
  *                     <b>通道非空閒狀態</b><br>
  *                     <b>高階通道已被拆分</b><br>
  *                     <b>刪除通道數據庫操做失敗</b><br>
  *                     <b>刪除HDSL系統失敗</b><br>
  * @return 成功與否
  */
 public String deleteChannel(String channelId){
        String returnResult = "true:刪除成功";
        Map<String,String> condition = new HashMap<String, String>();
        condition.put("channelID",channelId);
        condition.put("min_index","0");
        condition.put("max_index","1");
        boolean flag=true;
        List<Map<String,Object>> channel = this.channelJdbcDao.queryChannel(condition);
        if(channel==null||channel.size()==0){
            return "false:未查詢到通道信息";
        }
        //判斷是否被用,被用不能刪除
        String oprStateId = channel.get(0).get("OPR_STATE_ID").toString();
        if(!"170001".equals(oprStateId)){
            return "false:通道狀態非空閒,不能刪除";
        }
        //判斷是不是高階通道且已被拆分,被拆不能刪除
        flag=this.channelJdbcDao.isSplited(channelId);
        if(!flag){
            return "false:高階通道已被拆分,不能刪除";
        }
        //邏輯刪除通道路由表 而且清空關聯時隙、波道的channel_id字段
        this.channelJdbcDao.deleteChannelRoute(channelId,oprStateId);
        //將通道端口的端口狀態置爲空閒
        this.channelJdbcDao.occupyPort(String.valueOf(channel.get(0).get("A_PORT_ID")),"170001");
        this.channelJdbcDao.occupyPort(String.valueOf(channel.get(0).get("Z_PORT_ID")),"170001");
        //邏輯刪除通道表
        this.channelJdbcDao.delete(channelId);
        //若是通道走了HDSL時隙則刪除HDSL系統及下屬資源 ,這裏從新調用了傳輸系統的刪除的ejb。
        List<Map<String,Object>> syss=this.channelJdbcDao.findSysByChannel(channelId);
        for(int i=0;i<syss.size();i++){
            if("56".equals(syss.get(i).get("SYS_TYPE").toString())){
                List<Map<String,String>> paramLists = new ArrayList<Map<String,String>>();
                List paramList = new ArrayList();
                Map map = new HashMap();
                map.put("res_type_id", "1001");
                map.put("type", "MAIN");
                paramList.add(map);
                map = new HashMap();
                map.put("sys_id", syss.get(i).get("SYS_ID"));
                paramList.add(map);
                //EJB裏面從第二個數據開始讀取要刪除的系統id,因此下面又加了一層 。
                map = new HashMap();
                map.put("res_type_id", "1001");
                map.put("type", "SUB");
                paramList.add(map);
                map = new HashMap();
                map.put("sys_id", syss.get(i).get("SYS_ID"));
                paramLists.add(map);
                String inputXml = this.createInputXML("1001", "deleteTrsSys",
                        "TrsSysService", paramLists);
                String result = this.getEJBResult(inputXml);
                if(result==null||"".equals(result)){//若是ejb處理失敗是以拋異常形式,被底層捕獲而未拋出,致使返回結果爲空
                    return "false:刪除HDSL系統失敗";
                }
                Document document = XMLUtils.createDocumentFromXmlString(result);
                Element rootElement = XMLUtils.getRootElement(document);
                if (!TransferUtil.getResultSign(rootElement).equals("success")){
                    result =rootElement.attribute("description").getValue();
                    return "false:刪除HDSL系統失敗"+result;
                }
            }
        }
        return returnResult;
}

上面這些代碼,是我司n年前的一段已廢棄代碼。其實也是很典型的一種業務代碼編寫方式。

能夠看到,比較關鍵的幾個流程是 :

空閒不能刪除(狀態驗證)—>路由刪除->端口置爲空閒(路由資源置爲空閒)->資源實體刪除

其中各個步驟的具體實現,基本上都是經過調用 dao 層的方法,同時配合若干行service層代碼來實現的。這就帶來了第一個弊端,方法實現和 dao層實現過於緊密,而dao層的實現又是和各個資源所屬的表緊密耦合的。所以即使電路的刪除邏輯和通道的刪除邏輯有很類似的邏輯,也必然不可能進行代碼複用了。

若是非要將不一樣資源刪除方法統一塊兒來,那也必然是充斥着各類的 if/else 語句的硬性判斷,總代碼量卻甚至沒有減小反而增長了,得不償失。

拓展思考

筆者曾經看過前人寫的一段傳輸資源的保存方法的代碼。

方法目的是支持傳輸通道/段/電路三個資源的保存,方法參數是一些複雜的 List,Map 結構組合。因爲一次支持了三種資源,每種資源又有本身獨特的業務判斷規則,多狀況組合之後複雜度直接爆炸,再外本來方法的編寫人員沒有按期重構的習慣,因此到了筆者接手的時候,是一個長達500多行的方法,其間充斥着各式各樣的 if 跳轉,循環處理,以及業務邏輯驗證。

解決辦法

面對如此棘手的狀況,筆者先是參考《重構·改善既有代碼設計》一書中的一些簡單套路,拆解重構了部分代碼。將本來的 500 行變成了十來個幾十行左右的小方法,從新組合。

方案侷限

  • 重構難度及時間成本巨大。
  • 有大量的 if/else 跳轉根本無法縮減,由於代碼直接調用 dao 層方法,必然要有一些 if/else 方法用來驗證資源類型而後調用不一樣的 dao 方法
  • 也由於上一點,重構僅是小修小補,化簡了一些輔助性代碼的調用(參數提取,錯誤處理等),對於業務邏輯調用的核心代碼卻沒法進行簡化。service層代碼量仍是爆炸

小結

站在分層的角度思考下,上述流程按照技術特色將需求處理邏輯分爲了三個層次,但是爲何只有 Service 層會出現上述複雜度爆炸的狀況呢?

看到這樣的代碼,不禁讓我想到了小學時候老師教寫文章,講文章要鳳頭,豬肚,豹尾。還真是貼切呢 :-)

換作學生時代的我,可能也就接受了,可是見識太高手的代碼後,才發現寫代碼並不該該是簡單的行數堆砌。

業務情景再分析

對於一個具體的傳輸通道A的對象而言,其內部都要管理哪些數據呢?

  • 資源對象層面

    • 自身屬性信息
  • 路由層面

    • 下級路由對象列表
  • 層次關係層面

    • 上級資源對象
    • 下級資源對象列表

能夠看到,全部這些數據其實分爲了三個層面:

  1. 做爲普通資源,傳輸通道須要管理自身的屬性信息,好比速率,兩端網元,兩端端口,通道類型等。
  2. 做爲帶有路由的資源,傳輸通道須要管理關聯的路由信息,好比承載本身的下層傳輸段,下層傳輸通道等。
  3. 做爲帶有層次關係的資源,傳輸通道須要管理關聯的上下級資源信息,好比本身拆分出來的時隙列表。

更進一步,將傳輸通道的這幾種職責的適用範圍關係進行全業務對象級別彙總整理,以下所示:
圖片描述

各類職責對應的業務對象範圍以下:

  • 同時具備路由和層次關係的實體:

    • 傳輸時隙、傳輸通道、傳輸段、傳輸電路
  • 具備路由關係的實體:

    • 文本路由
  • 具備層次結構關係的對象:

    • 設備、機房、端口
  • 僅做爲資源的實體:

    • 傳輸網管、傳輸子網、傳輸系統

拓展思考

微觀層面

以傳輸通道這樣一個具體的業務對象來看,傳統的貧血模型基本不會考慮到傳輸通道自己的這三個層次的職責。可是對象的職責並不設計者沒意識到而變得不存在。如前所述的保存方法,由於要兼顧對象屬性的保存,對象路由數據的保存,對象層次結構數據的保存,再乘上通道,段,電路三種資源,很容易致使複雜度的飆升和耦合的嚴重。

所以,500行的函數出現某種程度上也是一種必然。由於本來業務的領域知識就是如此複雜,將這種複雜性簡單映射在 Service 層中必然致使邏輯的複雜和代碼維護成本的上升。

宏觀層面

以各個資源的職責分類來看,具有路由或層次關係的資源並不在少數。也就是說,貧血模型中,承擔相似路由管理職責的代碼老是平均的分散在通道,段,電路的相關 Service 層中。

每種資源都不一樣程度的實現了一遍,而並無有效的進行抽象。這是在業務對象的代碼模型角度來講,是個敗筆。

在這種狀況下就算使用重構的小技巧,所能作的也只是對於各資源的部分重複代碼進行抽取,很難天然而然的在路由的業務層面進行概念抽象。

既然傳統的貧血模型無法應對複雜的業務邏輯,那麼咱們又該怎麼辦呢?

新的架構

代碼示例

@Transactional
public int deleteResRoute(ResIdentify operationRes) {
    int result = ResCommConst.ZERO;
    
    //1:得到須要保存對象的Entity
    OperationRouteResEntity resEntity = context.getResEntity(operationRes,OperationRouteResEntity.class);
    
    //2:得到路由對象
    List<OperationResEntity> entityRoutes = 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(上下文容器):

    1. 程序啓動時,開始持有各個 DataOperation 對象
    2. 程序運行時,負責建立 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,得到一個惟一肯定標識特徵。

如圖:

clipboard.png

這裏舉出的 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 的業務代碼,下文同)幫助更大,即使再複雜也構不成不去作的理由,下面咱們就來聊聊這個實質上的複雜工廠是如何運做的。

引入 Context

OperationResEntity 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 對象時所作的一些具體操做,能夠看到,主要完成了這麼三件事:

  1. 獲取 Entity 實例
  2. 獲取 DataOpeartion 實例(持有於上述方法中的 hoder 對象中)
  3. 將 Entity 和 DataOperation 裝配起來

那接下來咱們就仔細分析下這三個步驟應該怎麼實現吧~

獲取 Entity

在本節的一開始,咱們就舉了兩個例子,對比了有 context 幫咱們封裝 Entity 與 DataOperation 組合關係,與缺乏 context 幫咱們封裝組合關係時的區別。具體來講,優點在與這麼兩點:

  • 簡化了業務開發人員使用 Entity 對象的成本,使其自然傾向於調用框架模型,便於保證後期業務領域模型的統一性
  • 減小了客戶代碼(service)中相似 new TrsChannelDataOperation() 這樣的硬編碼,客觀上便於 service 層構建更爲通用而健壯的實現

轉過頭來再思考下,咱們的 Entity 對象與 DataOperation 對象又是否自然存在一種很是複雜多變的動態組合關係呢?

一般,咱們在實際運行時才能肯定 service 中某個 Enity 的具體規格及其應該持有的 DataOperation對象。若是由業務代碼開發人員在調用處手動初始化,未免太過複雜,也不可避免的須要經過許多 If/ELSE 判斷來調整運行分支,這樣看代碼複雜度仍是居高不下,那咱們前面洋洋灑灑分析那麼多又還有什麼意義呢。

實際上,相似 EntityDataOperation 之類的動態(調用處才知道具體的組合關係)組合關係在不少優秀的代碼中都有應用,好比咱們熟知的 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

一不當心就講多了,咱們的 EntityDataOperation 的關聯關係遠沒有那麼複雜,不過咱們能夠仿照 Spring 創建 BeanNameBeanDefinition 映射關係的思想,在容器啓動時將咱們的 EntityDataOperation 組合關係加載好,實現後續使用時,獲取肯定的 Entity 同時容器本身幫咱們注入好須要的 DataOperation

待續

小結

待續。。。

參考資料

Eric Evans的《Domain-Driven Design領域驅動設計》

聯繫做者

zhihu.com
segmentfault.com
oschina.net

相關文章
相關標籤/搜索