> 领域设计 - 所有的领域应该有一个抽象基类,而且是一个泛型 ``` public abstract class Entity { public T Id{get;protected set;} } ``` - DDD中有一个很重要的概念,叫作Object Value Type。会把一个Aggregate里的对象类型看作是值类型。这样做一方面是减少Aggregate对关系的依赖,一方面可以很好判断Aggregage的状态。同样也是一个泛型抽象基类,如果一个Object类型的对象想要被当作值类型处理,那就继承这个泛型抽象基类。 - object value type怎么理解 - 值对象 主要描述领域的某个方面而本身没有概念表示的对象。 - 主要是为了方便比较领域对象。 - 以前 ``` public class Order { public List Lines{get;set;} public Address Address{get;set;} } ``` - 实际上Order里面包含了对象类型,是很难比较的 - 现在 ``` public class OrderLine:ValueObject, public class Address:ValueObject
``` - 这就把原来是引用类型的对象OrderLine和Address转换成值类型了,这就减少了对象之间的相互依赖。 ``` public abstract class ValueObject where T : ValueObject { /// /// 获取子类需要比较的属性 /// 抽象方法,通过子类override实现 /// protected abstract IEnumerable GetAttributesToIncludeInEqualityCheck(); /// /// 比较 /// 可以被子类重写 /// public virtual bool Equals(T other) { if (other == null) return false; //两个对象的属性值比较 return GetAttributesToIncludeInEqualityCheck().SequenceEqual(other.GetAttributesToIncludeInEqualityCheck()); } /// /// 重写object基类的比较大小的方法 /// public override bool Equals(object other) { return Equals(other as T); } /// /// 重写object基类的GetHashCode /// public override int GetHashCode() { var hash = 17; foreach (var obj in this.GetAttributesToIncludeInEqualityCheck()) { hash = hash * 31 + (obj == null ? 0 : obj.GetHashCode()); } return hash; } /// /// 重新定义== /// public static bool operator ==(ValueObject left, ValueObject right) { return Equals(left, right); } /// /// 重新定义= /// public static bool operator !=(ValueObject left, ValueObject right) { return !(left==right); } } ``` - 以上,ValueObject就是Object Value Type的抽象基类,大致作了如下事情: - 重写基类object的Equals方法 - 重写基类object的GetHashCode方法 - 提供一个抽象方法,让子类根据各自不同的属性比较大小,让子类重写 - 有一个自定义的比较大小的方法,会调用基类的抽象方法 - 重新定义==和!= - 所有的领域也好,或者Aggregate也好,都需要事件驱动。关于事件,同样需要一个基类。 ``` public abstract class DomainEvent { public Guid Id { get; protected set; } public DateTime OccuredOn { get; protected set; } protected DomainEvent(Guid id, DateTime occuredOn) { Id = id; OccuredOn = occuredOn; } protected DomainEvent() { Id = Guid.NewGuid(); OccuredOn = SysTime.Now(); } } 以上,DomainEvent就是事件的抽象基类。 ``` - 一个属性是Guid类型,这是很有特色的一个点,事件居然也有Guid类型,就像很多领域都有主键一样 - 在DDD中,完全可以实现Event Sourcing,事件追溯。注意,这里的Event Sourcing和前面说的Event Storming不一样。Event Soucing适合的场景包括:银行、保险公司等。对于一个领域来说,只要执行了相同数量、相同顺序的事件,领域就会来到一个确定的状态。据说,前端框架Redux就用到了Event Stroming,让领域的状态可追溯。当然,一般的项目用不上Event Sourcing。 - 以上,DateTime类型的属性OccurredOn的值是通过一个静态方法获取到的。这样做有什么好处呢? ``` public class SysTime { public static Func CurrentTimeProvider { get; set; } = () => DateTime.Now; public static DateTime Now() => CurrentTimeProvider(); } ``` - 以上,Now方法类似方法内的一个工厂方法,产生DateTime的实例。但实例是通过CurrentTimeProvider这个委托得到的。不难理解,通过委托获取DateTime实例增加了灵活性。所谓的委托,就是定义好输入形参和返回类型,委托就是一个方法,把方法看作一个变量,调用委托,就是调用方法。 - 以上,有了领域的基类Entity, 有了Object Value Type, 有了事件的基类。接下来,事件需要被发布,这样系统内都某个事件订阅者能获悉并执行。所以,事件发布需要一个接口: ``` public interface IEventPublisher { void Publish(DomainEvent @event); } ``` - 以上,所有的要么是抽象基类,要么是接口,我们可以体会到:在领域层基本都是抽象的。也就是在我们所熟悉的洋葱架构中,越内层的越抽象,越外层越具体,越抽象也意味着更低的耦合性、更好的扩展性。而且,领域层,乃至领域层的外面一层都是抽象居多,这一点也方便单元测试。 - 接下来,会把Event Storming中的设计拿来,考虑如何定义领域 - 首先,所有的领域都会有主键,通常使用Guid类型,但是Guid类型是一个Object类型,这在DDD的领域设计中需要把之转换成值类型,也就是Object Value Type。 ``` public class EntityId : ValueObject { protected override IEnumerable GetAttributesToIncludeInEqualityCheck() { yield return Value; } public Guid Value { get; } public EntityId(Guid value) { Value = value; } protected EntityId() { } } ``` - 以上,EntityId就是Object Value Type,派生于ValueObject。GetAttributesToIncludeInEqualityCheck方法是重写或者说实现基类的方法,当ValueObject的Equals方法的时候会调用GetAttributesToIncludeInEqualityCheck方法。EntityId有一个比较奇怪的地方,就是有个无参的构造函数,这个是为EF Core准备的。 - 接着,创建第一个领域或者说Aggregate: ``` /// /// 报警 /// public class Warning : Entity { } ``` - 以上,Warning派生于领域抽象基类Entity,这里的T表示领域的主键类型,这里的主键类型是EntityId,也就是Object Value Type, 可以看作是一个像int类型那样的值类型。