.net架構設計讀書筆記--第三章 第8節 域模型簡介(Introducing Domain Model)

1、數據--行爲轉變 sql

    很長的時間,典型的分析方法或多或少是如下兩種,第一,收集需求並作一些分析,找出有關實體 (例如,客戶、 訂單、 產品) 和進程來實現。 第二,手持這種理解你嘗試推斷一個物理 (和主要關係) 的數據模型,能夠支持您確保流程數據模型是關係一致 (主鍵約束、 歸一化、 索引),而後開始構建軟件組件對識別的最相關的業務實體的表 數據庫

    你也能夠依靠數據庫特定功能,如存儲過程做爲一種方式,同時保持從上層的代碼隱藏的數據庫結構的執行行爲。最後一步找到適合的模型來表示數據和將其移動到表示層。 windows

1. 模型和域的基本原則 架構

    多年來,咱們一直試圖比較領域驅動和數據驅動兩種軟件設計方法的優劣,不少時候是失敗的,咱們一般把全部的事做爲對象讀取數據。 app

  • 不能用對象來代替數據讀取

    咱們都知道數據訪問是在.net以前出現的,ado.net是windows開發的一個標誌性組件。ADO.NET是基礎架構層用來訪問關係數據庫的基礎。使用ado.net從數據庫中取得的數據是已經被反序列化的數據,只是這些數據用datareader,或dtattable還表示。爲了更進一對數據格式化,可使用linq to sql與Entity Framework來輕鬆轉轉換爲可管理的類。 dom

    Linq to SQL與Entity Framework在ORM工具下映射到一個現有的關係數據庫。但這不能說這就是使用了域設計方法來設計了軟件,這只是數據訪問層訪問數據的一個方法,它只是代替了DataReader或DateSet。 ide

  •  可序列化的對象不必定是域模型(A persistent object model is not a domain model)

DDD並不僅是批使用對像來代替數據訪問的中的date reader,DDD主要是爲了表示域在通用語言的概念和上下文邊界。在邊界上下文中咱們一般用Model來示域,這些用來表示域中的對象類一般被稱作域模型。 工具

    域模型側重於業務邏輯中對象表示,它是業務邏輯層的一個重要組成部分。 學習

    DDD對任何人都有益。DDD最是個種分析方法,它是對領域學習的指南,對領域詞彙的定義,它不是編碼,也不是技術。它是作爲一個架構師在頂層設計中必需掌握技能。 ui

2. 域層內部實現 Inside the domain layer

    一般狀況下,域模型架構設計爲主下文邊界的一個單獨的層。域模型在表示層、應用層、基礎設施層中以不一樣形式傳遞數據,在這裏咱們能夠把域模型看作爲一個對象模型。

  • 域模型

    簡單的說,定義一個域模型的最佳方式是:一個域模型表示一個業務領域的概念視圖。它用實體和值對象來表示現實世界和軟件系統裏對象映射。 

 

  • 模塊Modules

    轉換一個域模型到軟件系統裏時,可能要定義多個模塊。模塊最終將整個域內的Model分組,分類。在實際應用中,模塊中就是.net中的命名空間或一個程序

    在一個上下文邊界中,域是由多個模塊組成,在.net解決方案中域模型經過命名空間來組織並隔離類庫。模塊的組織隔離原則。

    一個上下文邊界一個域模型

    一個上下文邊界能夠有多個模塊

    一個域模型能夠屬於欠個模塊

  • 值對象Value objects

    一個DDD模型包括實體entities和值對象,eityties與value objects都是.net下的類,但他們的在概念上不一樣,職責也不一樣。在DDD設計中,value object所有被定義爲屬性表示,當value object 實例被建立以後值就不會變更。

  • 實體Eitities

    全部的entities都有屬性,但實體並不全是由屬性組成。當屬性不足以標識實體的惟一性時,能夠在實體中加入另外的屬性,如ID。

    有兩個清晰的概念,好比:你有兩個事務交易,在兩個域中要分別獨立開來。在事務惟一性的後面,域模型必需能夠清晰的表示對像的惟一性。域模型和值對象同時返回,意味着咱們要每個都要建立。

    value object只是數據的載體,而entities能夠說是數據加上行爲。如業務上的訂單,一個訂單可能表示爲一個entity,但在數據層面上一個訂單能夠還包括商品,配送地址,物流信息等。

  • 實體的持久化Persistence of entities 

    Demain model必須要能夠持久化,可是demain model本身沒直接可實例化的功能,demain model側重點不業務邏輯表示。

    repositories倉儲--實現demain model持久化功能的一種類型的組件。

    倉儲一般在外部調用域模型,好比:從application layer或從領域內的其它的組件,如域服務(demain services)。倉儲的契約在領域層內,倉儲的實現則在基礎架構層。

  • 合集模型

    在域模型中,多個實體組成的一個容器被稱之爲模型合集,模型合集也能夠是合集的合集。

 

