2010-01-28 21 views
5

Tengo una pregunta sobre la aplicación de una regla empresarial a través de un patrón de especificación. Consideremos el siguiente ejemplo:Ayuda de implementación del patrón de especificación

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

regla El negocio debe cumplir que no puede haber un niño en la colección que el período de validez se cruza con otra.

Para eso me gustaría implementar una especificación que luego se utilizará para lanzar una excepción si se agrega un elemento secundario no válido Y también se puede usar para verificar si la regla se violará ANTES de agregar el elemento secundario.

igual:


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

Sin embargo, en este ejemplo, el niño tiene acceso a la matriz. Y para mí eso no parece tan correcto. Es posible que ese padre no exista cuando el hijo aún no se haya agregado al padre. ¿Cómo lo implementarías?

Respuesta

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

ACTUALIZACIÓN:

Pero, por supuesto, el poder real del patrón de especificación es cuando se puede conectar y combinar diferentes reglas. Usted puede tener una interfaz como esta (posiblemente con un nombre mejor):

public interface ISpecification { 
    bool IsSatisfiedBy(Parent parent, Child candidate); 
} 

y luego usarlo como este en 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 sería simple:

public class Child { 
    internal Parent Parent { get; set; } 
    public DateTime ValidFrom; 
    public DateTime ValidTo; 
} 

Y podría implementar múltiples especificaciones o especificaciones compuestas. Esta es la única de su ejemplo:

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

Tenga en cuenta que tiene más sentido para hacer Child 's de datos públicas inmutable (sólo se establece a través del constructor) para que ningún caso se han cambiado sus datos de una manera que lo haría invalidar un Parent.

Además, considere encapsular el rango de fechas en un specialized abstraction.

0

¿No tendría una instrucción If para verificar que uno de los padres no fuera nulo y, en caso afirmativo, devuelva false?

+0

Eso puede ser una posibilidad. Pero me pregunto si estoy usando este patrón de la manera correcta ... ¿No sería la validez única cuando no hay un padre? – Chris

2

Creo que Parent debería probablemente hacer la validación. Entonces en el padre puede tener un método canBeParentOf (Child). Este método también se invocará en la parte superior de su método AddChild; luego, el método addChild genera una excepción si canBeParentOf falla, pero canBeParentOf no lanza una excepción.

Ahora, si quieres usar las clases de "Validator" para implementar canBeParentOf, eso sería fantástico. Es posible que tenga un método como validator.validateRelationship (Parent, Child). Entonces, cualquier padre podría tener una colección de validadores para que haya múltiples condiciones que impidan una relación padre/hijo. canBeParentOf simplemente iteraría sobre los validadores llamando a cada uno para el niño que se agrega, como en validator.canBeParentOf (this, child); - cualquier falso causaría que canBeParentOf devolviera un falso.

Si las condiciones para la validación son siempre las mismas para todos los padres/hijos posibles, entonces pueden codificarse directamente en canBeParentOf o la colección de validadores puede ser estática.

Un aparte: El back-link de hijo a padre probablemente debería cambiarse para que solo pueda establecerse una vez (una segunda llamada al conjunto arroja una excepción). Esto hará que A) evite que su hijo entre en un estado inválido después de que se haya agregado y B) detecte un intento de agregarlo a dos padres diferentes. En otras palabras: haga que sus objetos sean lo más cercanos e inmutables posible. (A menos que sea posible cambiarlo a padres diferentes). Es obvio que no es posible agregar un hijo a varios padres (de su modelo de datos)

0

Está tratando de evitar que Child esté en un estado inválido.De cualquier

  • utilice el Builder para crear completamente ocupado Parent tipos de modo que todo lo que se expone al consumidor está siempre en un estado válido
  • eliminar la referencia a la Parent completamente
  • tener Parent crear todas las instancias de Child por lo que este nunca puede ocurrir

El último caso puede tener un aspecto (algo) como esto (en 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 
    } 
} 
Cuestiones relacionadas