34

.NET Core开发实战(第27课:定义Entity:区分领域模型的内在逻辑和外在行为)--学习...

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzAwNTMxMzg1MA%3D%3D&%3Bmid=2654077838&%3Bidx=3&%3Bsn=f4777f308a0b7625a1383802411ad11c
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

总结一下

在定义领域模型的时候,首先领域模型的字段的修改应该设置为私有的

使用构造函数来表示对象的创建,它的初始值都是由构造函数的参数来赋值的

另外需要定义有业务含义的动作来操作模型的字段

领域模型只负责自己数据的处理,领域服务或者命令负责调用领域模型的业务动作

样就可以区分领域模型的内在逻辑和外在逻辑,使代码结构更加合理

ARZRF3J.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK