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
同樣看一下 Entity 的定義
public abstract class Entity : IEntity
public abstract class Entity<TKey> : Entity, IEntity<TKey>
同樣地定義了一個 Entity 和 Entity
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
實體中字段的 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) 。