面向對象架構模式之:領域模型(Domain Model)

一:面向對象設計中最簡單的部分與最難的部分html

若是說事務腳本是 面向過程 的,那麼領域模型就是 面向對象 的。面向對象的一個很重要的點就是:「把事情交給最適合的類去作」,即:「你得在一個個領域類之間跳轉,才能找出他們如何交互」,Martin Flower 說這是面向對象中最難的部分,這具備誤導的成份。確切地說,咱們做爲程序員若是已經掌握了 OOD 和 OOP 中技術手段,那麼如何尋找類之間的關係,可能就成了最難的部分。但在實際的狀況中,即使咱們不是程序員,也總能描述一件事情(即尋求關係),因此,找 對象之間的關係 還真的並非程序員最關係的部分,從技術層面來說,尋找類之間的關係由於與具體的編碼技巧無關,因此它如今對於程序員的咱們來講,應該是最簡單的部分,技術手段纔是這裏面的最難部分。程序員

好,切入正題。算法

 

二:構築類之間的關係(最簡單部分)sql

先來完成最簡單的部分,即找關係。也就是說,按照所謂的關係,咱們來重構 事務腳本 中的代碼。上篇「你在用什麼思想編碼:事務腳本 OR 面向對象?」中一樣的需求,若是用領域模式來作的話,咱們大概能夠這樣設計:數據庫

image

(備註:Product 和 RecognitionStrategy  爲 * –> 1 的關係是由於 一種確認算法能夠被多個產品的實例對象使用)緩存

從下面的示例代碼咱們就能夠看到這點:性能優化

class RevenueRecognition
{
    private double amount;
    private DateTime recognizedOn;
   
    public RevenueRecognition(double amount, DateTime recognizedOn)
    {
        this.amount = amount;
        this.recognizedOn = recognizedOn;
    }
   
    public double GetAmount()
    {
        return this.amount;
    }
   
    public bool IsRecognizedBy(DateTime asOf)
    {
        return asOf.CompareTo(this.recognizedOn) > 0 || asOf.CompareTo(this.recognizedOn) == 0;
    }
}架構

class Contract
{
    // 多 對 1 的關係,* -> 1。即:一個產品可有多個合同訂單
    private Product product;
    private long id;
    // 合同金額
    private double revenue;
    private DateTime whenSigned;
   
    // 1 對 多 的關係, 1 -> *
    private List<RevenueRecognition> revenueRecognitions = new List<RevenueRecognition>();
   
    public Contract(Product product, double revenue, DateTime whenSigned)
    {
        this.product = product;
        this.revenue = revenue;
        this.whenSigned = whenSigned;
    }
   
    public void AddRevenueRecognition(RevenueRecognition r)
    {
        revenueRecognitions.Add(r);
    }
   
    public double GetRevenue()
    {
        return this.revenue;
    }
   
    public DateTime GetWhenSigned()
    {
        return this.whenSigned;
    }
   
    // 獲得哪天前入帳了多少
    public double RecognizedRevenue(DateTime asOf)
    {
        double re = 0.0;
        foreach(var r in revenueRecognitions)
        {
            if(r.IsRecognizedBy(asOf))
            {
                re += r.GetAmount();
            }
        }
       
        return re;
    }
   
    public void CalculateRecognitions()
    {
        product.CalculateRevenueRecognitions(this);
    }
}app

class Product
{
    private string name;
    private RecognitionStrategy recognitionStrategy;
   
    public Product(string name, RecognitionStrategy recognitionStrategy)
    {
        this.name = name;
        this.recognitionStrategy = recognitionStrategy;
    }
   
    public void CalculateRevenueRecognitions(Contract contract)
    {
        recognitionStrategy.CalculateRevenueRecognitions(contract);
    }
   
    public static Product NewWordProcessor(string name)
    {
        return new Product(name, new CompleteRecognitionStrategy());
    }
   
    public static Product NewSpreadsheet(string name)
    {
        return new Product(name, new ThreeWayRecognitionStrategy(60, 90));
    }
   
    public static Product NewDatabase(string name)
    {
        return new Product(name, new ThreeWayRecognitionStrategy(30, 60));
    }
}分佈式

abstract class RecognitionStrategy
{
    public abstract void CalculateRevenueRecognitions(Contract contract);
}

class CompleteRecognitionStrategy : RecognitionStrategy
{
    public override void CalculateRevenueRecognitions(Contract contract)
    {
        contract.AddRevenueRecognition(new RevenueRecognition(contract.GetRevenue(), contract.GetWhenSigned()));
    }
}

class ThreeWayRecognitionStrategy : RecognitionStrategy
{
    private int firstRecognitionOffset;
    private int secondRecognitionOffset;
   
    public ThreeWayRecognitionStrategy(int firstRoff, int secondRoff)
    {
        this.firstRecognitionOffset = firstRoff;
        this.secondRecognitionOffset = secondRoff;
    }
   
    public override void CalculateRevenueRecognitions(Contract contract)
    {
        contract.AddRevenueRecognition(
            new RevenueRecognition(contract.GetRevenue() / 3, contract.GetWhenSigned()));
        contract.AddRevenueRecognition(
            new RevenueRecognition(contract.GetRevenue() / 3, contract.GetWhenSigned().AddDays(firstRecognitionOffset)));
        contract.AddRevenueRecognition(
            new RevenueRecognition(contract.GetRevenue() / 3, contract.GetWhenSigned().AddDays(secondRecognitionOffset)));
    }
}

 

正像我說的,以上的代碼是最簡單部分,每一個 OOP 的初學者都能寫出這樣的代碼來。可是我心想,即使咱們能寫出這樣的代碼來,咱們恐怕都不會心虛的告訴本身:是的,我正在進行領域驅動開發吧。

那麼,真正難的部分是什麼?

2.1 領域模型 對於程序員來講真正困難或者困惑的部分

是領域模型自己怎麼和其它模塊(或者其它層)進行交互,這些交互或者說關係是:

1:領域模型 自身具有些什麼語言層面的特性;

2:領域模型 和 領域模型 之間的關係;

3:領域模型 和 Repository 的關係;

4:工做單元 和 領域模型 及 Repository 的關係;

5:領域模型 的緩存;

6:領域模型 和 會話之間的關係;

 

三:那些交互與關係

3.1 領域模型 自身具有些什麼語言層面的特性

先看代碼:

public class User2 : DomainObj
{
#region Field
#endregion

#region Property

#endregion

#region 領域自身邏輯

#endregion

#region 領域服務
#endregion
}

對於一個領域模型來講,從語言層面來說,它具有 5 方面的特性:

1:有父類,放置公共的屬性之類的內容,同時,存在一個父類,也表示它不是一個 值對象(領域概念中的值對象);

2:有實例字段;

3:有實例屬性;

4:領域自身邏輯,非 static 方法,有 public 的和 非public;

5:領域服務,static 方法,可獨立出去放置到對應的 服務類 中;

