.NET Core開發實戰(第27課:定義Entity:區分領域模型的內在邏輯和外在行為)--學習筆記


27 | 定義Entity:區分領域模型的內在邏輯和外在行為

上一節講到領域模型分為兩層

一層是抽象層,定義了公共的接口和類

另一層就是領域模型的定義層

先看一下抽象層的定義

1、實體接口 IEntity

namespace GeekTime.Domain
{
    public interface IEntity
    {
        object[] GetKeys();
    }

    public interface IEntity<TKey> : IEntity
    {
        TKey Id { get; }
    }
}

通常情況下實體只有一個 ID,但是也不排除存在多個 ID 的情況,所以這里的接口 IEntity 定義實現為多個 ID 的情況,而 IEntity 表示實體只有一個 Id

同樣看一下 Entity 的定義

public abstract class Entity : IEntity

public abstract class Entity<TKey> : Entity, IEntity<TKey>

同樣地定義了一個 Entity 和 Entity ,這樣就可以在實體上面定義一些共享的方法,比如 ToString

public abstract class Entity : IEntity
{
    public abstract object[] GetKeys();

    public override string ToString()
    {
        // 輸出當前實體的名稱以及它的 Id 的清單
        return $"[Entity: {GetType().Name}] Keys = {string.Join(",", GetKeys())}";
    }
}

對於 Entity 定義了比較多的方法

public abstract class Entity<TKey> : Entity, IEntity<TKey>
{
    int? _requestedHashCode;
    public virtual TKey Id { get; protected set; }
    public override object[] GetKeys()
    {
        return new object[] { Id };
    }

    /// <summary>
    /// 表示對象是否相等
    /// 這個方法的重載使我們可以正確的判斷兩個實體是否是同一個實體
    /// 根據 Id 判斷,如果沒有 Id 的話,兩個實體是不會相等的
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is Entity<TKey>))
            return false;

        if (Object.ReferenceEquals(this, obj))
            return true;

        if (this.GetType() != obj.GetType())
            return false;

        Entity<TKey> item = (Entity<TKey>)obj;

        if (item.IsTransient() || this.IsTransient())
            return false;
        else
            return item.Id.Equals(this.Id);
    }

    /// <summary>
    /// 這個方法用來輔助對比兩個對象是否相等
    /// </summary>
    /// <returns></returns>
    public override int GetHashCode()
    {
        if (!IsTransient())
        {
            if (!_requestedHashCode.HasValue)
                _requestedHashCode = this.Id.GetHashCode() ^ 31;

            return _requestedHashCode.Value;
        }
        else
            return base.GetHashCode();
    }

    /// <summary>
    /// 表示對象是否為全新創建的,未持久化的
    /// </summary>
    /// <returns></returns>
    public bool IsTransient()
    {
        // 如果它沒有 Id 就表示它沒有持久化
        return EqualityComparer<TKey>.Default.Equals(Id, default);
    }

    public override string ToString()
    {
        return $"[Entity: {GetType().Name}] Id = {Id}";
    }

    /// <summary>
    /// 操作符 == 重載
    /// 借助上面的 Equals 方法
    /// 使得可以直接用 == 判斷兩個領域對象是否相等
    /// </summary>
    /// <param name="left"></param>
    /// <param name="right"></param>
    /// <returns></returns>
    public static bool operator ==(Entity<TKey> left, Entity<TKey> right)
    {
        if (Object.Equals(left, null))
            return (Object.Equals(right, null)) ? true : false;
        else
            return left.Equals(right);
    }

    /// <summary>
    /// 操作符 != 重載
    /// </summary>
    /// <param name="left"></param>
    /// <param name="right"></param>
    /// <returns></returns>
    public static bool operator !=(Entity<TKey> left, Entity<TKey> right)
    {
        return !(left == right);
    }
}

2、聚合根接口 IAggregateRoot

namespace GeekTime.Domain
{
    public interface IAggregateRoot
    {
    }
}

聚合根接口實際上是一個空接口,它不實現任何的方法,它的作用是在實現倉儲層的時候,讓一個倉儲對應一個聚合根

3、領域事件接口 IDomainEvent

namespace GeekTime.Domain
{
    public interface IDomainEvent : INotification
    {
    }
}

4、域事件處理接口 IDomainEventHandler

namespace GeekTime.Domain
{
    public interface IDomainEventHandler<TDomainEvent> : INotificationHandler<TDomainEvent> 
        where TDomainEvent : IDomainEvent
    {
    }
}

