鼎鼎知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

28.领域设计.md 7.1KB

4 vuotta sitten
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. > 领域设计
  2. - 所有的领域应该有一个抽象基类,而且是一个泛型
  3. ```
  4. public abstract class Entity<T>
  5. {
  6. public T Id{get;protected set;}
  7. }
  8. ```
  9. - DDD中有一个很重要的概念,叫作Object Value Type。会把一个Aggregate里的对象类型看作是值类型。这样做一方面是减少Aggregate对关系的依赖,一方面可以很好判断Aggregage的状态。同样也是一个泛型抽象基类,如果一个Object类型的对象想要被当作值类型处理,那就继承这个泛型抽象基类。
  10. - object value type怎么理解
  11. - 值对象 主要描述领域的某个方面而本身没有概念表示的对象。
  12. - 主要是为了方便比较领域对象。
  13. - 以前
  14. ```
  15. public class Order
  16. {
  17. public List<OrderLine> Lines{get;set;}
  18. public Address Address{get;set;}
  19. }
  20. ```
  21. - 实际上Order里面包含了对象类型,是很难比较的
  22. - 现在
  23. ```
  24. public class OrderLine:ValueObject<OrderLine>,
  25. public class Address:ValueObject<Address>
  26. ```
  27. - 这就把原来是引用类型的对象OrderLine和Address转换成值类型了,这就减少了对象之间的相互依赖。
  28. ```
  29. public abstract class ValueObject<T> where T : ValueObject<T>
  30. {
  31. /// <summary>
  32. /// 获取子类需要比较的属性
  33. /// 抽象方法,通过子类override实现
  34. /// </summary>
  35. protected abstract IEnumerable<object> GetAttributesToIncludeInEqualityCheck();
  36. /// <summary>
  37. /// 比较
  38. /// 可以被子类重写
  39. /// </summary>
  40. public virtual bool Equals(T other)
  41. {
  42. if (other == null) return false;
  43. //两个对象的属性值比较
  44. return GetAttributesToIncludeInEqualityCheck().SequenceEqual(other.GetAttributesToIncludeInEqualityCheck());
  45. }
  46. /// <summary>
  47. /// 重写object基类的比较大小的方法
  48. /// </summary>
  49. public override bool Equals(object other)
  50. {
  51. return Equals(other as T);
  52. }
  53. /// <summary>
  54. /// 重写object基类的GetHashCode
  55. /// </summary>
  56. public override int GetHashCode()
  57. {
  58. var hash = 17;
  59. foreach (var obj in this.GetAttributesToIncludeInEqualityCheck())
  60. {
  61. hash = hash * 31 + (obj == null ? 0 : obj.GetHashCode());
  62. }
  63. return hash;
  64. }
  65. /// <summary>
  66. /// 重新定义==
  67. /// </summary>
  68. public static bool operator ==(ValueObject<T> left, ValueObject<T> right)
  69. {
  70. return Equals(left, right);
  71. }
  72. /// <summary>
  73. /// 重新定义=
  74. /// </summary>
  75. public static bool operator !=(ValueObject<T> left, ValueObject<T> right)
  76. {
  77. return !(left==right);
  78. }
  79. }
  80. ```
  81. - 以上,ValueObject<T>就是Object Value Type的抽象基类,大致作了如下事情:
  82. - 重写基类object的Equals方法
  83. - 重写基类object的GetHashCode方法
  84. - 提供一个抽象方法,让子类根据各自不同的属性比较大小,让子类重写
  85. - 有一个自定义的比较大小的方法,会调用基类的抽象方法
  86. - 重新定义==和!=
  87. - 所有的领域也好,或者Aggregate也好,都需要事件驱动。关于事件,同样需要一个基类。
  88. ```
  89. public abstract class DomainEvent
  90. {
  91. public Guid Id { get; protected set; }
  92. public DateTime OccuredOn { get; protected set; }
  93. protected DomainEvent(Guid id, DateTime occuredOn)
  94. {
  95. Id = id;
  96. OccuredOn = occuredOn;
  97. }
  98. protected DomainEvent()
  99. {
  100. Id = Guid.NewGuid();
  101. OccuredOn = SysTime.Now();
  102. }
  103. }
  104. 以上,DomainEvent就是事件的抽象基类。
  105. ```
  106. - 一个属性是Guid类型,这是很有特色的一个点,事件居然也有Guid类型,就像很多领域都有主键一样
  107. - 在DDD中,完全可以实现Event Sourcing,事件追溯。注意,这里的Event Sourcing和前面说的Event Storming不一样。Event Soucing适合的场景包括:银行、保险公司等。对于一个领域来说,只要执行了相同数量、相同顺序的事件,领域就会来到一个确定的状态。据说,前端框架Redux就用到了Event Stroming,让领域的状态可追溯。当然,一般的项目用不上Event Sourcing。
  108. - 以上,DateTime类型的属性OccurredOn的值是通过一个静态方法获取到的。这样做有什么好处呢?
  109. ```
  110. public class SysTime
  111. {
  112. public static Func<DateTime> CurrentTimeProvider { get; set; } = () => DateTime.Now;
  113. public static DateTime Now() => CurrentTimeProvider();
  114. }
  115. ```
  116. - 以上,Now方法类似方法内的一个工厂方法,产生DateTime的实例。但实例是通过CurrentTimeProvider这个委托得到的。不难理解,通过委托获取DateTime实例增加了灵活性。所谓的委托,就是定义好输入形参和返回类型,委托就是一个方法,把方法看作一个变量,调用委托,就是调用方法。
  117. - 以上,有了领域的基类Entity, 有了Object Value Type, 有了事件的基类。接下来,事件需要被发布,这样系统内都某个事件订阅者能获悉并执行。所以,事件发布需要一个接口:
  118. ```
  119. public interface IEventPublisher
  120. {
  121. void Publish(DomainEvent @event);
  122. }
  123. ```
  124. - 以上,所有的要么是抽象基类,要么是接口,我们可以体会到:在领域层基本都是抽象的。也就是在我们所熟悉的洋葱架构中,越内层的越抽象,越外层越具体,越抽象也意味着更低的耦合性、更好的扩展性。而且,领域层,乃至领域层的外面一层都是抽象居多,这一点也方便单元测试。
  125. - 接下来,会把Event Storming中的设计拿来,考虑如何定义领域
  126. - 首先,所有的领域都会有主键,通常使用Guid类型,但是Guid类型是一个Object类型,这在DDD的领域设计中需要把之转换成值类型,也就是Object Value Type。
  127. ```
  128. public class EntityId : ValueObject<EntityId>
  129. {
  130. protected override IEnumerable<object> GetAttributesToIncludeInEqualityCheck()
  131. {
  132. yield return Value;
  133. }
  134. public Guid Value { get; }
  135. public EntityId(Guid value)
  136. {
  137. Value = value;
  138. }
  139. protected EntityId() { }
  140. }
  141. ```
  142. - 以上,EntityId就是Object Value Type,派生于ValueObject<T>。GetAttributesToIncludeInEqualityCheck方法是重写或者说实现基类的方法,当ValueObject的Equals方法的时候会调用GetAttributesToIncludeInEqualityCheck方法。EntityId有一个比较奇怪的地方,就是有个无参的构造函数,这个是为EF Core准备的。
  143. - 接着,创建第一个领域或者说Aggregate:
  144. ```
  145. /// <summary>
  146. /// 报警
  147. /// </summary>
  148. public class Warning : Entity<EntityId>
  149. {
  150. }
  151. ```
  152. - 以上,Warning派生于领域抽象基类Entity<T>,这里的T表示领域的主键类型,这里的主键类型是EntityId,也就是Object Value Type, 可以看作是一个像int类型那样的值类型。