領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)案例-圖書(shū)館圖書(shū)管理

領(lǐng)域模型

實(shí)體與聚合根

讀者和圖書(shū)是實(shí)體;由于每個(gè)讀者都將有自己的借書(shū)信息(比如,什么時(shí)候借的哪本書(shū),是否已經(jīng)歸還,或者是否已經(jīng)過(guò)期),而與之對(duì)應(yīng)地,每本書(shū)也可以有被借歷史(比如,這本書(shū)是什么時(shí)候借給哪個(gè)讀者),于是,借書(shū)信息也是實(shí)體。

再來(lái)看看聚合。借書(shū)信息是與讀者和圖書(shū)關(guān)聯(lián)的,也就是說(shuō),沒(méi)有讀者,借書(shū)信息沒(méi)有存在的意義,同樣,沒(méi)有圖書(shū),借書(shū)信息也同樣不存在。每個(gè)讀者可以沒(méi)有任何借書(shū)信息(或者說(shuō)借書(shū)記錄),也可以有多條借書(shū)信息;而每本書(shū)也同樣可以沒(méi)有任何被借信息(或者說(shuō)被借記錄),也可以有多條被借記錄。因此存在兩個(gè)聚合:讀者-借書(shū)信息聚合(1..0.)以及圖書(shū)-借書(shū)信息聚合(1..0.)。讀者和圖書(shū)分別為聚合根,借書(shū)信息為實(shí)體。與Tiny Library對(duì)應(yīng)起來(lái),總結(jié)如下:

  • 讀者:Reader,聚合根

  • 圖書(shū):Book,聚合根

  • 借書(shū)信息:Registration,實(shí)體
    根據(jù)上述描述,我們可以確定,我們將來(lái)需要針對(duì)讀者(Reader)和圖書(shū)(Book)實(shí)現(xiàn)倉(cāng)儲(chǔ)以及相應(yīng)的規(guī)約。

  • Reader聚合根

public partial class Reader : IAggregateRoot
{
}
  • Book聚合根
public partial class Book : IAggregateRoot
{
}
  • Registration實(shí)體
public partial class Registration : IEntity
{
}

聚合根、實(shí)體、值對(duì)象的關(guān)系

實(shí)體有ID,有生命周期,有狀態(tài)(用值對(duì)象描述狀態(tài)),實(shí)體通過(guò)ID進(jìn)行區(qū)分是這個(gè)實(shí)體還是那個(gè)實(shí)體;
聚合根是實(shí)體,聚合根的ID全局唯一,聚合根下面的實(shí)體的ID在聚合根內(nèi)唯一即可;

值對(duì)象的核心意思是值,與是否是復(fù)雜類(lèi)型無(wú)關(guān),比如下圖中的Price、Count、OrderNo、CustomerAddress都是值對(duì)象;
值對(duì)象無(wú)生命周期,它的本質(zhì)是一個(gè)值,通過(guò)兩個(gè)值對(duì)象的值是否相同來(lái)區(qū)分是同一個(gè)值對(duì)象;
值對(duì)象用于描述實(shí)體的狀態(tài);

using System.Collections.Generic;

namespace Rsdf.Net.Boilerplate.Domain.Entities
{
    // 聚合根
    public class Order
    {
        public string Id;                // 值對(duì)象,訂單的ID,全局唯一
        public string OrderNo;           // 值對(duì)象
        public Address CustomerAddress;  // 值對(duì)象
        public IList<OrderItem> Items;   // 實(shí)體集合
    }

    // 實(shí)體
    public class Address
    {
        public string ProductId;   // 實(shí)體的主鍵,Order內(nèi)唯一即可
        public string ProductName; // 值對(duì)象
        public float Price;        // 值對(duì)象
        public int Count;          // 值對(duì)象
    }

    // 值對(duì)象
    public class OrderItem
    {
        public string Province;  // 值對(duì)象
        public string City;      // 值對(duì)象
        public string County;    // 值對(duì)象
    }
}

實(shí)體

有時(shí),實(shí)體并不見(jiàn)得是一種適當(dāng)?shù)慕9ぞ?,而我們?duì)實(shí)體的使用也有可能是不恰當(dāng)?shù)摹:芏鄷r(shí)候,一個(gè)領(lǐng)域概念應(yīng)該建模成值對(duì)象,而不是實(shí)體對(duì)象。
由于只從數(shù)據(jù)出發(fā),CRUD系統(tǒng)是不能創(chuàng)建出好的業(yè)務(wù)模型的。
值對(duì)象可以用于存放實(shí)體的唯一標(biāo)識(shí)。值對(duì)象是不變(immutable)的,這可以保證實(shí)體身份的穩(wěn)定性,并且與身份標(biāo)識(shí)相關(guān)的行為也可以得到集中處理。