如今,咱們具體展開一下。不過,爲了展開講,咱們必須提供一個稍稍完整的 User2 的例子,它在真正的項目是這個樣子的:

     public class User2 : DomainObj
    {
        #region Field
        private Organization2 organization;

        private List<YhbjTest> myTests;

        private List<YhbjClass> myClasses;

        #endregion

        #region Property

        public override IRepository RootRep
        {
            get { return RepRegistory.UserRepository; }
        }

        public string UserName { get; private set; }

        public string Password { get; private set; }

        /* 演示了同時存在 Organization 和 OrganizationId 兩個屬性的狀況 */
        public string OrganizationId { get; private set; }

        public Organization2 Organization
        {
            get
            {
                if (organization == null && !string.IsNullOrEmpty(OrganizationId))
                {
                    organization = Organization2.FindById(OrganizationId);
                }

                return organization;
            }
        }

        /* 演示了存在 列表 屬性的狀況 */
        public List<YhbjClass> MyClasses
        {
            get
            {
                if (myClasses == null)
                {
                    myClasses = YhbjClass.GetClassesByUserId(this);
                }

                return myClasses;
            }
        }

        public List<YhbjTest> MyTests
        {
            get
            {
                /* 個人考試來自兩個地方,1:班級、項目上的考試;2:選人的考試;
                 * 故,有兩種設計方法
                 * 1:選人的考試沒有疑議;
                 * 2:班級、項目考試,能夠從本模型的 Classes -> Projects -> Tests 獲取;
                 * 3:也能夠直接從數據庫獲得獲取;
                 * 在這裏的實際實現,採用第 2 種作法。由於:
                 * 1:數據自己是緩存的,第一獲取的時候,貌似存在屢次查詢,可是一旦獲取就緩存了;
                 * 2:存在不少地方的數據一致性問題,採用方法 3 貌似快速了,但會帶來不可知 BUG ;
                 * 3:即使未來考試還有課程上的考試,能夠很方便的獲取,否則還須要重改 SQL;
                 */
                if (myTests == null)
                {
                    myTests = new List<YhbjTest>();

                    /* 加指定人的考試,這些考試沒有對應的 項目 和 班級*/
                    myTests.AddRange(YhbjTest.GetTestsByUserId(this.Id));

                    /* 加班級的考試,有對應的 班級 */
                    foreach (var c in MyClasses)
                    {
                        myTests.AddRange(c.Tests);
                        foreach (var t in c.Tests)
                        {
                            t.SetOwnerClass(c);
                        }

                        /* 加項目的考試,有對應的 班級 和 項目,代碼略 */
                    }
                }

                /* 其它邏輯 */
                foreach (var test in myTests)
                {
                    if (test.TestHistory == null)
                    {
                        test.SetHistory(MyTestHistories
                            .FirstOrDefault(p => p.TestId == test.Id && p.UserId == this.Id));
                    }
                }

                return myTests;
            }
        }
        #endregion

        #region 領域自身邏輯

        public void InitWithOrganization(Organization2 o)
        {
            /* 在這個方法中不用 MakeDirty,由於至關於初始化分爲兩步進行了
             */
            this.organization = o;
        }


        /* 不須要對外開放的邏輯,使用 internal*/
        internal virtual void UpdateOnline(string loginTime, string token, string loginIp, string loginPort)
        {
            /* 這樣作的好處是什麼呢?
             *  createnew 方法用戶不負責本身的持久化,而是由事務代碼進行負責
             *  可是 createnew 方法會標識本身爲 new,即 makenew 方法調用
             *  而後,因爲 ut 用的都是同一個 ut,因此在事務這裏 commit 了
             *  就是 commit 了 根 和 非根
             * 這裏也同時演示了多個領域對象共用一個 ut
             */
            this.UnitOfWork = new UnitOfWork();
            UserOnline2 userOnline2Old = UserOnline2.GetUserOnline(this.UserName);
            if (userOnline2Old != null)
            {
                userOnline2Old.UnitOfWork = this.UnitOfWork;
                userOnline2Old.Delete();
            }

            UserOnline2 = UserOnline2.CreateNew(UserName, loginIp, loginPort);
            UnitOfWork.RegisterNew(UserOnline2);
            UnitOfWork.Commit();
        }

        /* 對外開放的邏輯,使用 public */
        public List<YhbjTest> GetMyTest(string testName, int type, int page, int size, out int totalCount)
        {
            IEnumerable<YhbjTest> expName = from p in MyTests orderby p.CreateTime descending select p;
            IEnumerable<YhbjTest> expState = null;

            switch (type)
            {
                case 0:
                    /* 未考
                     * 須要排除掉 有效期 以外
                     */
                    expState =
                        from p in expName
                        where
                            p.StartTime <= DateTime.Now &&
                            p.EndTime >= DateTime.Now &&
                           (this.MyTestHistories.Exists(q => q.TestId == p.Id) == false
                           || (this.MyTestHistories.Exists(q => q.TestId == p.Id) == true && this.myTestHistories.Find(h => h.TestId == p.Id).TestState != 1)) &&
                           p.AuditState == AuditState.Audited
                        select p;
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }

            var re = expState.ToList();
            totalCount = re.Count;
            return re.Skip((page - 1) * size).Take(size).ToList();
        }

        public YhbjTest StartTest(string testId)
        {
            // 邏輯略
        }

        #endregion

        /// <summary>
        /// 1:服務是無狀態的,因此是 static 的
        /// 2:服務是公開的,因此是 public 的
        /// 3:服務實際是能夠建立專門的服務類的,這裏爲了演示須要,就放在一塊兒了
        /// </summary>
        #region 領域服務

        /* 這兩個字段演示其實服務部分的代碼是隨意的 */
        private static readonly CookieWrapper CookieWrapper;

        private static readonly HttpWrapper HttpWrapper;

        static User2()
        {

            CookieWrapper = new CookieWrapper();
            HttpWrapper = new HttpWrapper();
        }

        /* 內部的方法固然是私有的 */
        private static List<PaperQuestionStrategy3> GetUserPaperByUserAndTest(User2 user2, YhbjTest test)
        {
            var x = RepRegistory.UserRepository.FindTestUserPaper(user2, test);
            return x;
        }

        /* 獲取領域對象的方法,所有屬於領域服務部分,再次強調是靜態的 */
        public static User2 GetUserByName(string username)
        {
            var user = (RepRegistory.UserRepository).FindByName(username);
            return user as User2;
        }
        /* 領域對象的獲取和產生,還有另外的作法,就是在對象工廠中生成,但這不屬於本文要闡述的範疇 */
        public static User2 CreateCreater(
            string creatorOrganizationId, string creatorOrganizationName, string id, string name)
        {
            var user = new User2 { Id = id, Name = name, UnitOfWork = new UnitOfWork() };
            user.MakeNew();
            return user;
        }
        #endregion
    }

