【问题标题】:Specification pattern implementation help规范模式实现帮助
【发布时间】:2011-01-10 14:47:25
【问题描述】:

我有一个关于通过规范模式执行业务规则的问题。考虑以下示例:

public class Parent
{
    private ICollection<Child> children;

    public ReadOnlyCollection Children { get; }

    public void AddChild(Child child)
    {
        child.Parent = this;
        children.Add(child);
    }
}


public class Child
{
    internal Parent Parent
    {
        get;
        set;
    }

    public DateTime ValidFrom;
    public DateTime ValidTo;

    public Child()
    {
    }
}

业务规则应该强制集合中不能有一个有效期与另一个相交的孩子。

为此,我想实现一个规范,如果添加了无效的孩子,则该规范用于引发异常,并且还可用于在添加孩子之前检查是否违反规则。

喜欢:


public class ChildValiditySpecification
{
    bool IsSatisfiedBy(Child child)
    {
        return child.Parent.Children.Where(<validityIntersectsCondition here>).Count > 0;
    }
}

但是在这个例子中,孩子访问了父母。对我来说,这似乎并不正确。当子级尚未添加到父级时,该父级可能不存在。你将如何实现它?

【问题讨论】:

    标签: c# java .net oop domain-driven-design


    【解决方案1】:
    public class Parent {
      private List<Child> children;
    
      public ICollection<Child> Children { 
        get { return children.AsReadOnly(); } 
      }
    
      public void AddChild(Child child) {
        if (!child.IsSatisfiedBy(this)) throw new Exception();
        child.Parent = this;
        children.Add(child);
      }
    }
    
    public class Child {
      internal Parent Parent { get; set; }
    
      public DateTime ValidFrom;
      public DateTime ValidTo;
    
      public bool IsSatisfiedBy(Parent parent) { // can also be used before calling parent.AddChild
        return parent.Children.All(c => !Overlaps(c));
      }
    
      bool Overlaps(Child c) { 
        return ValidFrom <= c.ValidTo && c.ValidFrom <= ValidTo;
      }
    }
    

    更新:

    当然,规范模式的真正威力在于您可以插入并组合不同的规则。你可以有这样的界面(可能有更好的名字):

    public interface ISpecification {
      bool IsSatisfiedBy(Parent parent, Child candidate);
    }
    

    然后像这样在Parent上使用它:

    public class Parent {
      List<Child> children = new List<Child>();
      ISpecification childValiditySpec;
      public Parent(ISpecification childValiditySpec) {
        this.childValiditySpec = childValiditySpec;
      }
      public ICollection<Child> Children {
        get { return children.AsReadOnly(); }
      }
      public bool IsSatisfiedBy(Child child) {
        return childValiditySpec.IsSatisfiedBy(this, child);
      }
      public void AddChild(Child child) {
        if (!IsSatisfiedBy(child)) throw new Exception();
        child.Parent = this;
        children.Add(child);
      }
    }
    

    Child 很简单:

    public class Child {
      internal Parent Parent { get; set; }
      public DateTime ValidFrom;
      public DateTime ValidTo;
    }
    

    您可以实现多个规范或复合规范。这是您示例中的示例:

    public class NonOverlappingChildSpec : ISpecification {
      public bool IsSatisfiedBy(Parent parent, Child candidate) {
        return parent.Children.All(child => !Overlaps(child, candidate));
      }
      bool Overlaps(Child c1, Child c2) {
        return c1.ValidFrom <= c2.ValidTo && c2.ValidFrom <= c1.ValidTo;
      }
    }
    

    请注意,让Child 的公共数据不可变(仅通过构造函数设置)更有意义,这样任何实例都不能以使Parent 无效的方式更改其数据。

    另外,请考虑将日期范围封装在 specialized abstraction 中。

    【讨论】:

      【解决方案2】:

      我认为父母可能应该进行验证。所以在父母中你可能有一个 canBeParentOf(Child) 方法。此方法也将在您的 AddChild 方法的顶部调用 - 如果 canBeParentOf 失败,则 addChild 方法会引发异常,但 canBeParentOf 本身不会引发异常。

      现在,如果您想使用“Validator”类来实现 canBeParentOf,那就太好了。您可能有一个类似 validator.validateRelationship(Parent, Child) 的方法。然后任何父级都可以持有一组验证器,这样就可能有多个条件阻止父/子关系。 canBeParentOf 只会遍历为要添加的孩子调用每个验证器的验证器——如在 validator.canBeParentOf(this, child); 中——任何 false 都会导致 canBeParentOf 返回 false。

      如果每个可能的父/子的验证条件始终相同,那么它们可以直接编码到 canBeParentOf 中,或者验证器集合可以是静态的。

      顺便说一句:从子级到父级的反向链接可能应该更改,以便它只能设置一次(对 set 的第二次调用会引发异常)。这将 A)防止您的孩子在添加后进入无效状态,并且 B)检测尝试将其添加到两个不同的父母。换句话说:使您的对象尽可能接近不可变。 (除非可以将其更改为不同的父母)。将孩子添加到多个父母显然是不可能的(根据您的数据模型)

      【讨论】:

        【解决方案3】:

        你不会有一个 If 语句来检查父级是否不为空,如果是则返回 false 吗?

        【讨论】:

        • 这是有可能的。但我只是想知道我是否以正确的方式使用这种模式......当没有父母时有效性不是唯一的吗?
        【解决方案4】:

        您正试图防止Child 处于无效状态。要么

        • 使用构建器模式创建完全填充的 Parent 类型,以便您向消费者公开的所有内容始终处于有效状态
        • 完全删除对Parent的引用
        • Parent 创建Child 的所有实例,这样就不会发生这种情况

        后一种情况可能看起来像这样(在 Java 中):

        public class DateRangeHolder {
          private final NavigableSet<DateRange> ranges = new TreeSet<DateRange>();
        
          public void add(Date from, Date to) {
            DateRange range = new DateRange(this, from, to);
            if (ranges.contains(range)) throw new IllegalArgumentException();
            DateRange lower = ranges.lower(range);
            validate(range, lower);
            validate(range, ranges.higher(lower == null ? range : lower));
            ranges.add(range);
          }
        
          private void validate(DateRange range, DateRange against) {
            if (against != null && range.intersects(against)) {
              throw new IllegalArgumentException();
            }
          }
        
          public static class DateRange implements Comparable<DateRange> {
            // implementation elided
          }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2011-11-15
          • 2011-11-07
          • 1970-01-01
          • 2012-10-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多