2010-01-28 69 views
5

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

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

但在这个例子中,孩子上网家长。对我来说,这看起来并不错。当孩子尚未被添加到父母时,该父母可能不存在。你将如何实现它?

回答

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

UPDATE:

当然不过,该规范模式的真正威力在于当你可以插上,并结合不同的规则。你可以有一个这样的接口(可能有更好的名字):

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中。

0

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

+0

这可能是一种可能性。但我只是想知道我是否以正确的方式使用这种模式......当没有父母时,有效性不是唯一的吗? – Chris 2010-01-28 21:39:49

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)检测到将其添加到两个不同父母的尝试。换句话说:让你的对象尽可能接近不变。 (除非将其更改为不同的父母是可能的)。将孩子添加到多个父母显然是不可能的(从您的数据模型中)

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