請仔細查看上面代碼,爲了本文接下來的闡述,上面的代碼幾乎都是有意義的,我已經很精簡了。好了,基於上面這個例子,咱們展開講:

1:父類

public abstract class DomainObj
{
public Key Key { get; set; }

/// <summary>
/// 根倉儲
/// TIP: 由於是充血模式,因此每一個領域模型都有一個根倉儲
/// 用於提交自身的變更
/// </summary>
public abstract IRepository RootRep { get; }

protected DomainObj()
{
}

public UnitOfWork UnitOfWork { get; set; }

public string Id { get; protected set; }

public string Name { get; protected set; }

protected void MakeNew()
{
UnitOfWork.RegisterNew(this);
}

protected void MakeDirty()
{
UnitOfWork.RegisterDirty(this);
}

protected void MakeRemoved()
{
UnitOfWork.RegisterRemoved(this);
}

}

父類包含了,讓一個 領域模型 成爲 領域模型 所必備的那些特色,它有 標識映射(架構模式對象與關係結構模式之:標識域(Identity Field)),它持有 工做單元(),它負責調用 工做單元的API(換個角度說工做單元(Unit Of Work):建立、持有與API調用)。

若是咱們的對象是一個 領域模型對象,那麼它一定須要繼承之這個父類;

2:有實例字段

有人可能會有疑問,不是有屬性就能夠了嗎,爲何要有字段,一個理由是,若是咱們須要 延遲加載(),就須要使用字段來進行輔助。咱們在上面的源碼中看到的 if XX == NULL ,這樣的屬性代碼,就是延遲加載,其中使用到了字段。注意,若是使用了延遲加載,你應該會遇到序列化的問題,這是你須要注意的《延遲加載與序列化》。

3:有實例屬性

屬性是必然的,沒有屬性的領域模型很稀少的。有幾個地方須要你們注意,

1:屬性的 get 方法,能夠是很複雜的,其地位至關因而領域自身邏輯;

2:set 方法,都是 private 的,領域對象自身負責自身屬性的賦值;

3:在有必要的狀況下,使用 延遲加載,這可能須要另一個主題來說;

4:延遲加載的那些屬性,不少時候就是 導航屬性,即 Organization 和 MyClasses 這樣的屬性,就是導航屬性;

4:領域自身邏輯

領域自身邏輯,包含了應用系統大多數的業務邏輯,能夠理解爲:它就是傳統 3 層架構中的業務邏輯層的代碼。若是一段代碼,你不知道把它放到哪裏,那麼,它多半就屬於應該放在這裏。注意,只有應該公開的那些方法,才 public;

5:領域服務

領域服務,能夠獨立出去,成爲領域服務類。那麼,什麼樣的代碼是領域服務代碼?第一種狀況:

生成領域對象實例的方法,都應該是領域服務類。如 查詢 或者 Create New。

