酷播亮新聞
最棒的知識補給站

領域/驅動設計對軟體複雜度的應對

文章摘要: 3. 領域模型對領域知識的抽象 領域模型是對業務需求的一種抽象領域驅動設計通過分層架構與六邊形架構確保業務邏輯與技術實現的隔離

​​不管是因為規模與結構製造的理解力障礙,還是因為變化帶來的預測能力問題,最終的決定因素還是因為需求。Eric Evans認為「很多應用程式最主要的複雜性並不在技術上,而是來自領域本身、使用者的活動或業務」。因而,領域驅動設計關注的焦點在於領域和領域邏輯,因為軟件系統的本質其實是給客戶(使用者)提供具有業務價值的領域功能。

一、需求引起的軟體複雜度

需求分為業務需求與質量屬性需求,因而需求引起的複雜度可以分為兩個方面:技術複雜度與業務複雜度。

技術複雜度來自需求的質量屬性,諸如安全、高效能、高併發、高可用性等需求,為軟體設計帶來了極大的挑戰。讓人難受的是這些因素彼此之間又可能互相矛盾互相影響。例如,系統安全性要求對訪問進行控制,無論是增加防火牆,還是對傳遞的訊息進行加密,又或者對訪問請求進行認證和授權,都需要為整個系統架構新增額外的間接層。這不可避免會對訪問的低延遲產生影響,拖慢了系統的整體效能。又例如爲了滿足系統的高併發訪問,我們需要對應用服務進行物理分解,通過橫向增加更多的機器來分散訪問負載;同時,我們還可以將一個同步的訪問請求拆分為多級步驟的非同步請求,再通過引入訊息中介軟體對這些請求進行整合和分散處理。這種分離一方面增加了系統架構的複雜性,另一方面也因為引入了更多的資源,使得系統的高可用面臨挑戰,並增加了維護資料一致性的難度。

業務複雜度對應了客戶的業務需求,因而這種複雜度往往會隨著需求規模的增大而增加。由於需求不可能做到完全獨立,一旦規模擴大到一定程度,不僅產生了功能數量的增加,還會因為功能互相之間的依賴與影響使得這種複雜度產生疊加,進而影響到整個系統的質量屬性,例如系統的可維護性與可擴充套件性。在考慮系統的業務需求時,還會因為溝通不暢、客戶需求不清晰等多種局外因素帶來需求的變更和修改。如果不能很好地控制這種變更,就可能因為多次修改導致業務邏輯糾纏不清,系統可能開始慢慢腐爛,變得不可維護,最終形成一種如Brian Foote和Joseph Yoder所說的「大泥球」系統。

以電商系統的促銷規則為例。針對不同型別的顧客與產品,商家會提供不同的促銷力度;促銷的形式多種多樣,包括贈送積分、紅包、優惠券、禮品;促銷的週期需要支援定製,既可以是特定的日期,例如雙十一促銷,也可以是節假日的固定促銷模式。如果我們在設計時沒有充分考慮促銷規則的複雜度,並處理好促銷規則與商品、顧客、賣家與支付乃至於物流、倉儲之間的關係,開發過程就會變得踉踉蹌蹌,舉步維艱。

技術複雜度與業務複雜度並非完全獨立,二者混合在一起產生的化合作用更讓系統的複雜度變得不可預期,難以掌控。同時,技術的變化維度與業務的變化維度並不相同,產生變化的原因也不一致,倘若未能很好地界定二者之間的關係,系統架構缺乏清晰邊界,會變得難以梳理。複雜度一旦增加,團隊規模也將隨之擴大,再揉以嚴峻的交付週期、人員流動等諸多因素,就好似將各種不穩定的易燃易爆氣體混合在一個不可逃逸的密閉容器中一般,隨時都可能爆炸:

