Como usar o padrão Decorator para adicionar pouca funcionalidade a objetos grandes?

8

Esta pergunta diz respeito ao uso do padrão Decorator para adicionar pouca funcionalidade a objetos de classes grandes.

Seguindo o padrão clássico do Decorator, considere a seguinte estrutura de classes:

insira a descrição da imagem aqui

Por exemplo, imagine que isso acontece dentro de um jogo. As instâncias de ConcreteCharacterDecoratordestinam-se a adicionar pouca funcionalidade ao ConcreteCharacter'empacotamento'.

Por exemplo, methodA()retorna um intvalor que representa o dano que o personagem causa aos inimigos. O ConcreteCharacterDecoratorsimplesmente adiciona a esse valor. Portanto, ele só precisa adicionar código methodA(). A funcionalidade de methodB()permanece a mesma.

ConcreteCharacterDecorator ficará assim:

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

}

Isso não é problema com pequenas classes contendo dois métodos.

Mas e se AbstractCharacterdefinidos 15 métodos? ConcreteCharacterDecoratorteria que implementar todos eles, mesmo que apenas para adicionar pouca funcionalidade.

Terminarei com uma classe que contém um método que adiciona um pouco de funcionalidade e outros 14 métodos que simplesmente delegam ao objeto interno.

Seria assim:

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, muito feio.

Provavelmente não sou o primeiro a encontrar esse problema com o Decorator. Como posso evitar isso?

Aviv Cohn
fonte

Respostas:

6

Rápido e fácil: AbstractCharacterDelegatornão possui funções abstratas, mas virtuais. Por padrão, eles passam para o caractere agrupado e podem ser substituídos. Então você herda e substitui apenas os poucos que deseja.

Esse tipo de coisa não é particularmente útil, a menos que você faça um pouco de decoração e sua interface comum seja particularmente ampla. Isso não deve ser muito comum. E essa abordagem pode não ser, por letra , decoradora, mas usa o conceito e seria bem entendida como decoradora por programadores que conhecem o padrão.

Telastyn
fonte
Então você está sugerindo simplesmente - em Java - tornar os métodos em AbstractCharacterDelegator'regular' (já que todos os métodos 'regulares' em Java são virtuais por padrão - o que significa que eles podem ser substituídos por subclasses herdadas)? E sua implementação simplesmente delega para o objeto interno - e se eu quiser mudar isso, simplesmente substitua-o?
Aviv Cohn
Nesse caso, isso significa que, de fato, terei que usar classes abstratas, e não interfaces, pois as interfaces não permitem a implementação nos métodos. Portanto, terei um código mais limpo, mas nenhuma 'herança múltipla' com interfaces. Qual deles é mais importante?
Aviv Cohn
@ Pro Sim, é o que ele está dizendo. Não descarta o uso de uma interface. Voltando ao diagrama UML, você pode se transformar AbstractCharacterem um interface. O restante permanece praticamente o mesmo - ConcreteCharactere AbstractCharacterDecoratorimplementa a interface em vez de subclassificar. Tudo deve funcionar. Embora eles provavelmente não precisem estender mais nada, seus decoradores não estão presos à subclasse, AbstractCharacterDecoratorpois eles sempre podem fazer a delegação à moda antiga, se precisarem estender uma classe diferente por algum motivo.
Doval
@Doval Então, basicamente, a melhor abordagem seria ter AbstractCharacter uma interface e todo o resto de classes concretas ou abstratas. Isso permitiria um código limpo, como disse Telastyn, mas, para maior flexibilidade, o uso de todas as classes abstratas. Direita?
Aviv Cohn
1
@ Pro Sim, isso resume tudo.
Doval
3

Se o AbstractCharacter tiver 15 métodos, ele provavelmente precisará ser dividido em classes menores. Além disso, não há muito mais que você possa fazer sem o idioma fornecer algum tipo de suporte especial à sintaxe para delegação. Basta deixar um comentário onde toda a delegação começa para que o leitor saiba que não precisa olhar mais para baixo. Quanto a escrever todos os métodos de encaminhamento, seu IDE pode ajudá-lo.

Além disso, é minha opinião que o Decorator funciona melhor com interfaces. Geralmente, você deseja limitar sua abstração a um número finito (possivelmente 0) de subclasses ou percorrer todo o caminho com uma interface. A herança ilimitada tem todos os inconvenientes das interfaces (você pode receber uma implementação incorreta), mas não obtém flexibilidade total (você só pode subclassificar uma coisa.) Pior ainda, isso abre o problema da classe básica frágil e é não é tão flexível quanto a composição (você cria a SubclassA e a SubclassB, mas não pode herdar de ambas para criar a SubclassAB com recursos de ambas). Por isso, acho que ser forçado a entrar em uma árvore de herança apenas para apoiar a decoração é uma farsa.

Doval
fonte
+1 para indicar o problema de coesão (muitas responsabilidades são 15 métodos) e usar interfaces.
Fuhrmanator 22/03
Às vezes, não pode ser dividido em classes menores, digamos, uma classe de sistema de arquivos.
Vicente Bolea