在實際場景中,咱們可能使用對象工廠來生成它們,這裏爲了純粹的演示哪些是 領域自身邏輯,哪些是 領域服務,特地使用了領域類的 static 方法來生成領域對象。即:

領域對象,不能隨便被外界生成,要嚴格控制其生成。因此領域父類的構造器,咱們看到是 protected 的。

那麼,實際上,除了上面這種狀況外,任何代碼都應該是 領域自身邏輯的。我在上面還演示了這樣的一段代碼:

private static List<PaperQuestionStrategy3> GetUserPaperByUserAndTest(User2 user2, YhbjTest test)
{
    var x = RepRegistory.UserRepository.FindTestUserPaper(user2, test);
    return x;
}

這段代碼,實際上做爲領域服務部分,就是錯誤的,它應該被放置在 YhbjTest 這個領域類中。

 

3.2 領域模型 和 領域模型 之間的關係

也就是說那些導航屬性和領域模型有什麼關係。導航屬性必須都是延遲加載的嗎?固然不是。好比, User 所在的 Organization,咱們在在使用到用戶這個對象的時候,幾乎老是要使用到其組織信息,那麼,咱們在獲取用戶的時候,就應該當即獲取到組織對象,那麼,咱們的持久化代碼是這樣的:

        public override DomainObj Find(Key key)
        {
            var user = base.Find(key) as User2;
            if (user == null)
            {
                //
                string sql = @"
                DECLARE @ORGID VARCHAR(32)='';
                SELECT @ORGID=OrganizationId FROM [EL_Organization].[USER] WHERE ID=@Id
                SELECT * FROM [EL_Organization].[USER] WHERE ID=@Id
                SELECT * FROM [EL_Organization].[ORGANIZATION] WHERE ID=@ORGID";
                var pms = new SqlParameter[]
                {
                    new SqlParameter("@Id", key.GetId())
                };

                var ds = SqlHelper.ExecuteDataset(CommandType.Text, sql, pms);
                user = DataTableHelper.ToList<User2>(ds.Tables[0]).FirstOrDefault();
                var o = DataTableHelper.ToList<Organization2>(ds.Tables[1]).FirstOrDefault();
                if (user == null)
                {
                    return null;
                }

                user = Load(user);
                // 注意,除了 Load User 還須要 Load Organization
                user.InitWithOrganization(o);
                Load(user.Organization);

                return user;
            }

            return user;
        }

能夠看到,咱們在一次 sql 執行的時候,就獲得了 organization,而後,User2 類型中,有個屬於領域自身邏輯方法:

        public void InitWithOrganization(Organization2 o)
        {
            /* 在這個方法中不用 MakeDirty,由於至關於初始化分爲兩步進行了
             */
            this.organization = o;
        }

在這裏要多說一下,若是不是初始化時候的改屬性,如修改了用戶的組織信息,就應該 MakeDirty。

注意,還有一個比較重要的領域自身邏輯,就是 SetOwned,以下:

public void SetOwnerClass(YhbjClass yhbjClass)
{
    this.OwnerClass = yhbjClass;
    /* should not makeDirty, but if class repalced or removed, should makedirty*/
}

好比,領域模型 考試,就可能會有這個方法,考試自己須要知道:我屬於哪一個班級。

 

3.3 領域模型 和 Repository 之間的關係

第一,若是咱們在使用 領域模型,咱們必須使用 Repository 模式嗎?答案是:固然不是,咱們可使用 活動記錄模式(什麼是活動記錄,當前咱們能夠暫時理解爲傳統3層架構中的DAL層)。若是咱們在使用 Repository ,那麼,領域模型和 Respository 之間是什麼關係呢?這裏,有兩點須要闡述:

第一點是,通常的作法,Repository 是被注入的,它可能被注入到系統的某個地方,示例代碼是被注入到了類型 RepRegistory中。

領域模型要不要使用 Repository,個人答案是:要。

爲何,由於咱們要讓領域邏輯本身決定合適調用 Repository。

第二點是,每一個領域模型都有一個 RootRep,用於自身以及把自身當成根的那些導航屬性對象的持久化操做;

 

