¿Cómo usar el patrón Decorador para agregar poca funcionalidad a objetos grandes?

8

Esta pregunta se refiere al uso del patrón Decorator para agregar poca funcionalidad a los objetos de clases grandes.

Siguiendo el patrón clásico de Decorator, considere la siguiente estructura de clase:

ingrese la descripción de la imagen aquí

Por ejemplo, imagina que esto sucede dentro de un juego. Las instancias de ConcreteCharacterDecoratorestán destinadas a agregar poca funcionalidad a la ConcreteCharacterque están 'envolviendo'.

Por ejemplo, methodA()devuelve un intvalor que representa el daño que el personaje inflige a los enemigos. El ConcreteCharacterDecoratorsimplemente agrega a este valor. Por lo tanto, solo necesita agregar código methodA(). La funcionalidad de methodB()permanece igual.

ConcreteCharacterDecorator se verá así:

class ConcreteCharacterDecorator extends AbstractCharacterDecorator{

    ConcreteCharacter character;

    public ConcreteCharacterDecorator(ConcreteCharacter character){
        this.character = character;
    }

    public int methodA(){
        return 10 + character.methodA();
    }

    public int methodB(){
        character.methodB(); // simply delegate to the wrapped object.
    }

}

Esto no es problema con clases pequeñas que contienen dos métodos.

¿Pero qué AbstractCharacterpasa si se definen 15 métodos? ConcreteCharacterDecoratortendría que implementarlos todos, a pesar de que solo está destinado a agregar poca funcionalidad.

Terminaré con una clase que contiene un método que agrega un poco de funcionalidad y otros 14 métodos que simplemente delegan en el objeto interno.

Se vería así:

class ConcreteCharacterDecorator extends AbstractCharacterDecorator{

    ConcreteCharacter character;

    public ConcreteCharacterDecorator(ConcreteCharacter character){
        this.character = character;
    }

    public int methodA(){
        return 10 + character.methodA();
    }

    public int methodB(){
        character.methodB(); // simply delegate to the wrapped object.
    }
    public int methodC(){
        character.methodC(); // simply delegate to the wrapped object.
    }
    public int methodD(){
        character.methodD(); // simply delegate to the wrapped object.
    }
    public int methodE(){
        character.methodE(); // simply delegate to the wrapped object.
    }
    public int methodF(){
        character.methodF(); // simply delegate to the wrapped object.
    }
    public int methodG(){
        character.methodG(); // simply delegate to the wrapped object.
    }
    public int methodH(){
        character.methodH(); // simply delegate to the wrapped object.
    }
    public int methodI(){
        character.methodI(); // simply delegate to the wrapped object.
    }
    public int methodJ(){
        character.methodJ(); // simply delegate to the wrapped object.
    }
    public int methodK(){
        character.methodK(); // simply delegate to the wrapped object.
    }
    public int methodL(){
        character.methodL(); // simply delegate to the wrapped object.
    }
    public int methodM(){
        character.methodM(); // simply delegate to the wrapped object.
    }
    public int methodN(){
        character.methodN(); // simply delegate to the wrapped object.
    }
    public int methodO(){
        character.methodO(); // simply delegate to the wrapped object.
    }

}

Obviamente, muy feo.

Probablemente no soy el primero en encontrar este problema con el Decorador. ¿Cómo puedo evitar esto?

Aviv Cohn
fuente

Respuestas:

6

Rápido y fácil: AbstractCharacterDelegatorno tiene funciones abstractas, sino virtuales. De forma predeterminada, pasan al carácter envuelto y pueden anularse. Luego heredas y solo anulas las pocas que deseas.

Este tipo de cosas no es particularmente útil a menos que haga un poco de decoración y su interfaz común sea particularmente amplia. Eso no debería ser demasiado común. Y este enfoque podría no ser por carta un decorador, pero utiliza el concepto y los programadores que conocen el patrón lo entenderían bien como decorador.

Telastyn
fuente
Entonces, ¿está sugiriendo simplemente, en Java, hacer que los métodos sean AbstractCharacterDelegator'regulares' (ya que todos los métodos 'regulares' en Java son virtuales de forma predeterminada, lo que significa que pueden anularse heredando subclases)? ¿Y su implementación simplemente delega en el objeto interno, y si quiero cambiar eso, simplemente anularlos?
Aviv Cohn
Si es así, eso significa que de hecho tendré que usar clases abstractas, y no interfaces, ya que las interfaces no permiten la implementación en los métodos. Entonces tendré un código más limpio, pero no 'herencia múltiple' con interfaces. ¿Cual es mas importante?
Aviv Cohn
@Prog Sí, eso es lo que está diciendo. No descarta usar una interfaz. Volviendo al diagrama UML, puede convertirse AbstractCharacteren un interface. El resto sigue siendo el mismo, ConcreteCharactere AbstractCharacterDecoratorimplementa la interfaz en lugar de subclasificar. Todo debería funcionar. Aunque probablemente no necesiten extender nada más, sus decoradores no están sujetos a subclases AbstractCharacterDecoratorya que siempre podrían hacer la delegación a la antigua usanza si alguna vez necesitaran extender una clase diferente por alguna razón.
Doval
@Doval Entonces, básicamente, el mejor enfoque sería tener una interfaz de AbstractCharacter, y todo lo demás clases concretas o abstractas. Esto permitiría un código limpio, como dijo Telastyn, pero para una mayor flexibilidad al usar todas las clases abstractas. ¿Derecha?
Aviv Cohn
1
@Prog Sí, eso lo resume todo.
Doval
3

Si AbstractCharacter tiene 15 métodos, probablemente deba dividirse en clases más pequeñas. Aparte de eso, no hay mucho más que pueda hacer sin el lenguaje que proporciona algún tipo de soporte de sintaxis especial para la delegación. Simplemente deje un comentario donde comience toda la delegación para que el lector sepa que no necesita mirar más abajo. En cuanto a escribir todos los métodos de reenvío, su IDE puede ayudarlo allí.

Aparte de eso, es mi opinión que Decorator funciona mejor con interfaces. Por lo general, desea limitar su abstracción a un número finito (posiblemente 0) de subclases o utilizar una interfaz. La herencia ilimitada tiene todos los inconvenientes de las interfaces (se le podría pasar una implementación incorrecta) pero no obtiene total flexibilidad (solo puede subclasificar una cosa). Peor aún, lo abre al problema de la clase base frágil y es no es tan flexible como la composición (crea SubclassA y SubclassB, pero luego no puede heredar de ambos para crear SubclassAB con características de ambos). Así que me veo obligado a entrar en un árbol de herencia solo para apoyar la decoración como un error.

Doval
fuente
+1 para señalar el problema de cohesión (demasiadas responsabilidades son 15 métodos) y usar interfaces.
Fuhrmanator
A veces no se puede dividir en clases más pequeñas, digamos una clase de sistema de archivos.
Vicente Bolea