使用業務來定義合集,合集是設計的一部分,架構師和開發人員在業務上定義合集更易反映出業務,在邊界上下文中,合集表明不變性條件的形式層在,不變性條件是業務的規則,並反過來驗證檢測域實體。換言之,就是合集要業務的要保持一致性。

    一般一致性分爲兩種,事務一致性和結果一致性。事務一致性批域內的每一個事務後合集都要保持一致,結果一致性指不是第個事務都一致。

    全集模型的優勢有不少,首先,簡化了域模型和業務邏輯之間的複雜度,簡化了它們之間的顆粒度和關係。從SOLID思想看來,合集有如下幾個優勢:

  1.     純粹的邏輯分組
  2.     在代碼層面上,實體類保留着他自身的實現
  3.     沒有必要將每一個分組都生成一個新類

合集在抽象級別層面上將多個實體封閉成一個單元,將實體數據減小,實體關係的邏輯也同時簡化。以下圖:

      

 

  • 合集的根

    合集的根是合集關聯對象的根對象,合集的根在整個域是可見,並能夠被直接引用。實體在合集內部有有他的惟一性和生命週期,但它不能被外部引用。如下幾點是合集根的基本性:

  1.   合集根保證合集在業務規則中始終處於有效狀態。
  2.   聚合根是負責全部封裝對象的持久性
  3.   合集根是負責級聯更新和刪除合集的實體
  4.   查詢操做只能檢索聚合的根,經過各表層訪問內部實體也須要經過這個合集根

在代碼中要清晰的表示合集根,在服務和倉儲層在區分它。合集的根一般實現爲一個接口,這個接口一般叫IAggregateRoot。

public class Order : IAggregateRoot

{

...

}

接口並不須要明確的實現,它只用於對外表示這個實體是合集的根。

public interface IAggregateRoot

{

}

聚合根也能夠給出一個稍微複雜的實現,總這爲了讓合集更健壯。

public interface IAggregateRoot {

bool CanBeSaved { get; }

}

經過返回true達到標記爲IAggregateRoot 的做用,關且說明是非成員接口。

 

  • 域服務Domain services

    域服務是域邏輯的一系列實現方法的集合,它不屬於某個特定的合集,並頗有可能跨多個實體。域服務協調合集個倉儲用合適的方法來實現業務上的活動。在某些狀況下域服務多是基礎架構的消費服務,如郵件發送。

    給定一塊業務邏輯,這塊邏輯不適用於現用的任何合集,也不能用現有的合集從新設計,這種狀況下就考慮引用域服務。因此域服務是在邏輯在任何狀況下都不適用的最後解決手段。

  • 倉儲Repositories

倉儲是域服務中是長用的服務類型,主要提供合集的持久化功能。要爲每一個根合集提供一個倉儲,如:CustomerRepository, OrderRepository等。經常使用的作法是將相同類型的類取爲接口,並將接口作爲類庫的核心。倉儲對接口的實現一般被歸屬爲基礎架構層。一個倉儲一般基於一個接口實現,如:IRepository

public interface IRepository<T> where T:IAggregateRoot 

    // You can keep the interface a plain marker or    

    // you can have a few common methods here.  

 

    T Find (object id); 

    void Save (T item); 

}

public interface IOrderRepository : IRepository<Order> 

    // The actual list of members is up to you 

    ... 

}

建立倉儲沒有絕對正確或絕對錯誤的方法。一個好的倉儲一般是包含了一系列所須要的合集的基於接口的類。你也能夠忽略接口只實現持久化功能,如:

public interface IOrderRepository  

    // The actual list of members is up to you 

    ... 

}

 

    倉儲類成員實現了數據訪問,查詢、更新、插入等。數據訪問的技術由架構師來決定,今天一般使用Entity Framework這樣的ORM工具來實現。也沒有理由會阻止使用ADO.NET,存儲過程,甚至是NoSOL。

    須要注意的一點是,實現了IRepository<T>接口,並被標記爲契約類,能夠IL語言實現注入功能。

[ContractClass(typeof(RepositoryContract<>))] 

public interface IRepository<T> where T:IAggregateRoot 

  ... 

}

 

[ContractClassFor(typeof(IRepository<>))]  