3.4 工做單元 和 領域模型 及 Repository 的關係

這一點比較複雜,咱們單獨在 《換個角度說工做單元(Unit Of Work):建立、持有與API調用》 進行了闡述。固然,跟 Repository 同樣,使用 領域模型,必須使用 工做單元 嗎?答案也是否是。只是,在使用 工做單元 後,更易於咱們處理 領域模型 中的事務問題。

 

3.5 領域模型的緩存

緩存分爲兩類,第一類咱們能夠稱之爲 一級緩存,這對於客戶端程序員來講,不可見,它被放置在 AbstractRepository 中,每每在當前請求中有用:

public abstract class AbstractRepository : IRepository
{
    /* LoadedDomains 在有些文獻中能夠做爲高速緩存,可是這個緩存可不是指的
     * 業務上的那個緩存,而是 片斷 的緩存,指在當前實例的生命週期中的緩存。
     * 業務上的緩存在咱們的系統中,由每一個領域模型的服務部分自身持有。
     */
    protected Dictionary<Key, DomainObj> LoadedDomains =
        new Dictionary<Key, DomainObj>();

    public virtual DomainObj Find(Key key)
    {
        if (LoadedDomains.ContainsKey(key))
        {
            return LoadedDomains[key] as DomainObj;
        }
        else
        {
            return null;
        }

        //return null;
    }

    public abstract void Insert(DomainObj t);

    public abstract void Update(DomainObj t);

    public abstract void Delete(DomainObj t);

    public void CheckLoaedDomains()
    {
        foreach (var m in LoadedDomains)
        {
            Console.WriteLine(m.Value);
        }
    }
    /// <summary>
    /// 當緩存內容發生變更時進行重置
    /// </summary>
    /// <param name="keyField">緩存key的id</param>
    /// <param name="type">緩存的對象類型</param>
    public void ResetLoadedDomainByKey(string keyId,Type type)
    {
        var key=new Key(keyId,type);
        if (LoadedDomains.ContainsKey(key))
        {
           LoadedDomains.Remove(key);
        }
    }

    protected T Load<T>(T t) where T : DomainObj
    {
        var key = new Key(t.Id, typeof (T));
        /* 1:這一句很重要,由於咱們不會想要放到每一個子類裏去賦值
         * 2:其次,若是子類沒有調用 Load ,則永遠沒有 Key,不過這說得過去
         */
        t.Key = key;

        if (LoadedDomains.ContainsKey(key))
        {
            return LoadedDomains[key] as T;
        }
        else
        {
            LoadedDomains.Add(key, t);
            return t;
        }

        //return t;
    }

    protected List<T> LoadAll<T>(List<T> ts) where T : DomainObj
    {
        for (int i = 0; i < ts.Count; i++)
        {
            ts[i] = Load(ts[i]);
        }

        return ts;
    }
}

業務系統中的緩存,須要咱們隨着業務系統自身的特色,本身來建立,好比,若是咱們針對 User2 這個領域模型創建緩存,就應該把這個緩存掛接到當前會話中。此處不表。

 

3.6 領域模型 與 會話之間的關係

這是一個有意思的話題,不管是理論上仍是實際中,在一次會話當中(若是咱們會話的參照中,能夠回味下 ASP.NET 中的 Session,它們所表達的概念是一致的),只要會話不失效,那麼 領域對象 的狀態,就應該是被保持的。這裏難的是,咱們怎麼來建立這個 Session。Session 回到語言層面,就是一個類,它可能會將領域對象保持在 內存中,或者文件中,或者數據庫中,或者在一個分佈式系統中(如 Memcached,《ASP.NET性能優化之分佈式Session》)。

最簡單的,咱們可使用 ASP.NET 的 Session 來保存咱們的會話,而後把領域對象存儲到這裏。

 

四:總結

以上描述了讓領域模型成爲領域模型的一些最基本的技術手段。解決了這些技術手段,咱們的開發才基本算是 DDD 的,纔是面向領域模型的。解決了這些技術問題,接下來,咱們才能毫無後顧之憂地去解決 Martin Flower 所說的最難的部分:「你得在一個個領域類之間跳轉,才能找出他們如何交互」。

相關文章
相關標籤/搜索