zoukankan      html  css  js  c++  java
  • .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

    总结一下

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

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

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

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

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

    知识共享许可协议

    本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

    欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

    如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。

  • 相关阅读:
    MySQL学习笔记
    Git常用命令
    MacBook Pro m1安装swoole PHP版本7.4
    斐波那契数列实现的2种方法
    归纳一些比较好用的函数
    阶乘的实现
    冒泡排序
    PHP上传图片
    PHPStorm常用快捷键
    DataTables的使用
  • 原文地址:https://www.cnblogs.com/MingsonZheng/p/12528546.html
Copyright © 2011-2022 走看看