添加業(yè)務(wù)邏輯

根據(jù)DDD,實(shí)體是能夠處理業(yè)務(wù)邏輯的,應(yīng)該盡量將業(yè)務(wù)體現(xiàn)在實(shí)體上;如果某些業(yè)務(wù)牽涉到多個(gè)實(shí)體,無(wú)法將其歸結(jié)到某個(gè)實(shí)體的話,就需要引入領(lǐng)域服務(wù)(Domain Service)。案例業(yè)務(wù)簡(jiǎn)單,目前不會(huì)涉及到領(lǐng)域服務(wù),因此,在本案例中,業(yè)務(wù)邏輯都是在實(shí)體上處理的。

以讀者(Reader)為例,它有借書(shū)和還書(shū)的行為,我們將這兩種行為實(shí)現(xiàn)如下:
Reader中的業(yè)務(wù)邏輯

public partial class Reader : IAggregateRoot
{
    public void Borrow(Book book)
    {
        if (book.Lent)
            throw new InvalidOperationException("The book has been lent.");
        Registration reg = new Registration();
        reg.RegistrationStatus = RegistrationStatus.Normal;
        reg.Book = book;
        reg.Date = DateTime.Now;
        reg.DueDate = reg.Date.AddDays(90);
        reg.ReturnDate = DateTime.MaxValue;
        book.Registrations.Add(reg);
        book.Lent = true;
        this.Registrations.Add(reg);
    }
    public void Return(Book book)
    {
        if (!book.Lent)
            throw new InvalidOperationException("The book has not been lent.");
        var q = from r in this.Registrations
                where r.Book.Id.Equals(book.Id) &&
                r.RegistrationStatus == RegistrationStatus.Normal
                select r;
        if (q.Count() > 0)
        {
            var reg = q.First();
            if (reg.Expired)
            {
                // TODO: Reader should pay for the expiration.
            }
            reg.ReturnDate = DateTime.Now;
            reg.RegistrationStatus = RegistrationStatus.Returned;
            book.Lent = false;
        }
        else
            throw new InvalidOperationException(string.Format("Reader {0} didn't borrow this book.",
                this.Name));
    }
}

倉(cāng)儲(chǔ)

在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的案例中,倉(cāng)儲(chǔ)的設(shè)計(jì)是很具有爭(zhēng)議性的話題,因?yàn)閭}(cāng)儲(chǔ)這個(gè)角色本身就與領(lǐng)域模型和基礎(chǔ)結(jié)構(gòu)層對(duì)象相關(guān),它需要序列化領(lǐng)域?qū)ο螅☉?yīng)該說(shuō)是聚合),然后將其保存到基礎(chǔ)結(jié)構(gòu)層的持久化機(jī)制。于是,在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的社區(qū)中,存在兩種觀點(diǎn):

  • 領(lǐng)域模型不能訪問(wèn)倉(cāng)儲(chǔ),理由是:倉(cāng)儲(chǔ)需要跟技術(shù)架構(gòu)層打交道,在領(lǐng)域模型中訪問(wèn)倉(cāng)儲(chǔ)就會(huì)破壞領(lǐng)域模型的純凈度。需要使用倉(cāng)儲(chǔ)的,需要在領(lǐng)域模型上加上一層,比如Application層,在該層中獲取倉(cāng)儲(chǔ)實(shí)例并處理持久化邏輯

  • 領(lǐng)域模型可以訪問(wèn)倉(cāng)儲(chǔ),但僅僅是通過(guò)倉(cāng)儲(chǔ)接口和IoC容器訪問(wèn)倉(cāng)儲(chǔ);倉(cāng)儲(chǔ)的具體實(shí)現(xiàn)通過(guò)IoC注入到領(lǐng)域模型中

其實(shí),這只不過(guò)是個(gè)人習(xí)慣問(wèn)題,我認(rèn)為兩種方法都可以接受,具體采用哪種方法,就要看具體項(xiàng)目的需求和實(shí)現(xiàn)情況而定。在Tiny Library中,由于業(yè)務(wù)簡(jiǎn)單,所以采取的是第一種方式,但上述的理由并不充分,換句話說(shuō),出于業(yè)務(wù)需求,我采用了第一種方式,但并不是因?yàn)閭}(cāng)儲(chǔ)需要跟技術(shù)架構(gòu)層打交道所以才把對(duì)倉(cāng)儲(chǔ)的訪問(wèn)放在Application層中。倉(cāng)儲(chǔ)也是領(lǐng)域模型的一部分,領(lǐng)域模型依賴于倉(cāng)儲(chǔ)的抽象。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容