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


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

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

好,切入正題。

 

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

先來完成最簡單的部分,即找關系。也就是說,按照所謂的關系,我們來重構 事務腳本 中的代碼。上篇“你在用什么思想編碼:事務腳本 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);
    }
}

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 所說的最難的部分:“你得在一個個領域類之間跳轉,才能找出他們如何交互”。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM