views:

288

answers:

4

Hi all,

I've a question regarding enforcing a business rule via a specification pattern. Consider the following example:

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()
    {
    }
}

The business rule should enforce that there cannot be a child in the collection which validity period intersects with another.

For that I would like to implement a specification that is then be used to throw an exception if an invalid child is added AND as well can be used to check whether the rule will be violated BEFORE adding the child.

Like:


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

But in this example the child accesses the parent. And to me that doesnt seem that correct. That parent might not exist when the child has not been added to the parent yet. How would you implement it?

A: 

Would you not have an If statement to check that a parent was not null and if so return false?

Burt
That can be a possibility. But I am just wondering if I am using this pattern the right way... Wouldn't the validity be unique when there is no parent?
Chris
+2  A: 

I think the Parent should probably do the validation. So in the parent you might have a canBeParentOf(Child) method. This method would also be called at the top of your AddChild method--then the addChild method throws an exception if canBeParentOf fails, but canBeParentOf itself does not throw an exception.

Now, if you want to use "Validator" classes to implement canBeParentOf, that would be fantastic. You might have a method like validator.validateRelationship(Parent, Child). Then any parent could hold a collection of validators so that there could be multiple conditions preventing a parent/child relationship. canBeParentOf would just iterate over the validators calling each one for the child being added--as in validator.canBeParentOf(this, child);--any false would cause canBeParentOf to return a false.

If the conditions for validating are always the same for every possible parent/child, then they can either be coded directly into canBeParentOf, or the validators collection can be static.

An aside: The back-link from child to parent should probably be changed so that it can only be set once (a second call to the set throws an exception). This will A) Prevent your child from getting into an invalid state after it's been added and B) detect an attempt to add it to two different parents. In other words: Make your objects as close to immutable as possible. (Unless changing it to different parents is possible). Adding a child to multiple parents is obviously not possible (from your data model)

Bill K
A: 

You are trying to guard against Child being in an invalid state. Either

  • use the builder pattern to create fully populated Parent types so that everything you expose to the consumer is always in a valid state
  • remove the reference to the Parent completely
  • have Parent create all instances of Child so this can never occur

The latter case might look (something) like this (in 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
  }
}
McDowell
+3  A: 
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 
      (c.ValidFrom >= ValidFrom && c.ValidFrom < ValidTo) ||
      (c.ValidTo > ValidFrom && c.ValidTo <= ValidTo) ||
      (c.ValidFrom <= ValidFrom && c.ValidTo >= ValidTo);
  }
}
Jordão