sealed class RepositoryContract<T> : IRepository<T> where T : IAggregateRoot 

    public void Save(T item) 

    { 

       Contract.Requires<ArgumentNullException>(item != null, "item"); 

       Contract.Requires<ArgumentException>(item.CanBeSaved); 

       throw new NotImplementedException(); 

   } 

}

當Save方法被調用,合集中的所提供的CanBeSaved方法由CLR自動訪問。爲了操持一致性,CanBeSaved須要在倉儲啓用Save方法前檢查。

 

  • 域事件

    假設如下一個場景:一個在線商城應用,訂單提交併被成功處理,支付系統也處理完成,訂單的配送信息由一個配送公司來接收,此時訂單已經寫入系統。如今的問題是:當訂單建立時系統應該作什麼事?經過使用通用語言能夠準確的告訴你該作和什麼,這個任務在這稱爲Task T。

  • 事件的順序邏輯

    事件的順序邏輯首選項就是使用代碼將域服務的一系列方法按照訂單的處理邏輯順序組織起來,以下面的這段代碼:

void Checkout(ShoppingCart cart)

{

// Proceed through the necessary steps

...

if (success)

{

// Execute task T

...

}

}

上面這段代碼有一些問題,首先它沒有足夠的表達力,全部終端到終端的步驟是總體調用Checkout處理,外面的Checkout方法對沒內部方法執行沒有可視性。

    如何觸發相關事件?若是咱們只是引起域相關的一個事件會更好嗎?全部的事件能夠在一個地方刪除,這樣有兩個好處:一、在不須要改動生成事件的代碼的狀況下動態定義一系列事件。二、使咱們有可能有多個地方的同一事件引起。這意味着,處理程序將運行時不考慮實際調用方

  • 模式化域事件

    到目前爲止,域事件被視爲域內一個簡單類發生的某個特定的動做。好的例子如:訂單被建立或用戶的忠誠度到達了金牌級別。若是你花少量的時間來考慮事件,你會在幾乎全部的域模型中都會存在。一個發票的生成或修改、發貨單的生成、一個新客戶的註冊等等。

    根據這個原則,域事件能夠被定義爲一個簡單的類的事件成員。在域模型中,事件特殊表示爲一個實現特定接口的域中的類。

public interface IDomainEvent

{

}

 

public class CustomerReachedGoldMemberStatus : IDomainEvent

{

    public Customer Customer { get; set; }

}

EventArgs作事件基數,是.net事件機制的基本選項,別外的常見作法是在域模型內實現本身的事件引擎。經過接口標記事件,事件的信號狀態改變經過域實體內部的發佈訂閱來監聽。

    public class OrderRequestService

    {

        public void RegisterOrder(ShoppingCart cart)

        {

// Create and persist the new order

...

// Check gold status for the customer who made the order

    CheckGoldStatusForCustomer(cart.Customer);

        }

        public void CheckGoldStatusForCustomer(Customer customer)

        {

            // Get total amount of orders placed in current year

            var totalOrdered = CalculateTotalOrdersForCustomer(customer, DateTime.Today.Year)

            if (totalOrdered > 1000)

            {

                Bus.Raise(new CustomerReachedGoldMemberStatus() { Customer = customer });

            }

        }

    }

 

  • 處理域事件

    引起事件是一工做,要尋找一個處理這些事件的方式,如下是事件觸發的基本實現:

    public class Bus

    {

        private static IList<IHandler<IDomainEvent>> Handlers = new List<IHandler<IDomainEvent>>();

        public static void Register(IHandler<IDomainEvent> handler)

        {

            if (handler != null)

                Handlers.Add(handler);

        }

        public static void Raise<T>(T eventData) where T : IDomainEvent

        {

            foreach (var handler in Handlers)

            {

                if (handler.CanHandle(eventData))

                    handler.Handle(eventData);

            }

        }

    }

如上面的代碼,事件引起實際是讓訂閱列表中的每一個事件有機會執行。任何已註冊的處理程序老是獲得一個機會去處理給定類型的事件。處理程序是一個小型的類包含一些邏輯來運行在某一特定事件的反應,顯然,你能夠有多個類來處理同一域事件。也容許將一些事件組合在一塊兒,你能夠先執行Task 1,再執行Task 2,以下:

    public class GoldStatusHandler : IHandler<IDomainEvent>

    {

        public void Handle(IDomainEvent eventData)

        {

// Some synchronous task

...

return;

        }

        public bool CanHandle(IDomainEvent eventType)

        {

            return eventType is CustomerReachedGoldMemberStatus;

        }

    }

相關文章
相關標籤/搜索