MicrosoftNet企業級應用架構設計(中)

XML火柴發表於2017-12-25

8.領域模型導論

設計的模型和理念相互影響 ——Eric Evans

  1. 從資料到行為的轉變
    典型的開發方案:收集需求,通過一些分析找出相關實體和需要實現的流程。接著,帶著這些理解,嘗試推到能夠支撐流程的無理資料模型(通常是關係型)。確保資料模型符合關係型的一致性,然後根據標識相關業務實體的表構建軟體元件。可以通過儲存過程等資料庫特有的功能實現行為,使資料庫對上層程式碼隱藏起來。最後一步是找到合適的模型表示資料,並把它傳到表現層上。

    1. 模型和領域背後的基本原理
      1. 並非關於使用物件代替資料讀取器。
      2. 持久化物件模型並非領域模型
      3. 模型是什麼,為什麼需要模型(參考莫卡託投影)
      4. 關於行為的一切:領域模型的目標是儘可能地表達應用程式要處理的核心業務概念;若要與業務保持一致,你的設計應該以行為而不是資料為重點。
      5. DDD對每個人來說都是好的
    2. 資料庫是基礎設施
      DDD的建議是關注業務概念和繫結上下文流程,並瞭解它們,然後規劃一個可以忠實實現那些業務概念和流程的系統。產生的系統以模型為中心,包含了核心業務邏輯。DDD只是把領域建模放在前面,把它的持久化放在後面。
      1. 領域模型無需關心持久化:需要去關心相關實體與它們的關係、事件和可觀察行為。模型通過類和方法表示實體。持久化透明通常用來表達領域模型的一個關鍵特徵,其意味著領域模型的類不應該包含從磁碟儲存或構件例項的方法。類似:領域模型的類也不應該暴露需要訪問持久層才能做出判斷的方法。
      2. 應用程式應該關心持久化:所有應用程式都需要提供持久化的基礎設施以及各種橫切關注點(安全,快取,日誌)。最終,領域模型應該與持久化實現細節保持獨立,但通常情況下,O/RM技術可能會給模型帶來一些約束。
  2. 領域層的內部
    最常見的繫結上下文支撐架構師帶有領域模型的分層架構。在表現層之下,分層架構通過編排程式碼(應用程式層)對領域和基礎設施層進行操作。領域層包含模型和服務。

    1. 領域模型:提供業務領域的概念檢視。由實體和值物件構成,對現實世界進行建模,目的是要把這些概念轉變成軟體元件。
      這裡寫圖片描述
      1. 模組:當你把一個領域模型變成軟體時,你會標識一個或多個模組。,模組包含物件並對整個領域進行分割槽,以便領域模型設計的所有關注點都能清楚、乾淨地分離開來。在繫結上下文裡,領域模型通過模組組織。,有如下包含關係
        • 一個繫結上下文有一個領域模型
        • 一個繫結上下文可以有多個模組
        • 一個領域模型可以關聯多個模組
      2. 值物件:DDD包含實體和值物件。雖然二者都表示成.NET的類,但實體和值型別分別表示不同的概念,從而導致不同的實現細節。在DDD裡,值物件完全通過它的特性來定義,但其特性在例項建立之後就不會再變了。如果要變,值物件會變成另一個值物件的特例,包含一組全新的特性。在.NET裡,DDD值物件表示成不可變型別。
      3. 實體
        • 所有物件都有特性,但不是所有物件都能完全通過它們的特性集合來標識。
        • 值物件只是聚合在一起的資料
        • 實體通常由資料和行為構成
        • 領域邏輯在領域層裡(模型或服務)
        • 用例的實現則在應用程式層裡
      4. 實體的持久化
        領域模型必須持久化,但是,它不直接關心自己的持久化
    2. 聚合
      隨著構建領域模型,單個實體總是互相引用的情況很多,這意味著“大泥團”的徵兆:邏輯上相關的物件被單獨對待而不是組合起來並當做一個整體對待。
      聚合基本上是一致性的邊界,對模型裡的實體進行分組和隔離。一致性通常是事務性的,但在一些情況下也可能採取最終一致性。
      通常的做法是先把領域模型分解成聚合,然後再聚合裡標識出領域實體

      1. 構思聚合模型:在領域裡,單個容器下的多個實體合起來叫做聚合。最終的模型是聚合、單個實體和它們的值物件的結合
        這裡寫圖片描述
        模型裡的實體會被劃分為聚合。每個聚合都受限於一個邏輯邊界,這個邊界定義了哪些實體在這個聚合裡。一個聚合有一個根實體,被稱為聚合根。聚合根是物件圖的根,它封裝了包含於其中的實體,並充當它們的代理。
      2. 使用業務不變條件發現聚合:聚合是設計的一部分。在繫結上下文裡,聚合表示業務模型裡的不便條件,從另一個角度來說,聚合是一種保證業務一致性的方式。
        一般來說,一致性有兩種:
        • 事務一致性:聚合保證在領域裡每次發生業務事務後的一致性。
        • 最終一致性:一致性在某種程度上會得到保證,但可能不是在每次業務事務之後
          聚合並不關心邊界之外發生的一致性和操作,在一個良好的領域模型裡,每個事務智慧修改一個聚合
      3. 聚合模型的好處

        1. 整個領域模型的實現以及它的業務邏輯變得更加簡單

          • 聚合模型是單純的邏輯分組。
          • 就程式碼而言,實體類任然是單獨的類,並且每個都有自己的實現。
          • 聚合模型並不一定引入新的類來組織內嵌類的程式碼;但是,出於方便,聚合通常寫成新的類,封裝子物件的整個物件圖。

          聚合模型提升了抽象層次並在一個整體裡封裝了多個實體。模型包含層次減少,因而可操作的內部實體關係的數量也減少

        2. 防止緊耦合模型:每個實體都可能與很多其他實體關聯在一起。一個實體要麼是一個聚合根,要麼只被包含在一個聚合裡。
      4. 聚合之間的關係
        實現聚合模型只需對設計的實體類做出績效的改變,但會對領域模型的其餘部分造成很大的影響,尤其是服務和倉儲。聚合的根類對呼叫方隱藏了相關類,並要求呼叫方在進行任何互動時引用它們。
      5. 建立聚合根

        • 聚合根物件是組成這個聚合的物件群的根。
        • 聚合根在整個領域模型都可見,而且可以直接引用。
        • 聚合裡的實體任有它們的身份標識和生命週期,但它們不能從聚合之外直接引用

        聚合根物件有相應的責任:

        • 聚合根保證所包含的物件總是按照應用程式業務規則出於有效狀態(一致性)。
        • 聚合根負責持久化所有被封裝的物件。
        • 聚合根負責級聯更新和刪除聚合裡的實體。
        • 查詢操作只能獲取聚合根。訪問內部物件必須總是通過整合根的介面來完成。

        提示:

        1. 實體在關係型轉換模型裡會有單獨的表。
        2. 聚合是通過外間關係相互關聯的表的聯合。
        3. 聚合根是在外來鍵關係圖裡唯一一個只包含對外外來鍵引用的表。
    3. 領域服務
      領域服務類的方法實現的領域邏輯不屬於特定聚合,並且很可能跨越多個實體。為了實現業務操作,領域服務協調聚合和倉儲的活動。在某些情況下,領域服務可能使用基礎設施的服務。
      當一塊業務邏輯無法融入任何現有聚合,而聚合又無法通過重新設計適應操作時,就要需要考慮使用領域服務。因此,在某種程度上,領域服務收容無法放在其他地方的邏輯。領域服務使用的名字嚴格遵循統一語言。
      1. 服務及契約:領域服務的介面代表統一語言裡的契約,列出可供應用程式服務呼叫的操作。
      2. 跨聚合行為:領域服務的最常見場景是實現設計多個聚合或資料庫訪問方法的行為。模型的結構對領域服務的數量和介面有很大的影響。
      3. 倉儲:倉儲式領域讀物的最常見型別,它們負責持久化聚合。每個聚合根都有一個對應的倉儲。一個常見的做法是從這些類提取一個介面,並在核心模型所在的類庫裡定義這個介面(倉儲介面的實現屬於基礎設施層),其介面通常定義在領域層程式碼所在的程式集裡。一個好的倉儲也可以只是一個基於介面的類,有一組根據你需要安排的成員。倉儲類的成員會執行資料訪問——查詢、更新或者插入。
      4. 是服務還是聚合:傾向於認為領域服務是哪些無法自然放入聚合的行為的後背藏身之所;使用新的持久化實體而不是領域服務編排。兩個方案最終都行得通。
    4. 領域事件
      1. 從順序邏輯到事件
        需求說明了當某個事件觸發時要做什麼。如果在領域服務方法裡放置處理程式碼,就不得不在需求改變時觸碰這個領域服務方法,否則就要為相同的事件執行多個操作。修改領域服務類本身並不危險,但一般來說,任何做法只要降低修改現有的類帶來的風險都會受到歡迎。
      2. 在相關的事件發生時觸發事件:事件使我們沒有必要在一個地方放置所有處理程式碼。
        • 使我們可以再不觸碰產生事件的程式碼的情況下動態定義一組處理器
        • 使我們可以再多個地方觸發相同的事件。這意味著處理器的執行無需關心實際的呼叫方。
      3. 規範化領域事件:領域事件是一個簡單的類,表示領域裡引起關注的事件。領域事件可以只是定義在類上的一個成員。但是,在領域模型場景裡,事件通常表示成帶有專門介面標記的領域特定類。
      4. 處理領域事件:為領域事件定義處理器。任何註冊的處理器都有機會處理特定型別的事件。處理器是一個小類,它包含一些邏輯在要對某個事件做出反應時執行。可以有多個類處理相同的領域事件,這允許組合多個操作。
    5. 橫切關注點:在分層架構裡,你會發現幾處設施層是一個與具體技術有關的地方。
      1. 在單個事務的保護下:並非做了領域建模,就不必親自處理資料庫連線字串以及級聯規則、外來鍵和儲存過程這類繁瑣的東西。CRUD介面的多數高階方面都由O/RM完全處理。當使用Entity Framework 時,你以事務的方式操作工作單元,框架的上下文物件會為你透明管理。工作單元的跨度只有一個資料庫連線。對於多連線事務,可能要在應用程式層用這個類把一個邏輯操作的多個步驟拼在一起。最終,把資料庫訪問程式碼隔離在儲存倉庫,在應用程式層或領域服務裡使用倉儲。
      2. 驗證:有兩種主流觀點
        1. 實體必須總是處於有效狀態
        2. 實體應該當做普通.NET物件,直到要以某種方式使用它們為止。
          因為DDD是關於忠實地建模領域,所以它應該總是處理有效地實體。
          領域模型裡的類應該檢查它們能夠檢查的東西,並在出錯的時候。
      3. 安全
        1. 與應用程式所在的環境有關,觸及訪問控制許可權、跨站指令碼和使用者驗證等。這和領域層無關。
        2. 與授權有關,確保只有授權使用者才允許執行某些操作。
      4. 日誌記錄:決定在哪裡記錄什麼。
      5. 快取:快取的理想實現和相關程式碼的位置完全取決於你真正想快取的是什麼。把用到的元件隱藏在契約背後。
        出於測試和擴充套件的原因,可以把快取抽象為一個API介面,然後再倉儲或領域服務裡注入一個有效地實現。
  3. 總結:領域模型嘗試根據實體和物件以及它們的關係、狀態和行為對業務領域進行建模。在領域模型裡,實體是業務空間裡的相關物件,它有標識和生命週期。而值物件則是業務空間裡死氣沉沉的東西。為了編碼和設計,有些實體也會組合成聚合。