隨著業務需求的增加與變化,以及對質量屬性的高標準要求,自然也引起了軟件系統規模的增大與結構的繁雜,至於變化,則是軟件開發繞不開的話題。因此,當我們面對一個相對複雜的軟件系統時,通常面臨的問題在於:

  • 問題域過於龐大而複雜,使得從問題域中尋求解決方案的挑戰增加。該問題與軟件系統的規模有關。
  • 開發人員將業務邏輯的複雜度與技術實現的複雜度混淆在一起。該問題與軟件系統的結構有關。
  • 隨著需求的增長和變化,無法控制業務複雜度和技術複雜度。該問題與軟件系統的變化有關。

針對這三個問題,領域驅動設計都給出了自己的應對措施。

二、領域驅動設計的應對措施

1. 隔離業務複雜度與技術複雜度

要避免業務邏輯的複雜度與技術實現的複雜度混淆在一起,首要任務就是確定業務邏輯與技術實現的邊界,從而隔離各自的複雜度。這種隔離也是題中應有之義,畢竟技術與業務的關注點完全不同。例如在電商的領域邏輯中,訂單業務關注的業務規則包括驗證訂單有效性,計算訂單總額,提交和稽覈訂單的流程等;技術關注點則從實現層面保障這些業務能夠正確地完成,包括確保分散式系統之間的資料一致性,確保服務之間通訊的正確性等。

業務邏輯並不關心技術是如何實現的。無論採用何種技術,只要業務需求不變,業務規則就不會變化。換言之,理想狀態下,我們應該保證業務規則與技術實現是正交的。

領域驅動設計通過分層架構與六邊形架構確保業務邏輯與技術實現的隔離。

(1) 分層架構的關注點分離

分層架構遵循了「關注點分離」原則,將屬於業務邏輯的關注點放到領域層(Domain Layer)中,而將支撐業務邏輯的技術實現放到基礎設施層(Infrastructure Layer)中。同時,領域驅動設計又頗具創見的引入了應用層(Application Layer)。應用層扮演了雙重角色。一方面它作為業務邏輯的外觀(Facade),暴露了能夠體現業務用例的應用服務介面;另一方面它又是業務邏輯與技術實現的粘合劑,實現二者之間的協作。

下圖展現的就是一個典型的領域驅動設計分層架構。藍色區域的內容與業務邏輯有關,灰色區域的內容與技術實現有關,二者涇渭分明,然後匯合在應用層。應用層確定了業務邏輯與技術實現的邊界,通過直接依賴或者依賴注入(DI,Dependency Injection)的方式將二者結合起來:

(2) 六邊形架構的內外分離

由Cockburn提出的六邊形架構則以「內外分離」的方式,更加清晰地勾勒出業務邏輯與技術實現的邊界,且將業務邏輯放在了架構的核心位置。這種架構模式改變了我們觀察系統架構的視角:

體現業務邏輯的應用層與領域層處於六邊形架構的核心,並通過內部的六邊形邊界與基礎設施的模組隔離開。當我們在進行軟件開發時,只要恪守架構上的六邊形邊界,就不會讓技術實現的複雜度汙染到業務邏輯,保證了領域的整潔。邊界還隔離了變化產生的影響。如果我們在領域層或應用層抽象了技術實現的介面,再通過依賴注入將控制的方向倒轉,業務核心就會變得更加的穩定,不會因為技術選型或其他決策的變化而導致領域程式碼的修改。

(3) 案例:隔離資料庫與快取的訪問

