2011-04-29 11 views
9

Me encontré con una situación en la que debía extender la funcionalidad de una clase determinada, pero no estoy seguro de la mejor manera de hacerlo. Empecé invocando la funcionalidad "hacia arriba" y ahora he cambiado a "hacia abajo", pero veo problemas con ambos. Déjame explicarte lo que quiero decir. En primer lugar, el enfoque "hacia arriba":¿Cuál es la mejor manera de ampliar la funcionalidad?

public class ParentValidator 
{ 
    public void validate() { 
     // Some code 
    } 
} 

public class ChildValidator extends ParentValidator 
{ 
    @Override 
    public void validate() { 
     super.validate(); 
     // Some code 
    } 
} 

public class GrandchildValidator extends ChildValidator 
{ 
    @Override 
    public void validate() { 
     super.validate(); 
     // Some code 
    } 
} 

Esto funciona perfectamente bien, pero requiere que siempre me acuerdo de colocar super.validate() en mi cuerpo método o la lógica de la clase padre (s) ganó' t se ejecutará. Además, la extensión de esta manera puede considerarse "insegura" debido al hecho de que una clase hija podría reemplazar/modificar el código definido en la clase principal. Esto es lo que llamo invocar métodos "hacia arriba" porque estoy invocando métodos de clases de nivel superior a medida que avanzo.

Para contrarrestar estas deficiencias, decidí hacer que ParentValidator.validate() fuera el final y hacer que invoque un método diferente. Esto es lo que mi código se modificó para:

public class ParentValidator 
{ 
    public final void validate() { 
     // Some code 

     subValidate(); 
    } 

    protected void subValidate() {} 
} 

public class ChildValidator extends ParentValidator 
{ 
    @Override 
    public final void subValidate() { 
     // Some code 

     subSubValidate(); 
    } 

    protected void subSubValidate() {} 
} 

public class GrandchildValidator extends ChildValidator 
{ 
    @Override 
    public void subSubBalidate() { 
     // Some code 

     subSubSubValidate(); 
    } 

    protected void subSubSubValidate(); 
} 

Esto es lo que me refiero cuando digo que estoy llamando a la baja, ya que cada clase invoca métodos en las clases "hacia abajo" la cadena de herencia.

Usando este enfoque, se me puede garantizar que se ejecutará la lógica en la (s) clase (s) primaria (s), lo cual me gusta. Sin embargo, no escala bien. Cuantas más capas de herencia tengo, más fea es. En un nivel, creo que esto es muy elegante. En dos niveles, comienza a parecer de mala calidad. En tres o más, es horrible.

Además, así como tuve que recordar invocar super.validate() como la primera línea de cualquiera de los métodos de validación de mis hijos, ahora debo recordar invocar algún método "subValidar" al final de cualquiera de los métodos de validación de mis padres, por lo que no parecieron mejorar.

Hay una manera mejor de hacer este tipo de extensión que ni siquiera he tocado. Cualquiera de estos enfoques tiene algunos defectos serios y me pregunto si hay un mejor patrón de diseño que podría estar usando.

Respuesta

4

En lo que usted describe como su primer acercamiento que está utilizando la herencia simple, el segundo enfoque está más cerca de lo que el Gang of Four [GoF] llama un Template Method Pattern porque su clase padre es utilizando el llamado Hollywood Principle: "no nos llame, lo llamaremos".

Sin embargo, podría beneficiarse al declarar el método subvalidate() como abstracto en la clase principal y, con esto, asegurarse de que todas las subclases estén obligadas a implementarlo. Entonces sería un verdadero método de plantilla.

public abstract class ParentValidator 
{ 
    public final void validate() { 
     //some code 
     subValidate(); 
    } 
    protected abstract void subValidate() {} 
} 

Dependiendo de lo que esté haciendo hay otros patrones que podrían ayudarlo a hacer esto de otra manera. Por ejemplo, puede usar un Strategy Pattern para realizar las validaciones, y al favorecer la composición sobre la herencia, como se sugirió anteriormente, pero una consecuencia es que necesitará más clases de validación.

public abstract class ParentValidator 
    { 
     private final ValidatorStrategy validator; 

     protected ParentValidator(ValidatorStrategy validator){ 
      this.validator = validator; 
     } 

     public final void validate() { 
      //some code 
      this.validator.validate(); 
     } 
    } 

Luego puede proporcionar estrategias de validación específicas para cada tipo de Validator que tenga.

Si desea obtener lo mejor de ambos mundos, podría considerar implementar la solución como Decorator Pattern, donde las subclases pueden ampliar la funcionalidad de una clase principal y seguir con una interfaz común.

