2010-01-28 224 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 
    } 
}