領域模型導論

分析部分發現頂層架構,通過統一語言挖掘繫結上下文和它們之間的關係。
策略部分為每個繫結上下文提供最合適的架構。
  1. 分離命令與查詢
    查詢不以任何方式修改系統狀態,只返回資料。
    命令則修改系統狀態,但不返回資料(除了狀態程式碼或資訊確認)
    1. CQRS:查詢和命令式不同的東西,應該區別對待。
      1. 從領域模型到CQRS
        在某種程度上,CQRS是對構思複雜領域模型的困難的一種橫向思考。
        CQRS使用兩個不同的領域層而不是一個。這種分離把查詢操作放在一層,把命令操作放在另一層。接著每一層都有自己的架構和專門服務。分別用於查詢和命令。如圖區別:
        這裡寫圖片描述
        在CQRS裡,查詢棧基於SQL查詢,完全沒有模型、應用程式層和領域層。一般而言查詢棧應該儘量簡單。此外,通常的CQRS方案會為每個部分準備一個不同的資料庫。
      2. 查詢和命令領域層的結構
        在CQRS系統裡,查詢部分的領域層的模型可以只是一組量身定做的資料傳輸物件(DTO)。
        一般而言,很可能需要命令部分的領域模型,因為會在這裡表達業務邏輯和實現業務規則。
        CQRS系統的命令布馮的領域模型可能非常簡單,因為它是為命令量身定製的。
        領域模型是應對軟體核心複雜性的理想方式。大多數複雜性來源於查詢和命令的笛卡爾積。分離命令和查詢可以把複雜性降低一個級別。
      3. CQRS不是頂層架構:不是設計企業級系統的全面方案。CQRS只是一個模式,指導你家狗更大系統的特定繫結上下文。執行基於統一語言的DDD分析,標識出繫結上下文仍是值得推薦的初期步驟。
    2. CQRS的好處
      1. 簡化設計
        從使用模型中得到的教訓是,你在軟體系統裡面臨大多數複雜性通常都與改變系統狀態的操作有關。命令應當驗證當前狀態,然後決定是否可以執行。接著,命令應該確保系統處於一致狀態。
      2. 增強可伸縮性的潛能
        可伸縮性的實施方案對所考慮的每個系統來說似乎都是唯一的。
        • 可伸縮性定義了系統在使用者數量增加時維護相同級別效能的能力。
        • 可伸縮性取決於架構師微調系統使之在相同的時間單元裡執行更多操作的餘地。
          分離 查詢和命令讓你可以完全隔離處理兩個部分的可伸縮性。
      3. CQRS的積極副作用
        1. 引導你深入理解你的應用查詢讀取什麼以及處理什麼。模組的嚴格分離也使你可以安全地改變某一個而不會對另一個造成某種迴歸。
        2. 對查詢和命令的考慮引導你按照任務和機遇任務的使用者介面進行分析。
    3. 在業務層裡使用CQRS
      • 一個包含讀取操作所需的模型和服務
      • 包含命令操作所需的模型和服務
        模型的形式最終只是實現細節,不管是物件模型、一個函式庫、還是一組資料傳輸物件。
        1. 非協作系統與協作系統
          CQRS是在尋找更有效的方式應對複雜系統時發現的。
          在協作系統裡,底層資料在任何時候都可能受到當前使用者、通過各個前端連線的併發使用者和後端軟體的影響而發生改變。在協作系統裡,使用者競爭相同資源,意味著資料的實時性。導致這種持續改變的其中一個原因是業務邏輯特別複雜,牽涉到多個可能需要動態載入的模組。架構師有兩個選擇:
          • 在需要完成任何操作時鎖定整個聚合(吞吐量很低)
          • 保持聚合對改變開放,代價可能是顯示過期資料,但最終會達到一致。
        2. CQRS
          CQRS不只是使用不同的領域層來執行查詢和命令,更多是使用根據下圖這些新的指導原則進行架構的不同棧來執行查詢和命令。
          這裡寫圖片描述
          在命令通道里,然和來自表現層的請求都會變成一個命令,並加入到處理器佇列。命令處理器只對命令處理一次。處理命令可能會產生事件,這些事件會被其它註冊元件處理。
          當業務邏輯非常複雜時,你會負擔不起同步處理命令。原因如下:
          • 它使系統變慢
          • 有關的領域服務會變得過於複雜,甚至費解,容易出現迴歸,尤其在規則頻繁改變時。
            查詢通道非常簡單。它就是一組倉儲,從專門的去規範化資料快取查詢內容。查詢通道時分開的,任何時候都可以把它放到一個專用的伺服器。
    4. CQRS總能勝任架構需要
      查詢棧和命令棧之間的簡單分離也會簡化設計。
      1. 在命令棧裡使用事務指令碼
      2. EDMX用作只讀模型
      3. 務實的架構師角度
  2. 查詢棧:處理過期資料的必要性
    1. 只讀領域模型:只處理查詢的模型比同事處理查詢和命令的模型更簡單。
      1. 為何需要不同的模型:領域模型是業務領域的API。一旦公開暴露,就可以呼叫API執行任何執行的操作。為了確保一致性,API不應該以來開發者僅以正確的方式使用。領域模型的類會改變得越來越複雜,因為它們可以同時在查詢和命令場景裡交替使用。
      2. 從領域模型到只讀模型:當你的目標只是為只讀操作建立領域模型時,一切都會變得更容易,類從整體上來說也會變得更簡單。累的總體結構更像資料傳輸物件,屬性也比方法變得多得多。
    2. 設計只讀模型外觀
      查詢棧可能仍然需要領域服務從儲存提取資料,為上面的應用程式層和表現層所用。在這種情況下,領域服務,特別是倉儲,應該重新界定為只允許讀取儲存。
      1. 限制資料庫上下文
        在只讀棧裡,並不需要擁有全部CRUD方法的經典倉儲。
        實現查詢通道的基本步驟是把對資料庫德訪問限制為只能查詢。
        聚合的概念在只讀模型裡不再重要。但是,只讀模型外觀裡的可查詢資料通常與完整領域模型的聚合對應。
      2. 調整倉儲
        就倉儲而言,底線是你在查詢棧裡並不需要它們。整個資料訪問層都能直接在應用程式層裡通過在某些物件/關係模型類之上的LINQ查詢來表達清楚。
    3. 分層表示式樹:當一個通用解決方案隨著時間變得極其複雜,並越來越不可管理時,很可能是因為它沒有很好地解決問題。在這裡推薦的不同方案可以利用LINQ和表示式樹的威力降低只讀模型裡的倉儲和DTO的複雜性。
  3. 命令棧
    在CQRS裡,命令棧只考慮改變應用程式狀態的任務的執行。通常,應用程式層從表現層獲取請求,然後編排它們的執行。

    1. 回到表現層:命令式通過後端執行的操作。從CQRS的角度來看,任務是單向的,它會產生一個工作流,從表現層下達到領域層,最終可能修改某些儲存。任務通過兩種方式觸發:

      1. 使用者通過操作某些UI元素顯示啟動任務。
      2. 某些自主服務於系統非同步互動。

      提交的請求會更新系統的狀態,但呼叫方法可能仍需獲取某些反饋。

    2. 規範化命令和事件
      所有軟體系統都從某個前端資料來源獲取輸入。輸入資料從前端傳到應用程式層,輸入資料的處理階段會在這裡編排。總之,用於輸入處理的任何前端請求都可以看做傳送給應用程式層(接受者)的訊息。訊息是一個包含任何後續處理所需的普通資料的資料傳輸物件。前端可以通過多種方式把資訊傳給應用程式層。一般情況下,傳輸是一個普通的方法呼叫。
      訊息有兩種:命令和事件。兩種訊息都包含了一組資料。
      命令是一種命令式的訊息,就像為了執行某些任務顯示提交給系統的請求。

      • 一個命令由一個處理器管理。
      • 命令可以被系統拒絕。
      • 命令可以再某個處理器的執行過程中失敗。
      • 命令的實際效果會因系統的當前狀態而有所不同。
      • 命令通常不會非法入侵某個繫結上下文邊界,
      • 命令的推薦命名規範認為它們應該是命令式的,指出需要完成什麼。

      事件是一個用來通知某些事情已經發生的訊息。

      • 事件可以被系統拒絕或取消。
      • 事件可以有多個想處理它的處理器,
      • 事件的處理可以產生其他事件,由其他處理器來處理。
      • 事件的訂閱者可以位於發起它的繫結上下文之外。

      設計領域事件的基本指導原則是它們應該儘可能具體,清楚表明目的。

    3. 處理命令和事件
      命令是由處理器來管理的,我們通常把它稱為命令匯流排。事件則有事件匯流排元件來管理。然而,命令和事件由同一個匯流排來處理也並非罕見。在使用者介面發生任何互動都會對系統產生某些請求。
      1. 匯流排元件
        命令匯流排持有一組已知的業務流程,可以被命令觸發。命令可以進一步發起這些流程的活動例項。處理命令有時候會在領域裡產生事件;產生的事件會發布到相同的命令匯流排,或者並行的事件匯流排,如果有的話。處理命令和相關事件的流程通常被稱為Saga
        命令匯流排是一個類,它接受訊息(執行命令的請求和事件的通知)並尋找處理它們的方式。匯流排本身並不實際處理事情,而是選出可以處理命令和事件的註冊處理器。
      2. Saga元件
        一般而言,Saga元件看起來像一組邏輯上相關的方法和處理器。每個Saga元件都宣告瞭以下資訊。
        • 一個命令或者事件啟動與這個Saga關聯的流程。
        • 這個Saga可以處理的命令以及它感興趣的事件。
      3. 命令和事件合起來的效果
        當你寫的系統基於非常動態的業務邏輯時,你可能會認真地考慮為無需修補系統就能擴充套件和改變某些工作流開啟一扇門。
        把工作流的整個業務邏輯放在一個方法裡可能會為維護和測試帶來問題。
      4. Saga和事務:Saga不是讓單個元件知曉整個工作流,而是使用事件把整個工作流變成更小的命令處理器的組合效果,相互之間通過觸發事件來協同工作。在涉及跨越多個繫結上下文的長時間執行流程時,Saga也減少了分佈事務的需要。對於失敗的情況,Saga方法可能會多次受到相同的事件。
      5. 命令匯流排的缺點:額外加了一層,讓程式碼變得更不易讀。
    4. 現成的儲存
      大多數真實系統都會寫入資料,稍後再讀出來。通常,你會在命令棧裡有一個領域層,在查詢棧裡有一個非常簡單的資料訪問層。
      1. 為查詢優化儲存:很多真實系統使用單個資料完成讀寫操作。使用單個關係型資料庫仍是最佳選擇。在查詢棧裡,查詢的東西與檢視模型幾乎有著相同的結構。
      2. 建立資料庫快取:為了避免賦予每個命令太多的責任,最常見的做法是任何對查詢資料庫有影響的命令在最後觸發一個事件。然後事件處理器會負責更新查詢資料庫。查詢資料庫並不完全代表應用程式背後的業務。
      3. 過時的資料和最終一致性
        如果在命令的最後更新查詢資料庫,最好自動保持命令和查詢資料庫同步。或者延遲命令和查詢資料庫之間的同步。當查詢和命令資料庫不同步時,表現層可能會顯示過期的資料,整個系統也會儲存在區域性不一致性。在某個時刻,資料庫會回到一致,但不是每個時刻。最終一致性通過定期執行的計劃任務或非同步操作的佇列來實現。
  4. 總結
    1. CQRS提議分離領域層並且分別使用獨立的模型那個進行讀寫。
    2. CQRS極其適合高協作系統,但它的簡化模式也同樣適用簡單場景。
    3. CQRS的主要特點是分離命令棧和查詢棧。
    4. 完整的CQRS是一個好的解決方案,但並不適用於所有問題。

2017/12/25 16:07:28

彩蛋


  1. 如果開發者可以把API用錯,他最終會用錯。
  2. 真實資料很少反映你對它先入為主的看法。而你通常會在最不合適的時候發現這點。
  3. 實踐經驗最不足的人通常在如何讓出事方面見解最多。
  4. 問題的解決方案改變了問題。
  5. 當心虛假知識,它比無知更危險。
  6. 要理解遞迴,必須先理解遞迴。
  7. 軟體在複用之前要先可用。
  8. 90%的東西都是CRUD。
  9. 如果除錯是除掉軟體缺陷的過程,那麼程式設計就把它們放進去的過程。
  10. C很容易搬石頭砸自己的腳;C++比較難做到,不過當你這樣做時,它會讓你半身不遂。
  11. 一個可工作的程式只包含未被發現的缺陷。
  12. 無論你有多少資源,永遠都不夠。
  13. 所有常量都是變數。
  14. 變數不會改變。

相關文章