public abstract class ValidatorDecorator implements Validator 
     { 
      private final Validator validator; 

      protected ParentValidator(Validator validator){ 
       this.validator = validator; 
      } 

      public final void validate() { 
       //some code 
       super.validate(); //still forced to invoke super 
       this.validator.validate(); 
      } 
     } 

Todos los patrones tienen consecuencias, ventajas y desventajas que debe tener en cuenta cuidadosamente.

1

Prefiero usar composición sobre herencia si su subSubSubValidate está relacionado con la funcionalidad general. Puede extraer una nueva clase y moverla allí de lo que puede usar sin herencia en las otras clases.

Hay también

"favor 'composición de objetos' sobre 'herencia de clases'." (Cuadrilla de cuatro 1995: 20)

0

tal vez un vistazo al patrón de visitante puede ayudarlo a desarrollar su patrón.

Éstos son algunos datos sobre ella: http://en.wikipedia.org/wiki/Visitor_pattern

+0

La única limitación de este enfoque es que debe tener una devolución de llamada en el objeto que se está visitando. Si el objeto está "cerrado", entonces debes resolverlo para reflejarlo (feo). –

2

Preferiría 1) programar contra interfaces, y 2) optar por composición sobre herencia. Así es como lo han hecho I. A algunas personas les gusta, otras no. Funciona.

// java pseudocode below, you'll need to work the wrinkles out 

/** 
* Defines a rule or set of rules under which a instance of T 
* is deemed valid or invalid 
**/ 
public interface ValidationRule<T> 
{ 
    /** 
    * @return String describing invalidation condition, or null 
    * (indicating then that parameter t is valid */ 
    **/ 
    String apply(final T t); 
} 

/** 
* Utility class for enforcing a logical conjunction 
* of zero or more validatoin rules on an object. 
**/ 
public final class ValidatorEvaluator 
{ 
    /** 
    * evaluates zero or more validation rules (as a logical 
    * 'AND') on an instance of type T. 
    **/ 
    static <T> String apply(final T t, ValidationRule<T> ... rules) 
    { 
     for(final ValidationRules<T> v : rules) 
     { 
      String msg = v.apply(t); 
      if(msg != null) 
      { 
      return msg; // t is not valid 
      } 
     } 
     return null; 
    } 
} 

// arbitrary dummy class that we will test for 
// i being a positive number greater than zero 
public class MyFoo 
{ 
    int i; 
    public MyFoo(int n){ i = n; } 
    /// 
} 

public class NonZeroValidatorRule implements ValidatorRule<MyFoo> 
{ 
    public String apply(final MyFoo foo) 
    { 
     return foo.i == 0 ? "foo.i is zero!" : null; 
    } 
} 

// test for being positive using NonZeroValidatorRule and an anonymous 
// validator that tests for negatives 

String msg = ValidatorEvaluator.apply(new MyFoo(1), 
             new NonZeroValidatorRule(), 
             new ValidatorRule<MyFoo>() 
             { 
             public String apply(final MyFoo foo) 
             { 
              return foo.i < 0 ? "foo.i is negative!" : null; 
             } 
             } 
            ); 

if(msg == null) 
{ 
    \\ yay! 
    ... 
} 
else 
{ 
    \\ nay... 
    someLogThingie.log("error: myFoo now workie. reason=" + msg); 
} 

Reglas de evaluación más complejas y no triviales pueden implementarse de esta manera.

La clave aquí es que no debe usar la herencia a menos que exista una relación is-a. No lo use solo para reciclar o encapsular la lógica. Si todavía siente que necesita usar la herencia, entonces no se exceda tratando de asegurarse de que cada subclase ejecute la lógica de validación heredada de la superclase. Tener implementaciones de cada subclase hacer una ejecución explícita sobre súper:

public class ParentValidator 
{ 
    public void validate() { // notice that I removed the final you originally had 
     // Some code 
    } 
} 

pubic class ChildValidator extends ParentValidator 
{ 
    @Override 
    public void validate() { 
     // Some code 
     super.validate(); // explicit call to inherited validate 
     // more validation code 
    } 
} 

simplificar las cosas, y no tratan de hacer imposible o a prueba de tontos. Existe una diferencia entre codificar de forma defensiva (una buena práctica) y codificar contra estúpido (un esfuerzo inútil). Simplemente establezca las reglas de codificación sobre cómo subclasificar sus validadores. Es decir, pon la responsabilidad en los implementadores. Si no pueden seguir las pautas, ninguna cantidad de codificación defensiva protegerá su sistema contra su estupidez. Ergo, deja las cosas claras y simples.

Cuestiones relacionadas