5、還有一個領域模型里面比較關鍵的值對象 ValueObject

值對象的定義比較特殊,因為它是沒有 Id 的,所以沒有關於 Id 的定義,並且沒有對值對象定義接口

重點實現了它是否相等的判斷,也是重載了 Equals 這個方法和 GetHashCode 這個方法

protected static bool EqualOperator(ValueObject left, ValueObject right)
{
    if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
    {
        return false;
    }
    return ReferenceEquals(left, null) || left.Equals(right);
}

protected static bool NotEqualOperator(ValueObject left, ValueObject right)
{
    return !(EqualOperator(left, right));
}

public override int GetHashCode()
{
    return GetAtomicValues()
     .Select(x => x != null ? x.GetHashCode() : 0)
     .Aggregate((x, y) => x ^ y);
}

它有一個特殊的抽象方法的定義,獲取它的原子值

protected abstract IEnumerable<object> GetAtomicValues();

這個方法的作用是將值對象的字段輸出出來,作為唯一標識來判斷兩個對象是否相等,可以看到 Equals 的定義里面也是調用了獲取原子值這個方法來判斷它是否相等

public override bool Equals(object obj)
{
    if (obj == null || obj.GetType() != GetType())
    {
        return false;
    }
    ValueObject other = (ValueObject)obj;
    IEnumerator<object> thisValues = GetAtomicValues().GetEnumerator();
    IEnumerator<object> otherValues = other.GetAtomicValues().GetEnumerator();
    while (thisValues.MoveNext() && otherValues.MoveNext())
    {
        if (ReferenceEquals(thisValues.Current, null) ^ ReferenceEquals(otherValues.Current, null))
        {
            return false;
        }
        if (thisValues.Current != null && !thisValues.Current.Equals(otherValues.Current))
        {
            return false;
        }
    }
    return !thisValues.MoveNext() && !otherValues.MoveNext();
}

接下來看一下定義的 Order 實體

public class Order : Entity<long>, IAggregateRoot
{
    public string UserId { get; private set; }

    public string UserName { get; private set; }

    public Address Address { get; private set; }

    public int ItemCount { get; private set; }

    protected Order()
    { }

    public Order(string userId, string userName, int itemCount, Address address)
    {
        this.UserId = userId;
        this.UserName = userName;
        this.Address = address;
        this.ItemCount = itemCount;

        this.AddDomainEvent(new OrderCreatedDomainEvent(this));
    }

    public void ChangeAddress(Address address)
    {
        this.Address = address;
    }
}

它首先實現了 Entity ,這一個在上一節已經講過,另外一個 Order 定義為一個聚合根,它需要實現聚合根接口 IAggregateRoot

實體中字段的 set 設置為 private,這樣的好處是 Order 所有的數據的操作都應該由實體負責,而不應該被外部對象去操作,從而讓領域模型符合封閉開放的原則

對於領域模型的操作,都應該是定義具有業務邏輯含義的方法來定義

比如說 ChangeAddress,就定義一個 ChangeAddress 的方法,把新的地址傳進來,由領域模型負責賦值

這里面就可以添加一些地址的校驗,比如新的地址是否能夠與舊的地址距離太遠

看一下地址的定義

public class Address : ValueObject
{
    public string Street { get; private set; }
    public string City { get; private set; }
    public string ZipCode { get; private set; }

    public Address() { }
    public Address(string street, string city, string zipcode)
    {
        Street = street;
        City = city;
        ZipCode = zipcode;
    }

    protected override IEnumerable<object> GetAtomicValues()
    {
        yield return Street;
        yield return City;
        yield return ZipCode;
    }
}

只能通過構造函數給值對象賦值,這里面需要注意的是重載了獲取原子值的方法,使用了 yield return

總結一下

在定義領域模型的時候,首先領域模型的字段的修改應該設置為私有的

使用構造函數來表示對象的創建,它的初始值都是由構造函數的參數來賦值的

另外需要定義有業務含義的動作來操作模型的字段

領域模型只負責自己數據的處理,領域服務或者命令負責調用領域模型的業務動作

樣就可以區分領域模型的內在邏輯和外在邏輯,使代碼結構更加合理

GitHub源碼鏈接:

https://github.com/witskeeper/geektime

知識共享許可協議

本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。

歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含鏈接: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改后的作品務必以相同的許可發布。

如有任何疑問,請與我聯系 (MingsonZheng@outlook.com) 。


免責聲明!

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



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