領域驅動設計建議我們在領域層建立資源庫(Repository)的抽象,它的實現則被放在基礎設施層,然後採用依賴注入在執行時為業務邏輯注入具體的資源庫實現。那麼,對於處於核心之外的Repositories模組而言,即使選擇從MyBatis遷移到Sprint Data,領域程式碼都不會受到牽連:

  • package practiceddd.ecommerce.ordercontext.application; 
  •  
  • @Transaction 
  • public class OrderAppService { 
  •    @Service 
  •    private PlaceOrderService placeOrder; 
  •     
  •    public void placeOrder(Identity buyerId, List items, ShippingAddress shipping, BillingAddress billing) { 
  •        try { 
  •            palceOrder.execute(buyerId, items, shipping, billing); 
  •        } catch (OrderRepositoryException | InvalidOrderException | Exception ex) { 
  •            ex.printStackTrace(); 
  •            logger.error(ex.getMessage()); 
  •        } 
  •    } 
  •  
  • package practiceddd.ecommerce.ordercontext.domain; 
  •  
  • public interface OrderRepository { 
  •    List forBuyerId(Identity buyerId); 
  •    void add(Order order); 
  • }  
  •  
  • public class PlaceOrderService { 
  •    @Repository 
  •    private OrderRepository orderRepository; 
  •  
  •    @Service 
  •    private OrderValidator orderValidator;     
  •  
  •    public void execute(Identity buyerId, List items, ShippingAddress shipping, BillingAddress billing) { 
  •        Order order = Order.create(buyerId, items, shipping, billing); 
  •        if (orderValidator.isValid(order)) { 
  •            orderRepository.add(order); 
  •        } else { 
  •            throw new InvalidOrderException(String.format(“the order which placed by buyer with %s is invalid.”, buyerId)); 
  •        } 
  •    } 
  •  
  • package practiceddd.ecommerce.ordercontext.infrastructure.db; 
  •  
  • public class OrderMybatisRepository implements OrderRepository {} 
  • public class OrderSprintDataRepository implements OrderRepository {} 
  • 對快取的處理可以如法炮製,但它與資源庫稍有不同之處。資源庫作為訪問領域模型物件的入口,其本身提供的增刪改查功能,在抽象層面上是對領域資源的訪問。因此在領域驅動設計中,我們通常將資源庫的抽象歸屬到領域層。對快取的訪問則不相同,它的邏輯就是對key和value的操作,與具體的領域無關。倘若要為快取的訪問方法定義抽象介面,在分層的歸屬上應該屬於應用層,至於實現則屬於技術範疇,應該放在基礎設施層:

  • package practiceddd.ecommerce.ordercontext.application; 
  •  
  • @Transaction 
  • public class OrderAppService { 
  •    @Repository 
  •    private OrderRepository orderRepository; 
  •  
  •    @Service 
  •    private CacheClient> cacheClient; 
  •     
  •    public List findBy(Identity buyerId) { 
  •        Optional> cachedOrders = cacheClient.get(buyerId.value()); 
  •        if (cachedOrders.isPresent()) { 
  •            return orders.get(); 
  •        }  
  •        List orders = orderRepository.forBuyerId(buyerId); 
  •        if (!orders.isEmpty()) { 
  •            cacheClient.put(buyerId.value(), orders); 
  •        } 
  •        return orders; 
  •    } 
  •  
  • package practiceddd.ecommerce.ordercontext.application.cache; 
  •  
  • public interface CacheClient { 
  •    Optional get(String key); 
  •    void put(String key, T value); 
  •  
  • package practiceddd.ecommerce.ordercontext.infrastructure.cache; 
  •  
  • public class RedisCacheClient implements CacheClient {} 
  • 2. 限界上下文的分而治之

    在前面分析快取訪問介面的歸屬時,我們將介面放在了系統的應用層。從層次的職責來看,這樣的設計是合理的,但它卻使得系統的應用層變得更加臃腫,職責也變得不夠單一了。這是分層架構與六邊形架構的侷限所在,因為這兩種架構模式僅僅體現了一個軟件系統的邏輯劃分。倘若我們將一個軟件系統視為一個縱橫交錯的魔方,前述的邏輯劃分僅僅是一種水平方向的劃分。至於垂直方向的劃分,則是面向垂直業務的切割。這種方式更利於控制軟件系統的規模,將一個龐大的軟件系統劃分爲鬆散耦合的多個小系統的組合。

    針對前述案例,我們可以將快取視為一個獨立的子系統。它同樣擁有自己的業務邏輯和技術實現,因而也可以為其建立屬於快取領域的分層架構。在架構的巨集觀視角,這個快取子系統與訂單子系統處於同一個抽象層次,這一概念在領域驅動設計中,被稱之為限界上下文(Bounded Context)。

    針對龐大而複雜的問題域,限界上下文采用了「分而治之」的思想對問題域進行了分解,有效地控制了問題域的規模,進而控制了整個系統的規模。一旦規模減小,無論業務複雜度還是技術複雜度,都會得到顯著的降低,在對領域進行分析以及建模時,也能變得更容易。如果說分層架構與六邊形架構確保了業務邏輯與技術實現的隔離,則限界上下文對整個系統進行了劃分,將一個大系統拆分為一個個小系統後,我們再利用分層架構與六邊形架構思想對其進行邏輯分層,設計會變得更易於把控,系統的架構也會變得更加的清晰。

    案例:限界上下文幫助架構的演進

    國際報稅系統是為跨國公司的駐外出差僱員(系統中被稱之為Assignee)提供方便一體化的稅收資訊填報平臺。客戶是一家會計師事務所,該事務所的專員(Admin)通過該平臺可以收集僱員提交的報稅資訊,然後對這些資訊進行稅務評審。如果Admin評審出資訊有問題,則返回給Assignee重新修改和填報。一旦資訊確認無誤,則進行稅收分析和計算,並獲得最終的稅務報告提交給當地政府以及僱員本人。

    系統主要涉及的功能包括:

    • 駐外出差僱員的薪酬與福利
    • 稅收計劃與合規評審
    • 對稅收評審的分配管理
    • 稅收策略設計與評審
    • 對駐外出差僱員的稅收合規評審
    • 全球的Visa服務

    主要涉及的使用者角色包括:

    • Assignee:駐外出差僱員
    • Admin:稅務專員
    • Client:出差僱員的僱主

    在早期的架構設計時,架構師並沒有對整個系統的問題域進行拆分,而是基於使用者角色對系統進行了簡單粗暴的劃分,分為兩個相對獨立的子系統:Frond End與Office End。這兩個子系統單獨部署,分別面向Assignee與Admin。系統之間的整合則通過訊息和Web Service進行通訊。兩個子系統的開發分屬不同的團隊,Frond End由美國的團隊負責開發與維護,而Office End則由印度的團隊負責。整個架構如下圖所示:

    採用這種架構面臨如下問題:

    • 龐大的程式碼庫:整個Front End和Office End都沒有做物理分解,隨著需求的增多,程式碼庫變得格外龐大
    • 分散的邏輯:系統分解的邊界是不合理的,沒有按照業務分解,而是按照使用者的角色進行分解,導致大量相似的邏輯分散在兩個不同的子系統中
    • 重複的資料:兩個子系統中存在業務重疊,因而也導致了部分資料的重複
    • 複雜的整合:Front End與Office End因為某些相關的業務需要彼此通訊,這種整合關係是雙向的,且由兩個不同的團隊開發,導致整合的介面混亂,訊息協議多樣化
    • 知識未形成共享:兩個團隊完全獨立開發,沒有掌握端對端的整體流程,團隊之間沒有形成知識的共享
    • 無法應對需求變化: 新增需求包括對國際旅遊、Visa的支援,現有系統的架構無法很好地支援這些變化

    採用領域驅動設計,我們將架構的主要關注點放在了「領域」,與客戶進行了充分的需求溝通和交流。通過分析已有系統的問題域,結合客戶提出的新需求,對整個問題域進行了梳理,並利用限界上下文對問題域進行了分解,獲得瞭如下限界上下文:

    • Account Management:管理使用者的身份與配置資訊
    • Calendar Management:管理使用者的日程與旅行足跡

    之後,客戶希望能改進需求,做到全球範圍內的工作指派與管理,目的在於提高公司的運營效率。通過對領域的分析,我們又識別出兩個限界上下文。在原有的系統架構中,這兩個限界上下文同時處於Front End與Office End之中,屬於重複開發的業務邏輯:

    • Work Record Management:實現工作的分配與任務的跟蹤
    • File Sharing:目的是實現客戶與會計師事務所之間的檔案交換

    隨著我們對領域知識的逐漸深入理解與分析,又隨之識別出如下限界上下文:

    • Consent:管理合法的遵守法規的狀態
    • Notification:管理系統與客戶之間的交流
    • Questionnaire:對問卷調查的資料收集

    這個領域分析的過程實際上就是通過對領域的分析,引入限界上下文對問題域進行分解,通過降低規模的方式降低問題域的複雜度;同時,通過為模型確定清晰的邊界,使得系統的結構變得更加的清晰,保證了領域邏輯的一致性。一旦確定了清晰的領域模型,就能夠幫助我們更加容易地發現系統的可重用點與可擴充套件點,並遵循「高內聚鬆耦合」原則對系統職責進行合理分配,再輔以分層架構劃分邏輯邊界,如下圖所示:

    我們將識別出來的限界上下文定義為微服務,並對外公開REST服務介面。UI Applications是一個薄薄的展現層,它會呼叫後端的RESTful服務,也使得服務在保證介面不變的前提下能夠單獨演化。每個服務都是獨立的,可以單獨部署,因而可以針對服務建立單獨的程式碼庫和對應的特性團隊(Feature Team)。服務的重用性和可擴充套件性也有了更好的保障,服務與UI之間的整合變得更簡單,整個架構更加清晰了。

    3. 領域模型對領域知識的抽象

    領域模型是對業務需求的一種抽象,表達了領域概念、領域規則以及領域概念之間的關係。一個好的領域模型是對統一語言的視覺化表示,通過它可以減少需求溝通可能出現的歧義;通過提煉領域知識,並運用抽象的領域模型去表達,就可以達到對領域邏輯的化繁為簡。模型是封裝,實現了對業務細節的隱藏;模型是抽象,提取了領域知識的共同特徵,保留了面對變化時能夠良好擴充套件的可能性。

    案例:專案管理系統的領域模型

    我們開發的專案管理系統需要支援多種軟體專案管理流程,例如瀑布、RUP、XP或者Scrum。這些專案管理流程是迥然不同的,如果需要各自提供不同的解決方案,就會使得系統的模型變得非常複雜,也可能引入許多不必要的重複。通過領域建模,我們可以對專案管理領域的知識進行抽象,尋找具有共同特徵的領域概念。這就需要分析各種專案管理流程的主要特徵與表現,才能從中提煉出領域模型。

    瀑布式軟件開發由需求、分析、設計、編碼、測試、驗收六個階段構成,每個階段都由不同的活動構成,這些活動可能是設計或開發任務,也可能是召開評審會。流程如下圖所示:

    RUP清晰地劃分了四個階段:先啟階段、細化階段、構造階段與交付階段。每個階段可以包含一到多個迭代,每個迭代有不同的工作,例如業務建模、分析設計、配置與變更管理等。RUP的流程如下圖所示:

    XP作為一種敏捷方法,採用了迭代的增量式開發,提倡為客戶交付具有業務價值的可執行軟體。在執行交付計劃之前,XP要求團隊對系統的架構做一次預研(Architectual Spike,又被譯為架構穿刺)。當架構的初始方案確定後,就可以進入每次小版本的交付。每個小版本交付又被劃分爲多個週期相同的迭代。在迭代過程中,要求執行一些必須的活動,如編寫使用者故事、故事點估算、驗收測試等。XP的流程如下圖所示:

    Scrum同樣是迭代的增量開發過程。專案在開始之初,需要在準備階段確定系統願景、梳理業務用例、確定產品待辦項(product backlog)、制定釋出計劃以及組建團隊。一旦在確定了產品待辦項以及釋出計劃之後,就進入sprint迭代階段。sprint迭代過程是一個固定時長的專案過程,在這個過程中,整個團隊需要召開計劃會議、每日站會、評審會議和回顧會議。Scrum的流程如下圖所示:

    不同的專案管理流程具有不同的業務概念。例如瀑布式開發分爲了六個階段,但卻沒有釋出和迭代的概念。RUP沒有釋出的概念,而Scrum又為迭代引入了sprint的概念。

    不同的專案管理流程具有不同的業務規則。例如RUP的四個階段會包含多個迭代週期,每個迭代週期都需要完成對應的工作,只是不同的工作在不同階段所佔的比重不同。XP需要在進入釋出階段之前,進行架構預研,而在每次小版本釋出之前,都需要進行驗收測試和客戶驗收。Scrum的sprint是一個基本固定的流程,每個迭代召開的四會(計劃會議、評審會議、回顧會議與每日站會)都有明確的目標。

    領域建模就是要從這些紛繁複雜的領域邏輯中尋找到能夠表示專案管理領域的概念,並利用物件導向建模範式或其他正規化對概念進行抽象,並確定它們之間的關係。經過對這些專案管理流程的分析,我們雖然發現在業務概念和規則上確有不同之處,但由於它們都歸屬於軟件開發領域,我們自然也能尋找到某些共同特徵的蛛絲馬跡。

    首先,從專案管理系統的角度看,無論針對何種專案管理流程,我們的主題需求是不變的,就是要為這些管理流程制定軟件開發計劃(Plan)。不同之處在於,計劃可以由多個階段(Phase)組成,也可以由多個釋出(Release)組成。一些專案管理流程沒有釋出的概念,我們可以認為是一個釋出。那麼,到底是釋出包含了多個階段,還是階段包含了多個釋出呢?我們發現在XP中,明顯地劃分了兩個階段:Architecture Spike與Release Planning,而釋出只屬於Release Planning階段。因而從概念內涵上,我們可以認為是階段(Phase)包含了釋出(Release)。每個釋出又包含了一到多個迭代(Iteration),至於Scrum的sprint概念其實可以看做是迭代的一種特例。每個迭代可以開展多種不同的活動(Activity),這些活動可以是整個團隊參與的會議,也可以是部分成員或特定角色執行的實踐。對於計劃而言,我們還需要跟蹤任務(Task)。與活動不同,任務具有明確的計劃起止時間、實際起止時間、工作量、優先順序與承擔人。

    於是,我們提煉出如下的統一領域模型:

    爲了專案管理者更加方便地制定專案計劃,產品經理提出了計劃模板功能。當管理者選擇對應的專案管理生命週期型別後,系統會自動建立滿足其規則的初始計劃。基於該需求,我們更新了之前的領域模型:

    在增加的領域模型中,LifeCycleSpecification是一個隱含的概念,遵循領域驅動設計提出的規格(Specification)模式,封裝了專案開發生命週期的約束規則。

    領域模型以視覺化的方式清晰地表達了業務含義,我們可以根據這個模型來指導後面的程式設計與編碼實現。當增加新的需求或者需求發生變化時,我們能夠敏銳地捕捉到現有模型的不匹配之處,並對其進行更新。領域模型傳遞了知識,可以作為交流的載體,符合人們的心智模型,有利於讓開發人員從紛繁複雜的業務中解脫出來。這是領域驅動設計對於前述第三個問題——控制業務複雜度的解答。

    【本文為51CTO專欄作者「張逸」原創稿件,轉載請聯絡原作者】

    戳這裏,看該作者更多好文

    【編輯推薦】

  • 從碼農到架構師,如何成長為技術Leader?
  • 我覺得微信7.0的「看一看」還是不行
  • @你,技術圈裏的閱讀錦鯉,是你嗎?
  • 為什麼RESTful很糟糕?
  • 為什麼阿里巴巴要求程式設計師謹慎修改serialVersionUID 欄位的值
  • 【責任編輯:

    趙寧寧

    TEL:(010)68476606】

    ​​​​

    如有侵權請來信告知:酷播亮新聞 » 領域/驅動設計對軟體複雜度的應對