Справка по реализации шаблона спецификаций - PullRequest
5 голосов
/ 29 января 2010

У меня есть вопрос, касающийся применения бизнес-правила с помощью шаблона спецификации. Рассмотрим следующий пример:

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 ]

6 голосов
/ 29 января 2010
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.

Также рассмотрите возможность включения диапазона дат в специализированную абстракцию .

2 голосов
/ 29 января 2010

Я думаю, что Родитель, вероятно, должен сделать проверку. Так что в родителе у вас может быть метод canBeParentOf (Child). Этот метод также будет вызываться в верхней части вашего метода AddChild - тогда метод addChild выдает исключение, если canBeParentOf не удается, но сам canBeParentOf не выдает исключение.

Теперь, если вы хотите использовать классы "Validator" для реализации canBeParentOf, это было бы замечательно. У вас может быть такой метод, как validator.validateRelationship (Parent, Child). Тогда любой родитель может содержать коллекцию валидаторов, чтобы могло быть несколько условий, предотвращающих отношения родитель / потомок. canBeParentOf будет просто перебирать валидаторы, вызывающие каждый из них для добавляемого потомка - как в validator.canBeParentOf (this, child); - любое ложное приведет к тому, что canBeParentOf вернет false.

Если условия проверки всегда одинаковы для каждого возможного родителя / потомка, то они могут быть либо закодированы непосредственно в canBeParentOf, либо коллекция валидаторов может быть статической.

В сторону: обратная ссылка от дочернего элемента к родительскому, вероятно, должна быть изменена, чтобы ее можно было установить только один раз (второй вызов набора вызывает исключение). Это позволит: а) предотвратить переход вашего ребенка в недопустимое состояние после его добавления, и б) обнаружить попытку добавить его к двум разным родителям. Другими словами: сделайте ваши объекты как можно ближе к неизменным. (Если возможно изменить его на разных родителей). Добавление ребенка к нескольким родителям, очевидно, невозможно (из вашей модели данных)

0 голосов
/ 29 января 2010

Вы пытаетесь защититься от 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
  }
}
0 голосов
/ 29 января 2010

У вас не было бы оператора If, чтобы проверить, что родитель не был нулевым, и если да, вернуть false?

...