规范模式实现帮助

时间:2010-01-28 21:31:02

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

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

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;
    }
}

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

4 个答案:

答案 0 :(得分:6)

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

答案 1 :(得分:2)

我认为家长应该做验证。所以在父级中你可能有一个canBeParentOf(Child)方法。此方法也会在AddChild方法的顶部调用 - 如果canBeParentOf失败,则addChild方法会抛出异常,但canBeParentOf本身不会抛出异常。

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

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

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

答案 2 :(得分:0)

您是否没有If语句来检查父项是否为空,如果是,则返回false?

答案 3 :(得分:0)

您正试图防止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
  }
}