C# -  Padrões Estruturais Gof - Decorator


 Neste artigo vou apresentar o padrão estrutural Gof Decorator.

O padrão de projeto Decorator atribui responsabilidades adicionais a um objeto de forma dinâmica sem afetar o comportamento de outros objetos da mesma classe.

Este padrão fornece uma alternativa flexível à herança e permite estender (decorar) de forma dinâmica as características (propriedades e comportamentos) de uma classe qualquer.

O Decorator surgiu da necessidade de adicionar um comportamento, funcionalidade ou estado extra a um objeto em tempo de execução,  quando o uso da Herança não for concebível, por ser um caso que geraria um número muito alto de classes derivadas.

Ele é mais eficiente do que criar várias subclasses, porque o comportamento de um objeto pode ser estendido sem você ter que definir um objeto inteiramente novo.

Dessa forma uma característica do padrão Decorator e favorecer a composição ou agregação sobre a herança.

Os problemas da herança.

A primeira opção que nos vem a mente quando precisamos alterar um comportamento de um objeto é usar herança e estender a classe. Na herança, o próprio objeto herda o comportamento da sua superclasse e um dos benefícios da herança é que ela captura o que é comum e o isola daquilo que é diferente.

Acontece que a herança apresenta os seguintes problemas:

- A Herança é estática – Não podemos alterar o comportamento de um objeto existente durante o tempo de execução só podemos substituir todo o objeto por outro que foi criado de uma subclasse diferente.

- Viola o encapsulamento, visto que a mudança em uma superclasse tem o potencial de afetar todas as subclasses;

- Causa um Forte Acoplamento entre as classes;

- As subclasses somente podem ter uma classe Pai (herança simples).  Lembrando que a linguagem C# que não suporta a herança múltipla;

Na figura temos a representação UML da herança :



Na composição ou agregação um objeto tem uma referência com outro objeto e delega a ele alguma funcionalidade.

A grande vantagem da Composição/Agregação é que o comportamento pode ser escolhido em tempo de execução em vez de estar amarrado em tempo de compilação.

A composição apresenta uma menor dependência de implementações e assim cada classe esta focada em apenas uma tarefa,
respeitando assim o principio da responsabilidade única.(SRP)

A seguir temos a representação UML para agregação e para composição :

a- Agregação


b-) Composição



Dessa forma na composição/agregação um objeto pode usar o comportamento de várias classes, ter referências a múltiplos objetos, e delegar qualquer tipo de trabalho a eles. e pode  fazer isso em tempo de execução.

Exemplo de aplicação do padrão Decorator

Quando você acessa o site de um fabricante de automóveis para montar um veículo, você geralmente tem a opção de ir acrescentando acessórios a um veículo base ou padrão :



E você tem disponíveis diversos acessórios que serão usados conforme a necessidade de cada cliente.

E cada cliente pode incluir os acessórios que achar conveniente ao veiculo base. 

Assim um cliente pode montar um carro com câmbio manual outro com câmbio CVT; um cliente pode montar um carro com a cor prata outro com a cor branca; um cliente pode incluir ar condicionado digital outro ar condicionado manual :

Aqui o objeto veiculo pode receber diversos novos comportamentos/propriedades estendendo sua funcionalidade base.

Podemos replicar este cenário para diversos tipos de objetos como bebidas, pizzas, vestuário, computadores,imóveis, etc.

Nestes cenários temos um exemplo onde o padrão Decorator pode ser usado para incluir novas propriedades e comportamentos a um objeto existente estendendo suas funcionalidades.

Diagrama UML

O diagrama UML do padrão Decorator segundo o Gof apresenta os seguintes participantes


1- Component - Define a interface para objetos que podem ter responsabilidades adicionadas a eles dinamicamente;

2- ConcreteComponent - Implementa Component e sua instância pode ser decorada pela inclusão de comportamento;

3- Decorator - Mantém uma referência para Component e define uma interface compatível com Component;

4- ConcreteDecorator - Estende Decorator e pode incluir ou sobrescrever uma funcionalidade;

Quando podemos usar o padrão Decorator

Podemos usar o padrão Decorator :

- Quando houver necessidade de anexar ou remover o comportamento de apenas algumas instâncias de uma classe, em vez de todas as instâncias da classe.

- Quando a estensão através de herança é impraticável (explosão de classes);

- Quando temos uma classe que não pode ser herdada por estarmos herdando de uma outra classe qualquer

- Queremos adicionar responsabilidades a objetos individuais de forma dinâmica e transparente, sem afetar outros objetos;

- Quando não podemos usar herança (classe sealed)

Vantagens do padrão Decorator

Como vantagens do padrão Decorator temos que :

- É mais flexível que a herança pois adiciona responsabilidade em tempo de execução e não em tempo de compilação

- Podemos ter qualquer número de decoradores e em qualquer ordem

- Estende a funcionalidade do objeto sem afetar outros objetos

De forma geral o padrão do decorador suporta o princípio de que as classes devem ser abertas para extensão, mas fechadas para modificação.

Desvantagem

A principal desvantagem do padrão decorador é a manutenção do código, porque esse padrão pode criar muitos decoradores semelhantes que às vezes são difíceis de manter e distinguir.

Assim podemos ter um aumento na complexidade do código e um grande número de objetos criados.

Aplicando o padrão Decorator

Veremos agora um exemplo prático de aplicação do padrão Composite.

Neste exemplo vamos decorar um objeto pizza com opcionais como : Massa Especial,  Bacon e  Borda Recheada.

Inicialmente teremos o objeto padrão Pizza que tem um preço e a definição de opcionais :



A seguir iremos decorar este objeto incluindo o opcional Massa Especial e calculando o novo preço; Depois vamos decorar este objeto incluindo o opcional Bacon e recalculando o preço e a seguir vamos decorar este objeto incluindo o opcional Borda Recheada e recalculado o preço:



Aqui vamos aplicar o padrão Decorator onde estamos incluindo novas funcionalidades a um objeto existente usando a composição.  Note que podemos visualizar os objetos decorators como invólucros ou wrappers.

Para realizar a implementação vamos criar uma aplicação Console no ambiente do .NET 5.0 usando o VS 2019.  A seguir temos o diagrama de classes que foi gerado pela implementação feita no VS 2019:



1- A interface IPizza :  Representa o Component que define a interface que podemos adicionar a outros objetos;

2- A classe Pizza :  É o ConcreteComponent e Implementa o Component e define o preço e o valor padrão do opcional
e sua instância pode ser decorada pela inclusão de opcionais que são os novos comportamentos;

3- A classe abstrata PizzaDecorator :  Representa o Decorator e mantém uma referência a Component definindo um variável do tipo IPizza e injetando uma instância do tipo IPizza no construtor da classe e a seguir sobrescreve os métodos Preco() e Opcionais() usando a instância de IPizza;

4- As classes BaconDecorator, BordaRecheadaDecorator e MassaEspecialDecorator representam o ConcreteDecorator e estendem PizzaDecorator sobrescrevendo as funcionalidades que no nosso exemplo são o Preco e Opcionais;

A seguir temos o código usado na implementação:

1- Interface IPizza (Component)

public interface IPizza
{
    string Opcionais();
    decimal Preco();
}

2- Classe Pizza (ConcreteComponent)

public class Pizza : IPizza
{
     public string Nome { get; set; }
   
     public Pizza(string nome)
     {
                Nome = nome;
     }
 
     public string Opcionais()
     {
           var opcional = $"Pizza de {Nome} ";
            return opcional;
    }

     public decimal Preco()
     {
          var preco = 10.00M;
         return preco;
     }
 }

3- Classe PizzaDecorator (Decorator)

  public abstract class PizzaDecorator : IPizza
    {
        protected readonly IPizza _pizza;

        public PizzaDecorator(IPizza pizza)
        {
            _pizza = pizza;
        }
        public virtual string Opcionais()
        {
            var opcional = _pizza.Opcionais();
            return opcional;
        }
        public virtual decimal Preco()
        {
            var preco = _pizza.Preco();
            return preco;
        }
    }

5- MassaEspecialDecorator

 public class MassaEspecialDecorator : PizzaDecorator
    {
        public MassaEspecialDecorator(IPizza pizza) : base(pizza)
        {}

        public override string Opcionais()
        {
            var opcional = base.Opcionais();
            opcional += "\r\n com massa especial";
            return opcional;
        }

        public override decimal Preco()
        {
            var preco = base.Preco();
            preco += 2.50M;
            return preco;
        }
    }

6- BaconDecorator

    public class BaconDecorator : PizzaDecorator
    {
        public BaconDecorator(IPizza pizza) : base(pizza)
        { }

        public override string Opcionais()
        {
            var opcional = base.Opcionais();
            opcional += "\r\n com porção extra de bacon";
            return opcional;
        }
        public override decimal Preco()
        {
            var preco = base.Preco();
            preco += 4.00M;
            return preco;
        }
    }

7- BordaRecheadaDecorator

public class BordaRecheadaDecorator : PizzaDecorator
{
        public BordaRecheadaDecorator(IPizza pizza) : base(pizza)
        { }

        public override string Opcionais()
        {
            var opcional = base.Opcionais();
            opcional += "\r\n com borda recheada";
            return opcional;
        }

        public override decimal Preco()
        {
            var preco = base.Preco();
            preco += 3.00M;
            return preco;
        }
    }

8- Program

Na classe Program inicialmente criamos uma instancia de Pizza  que será o objeto que desejamos decorar.  A seguir vamos aplicar o padrão Decorator  criando uma nova instancia de Pizza  e incluindo os opcionais: - massa especial,  bacon e borda recheada no objeto Pizza e recalculando o preço total :

        static void Main(string[] args)
        {
            IPizza pizzaMussarela = new Pizza("Mussarela");

            Console.WriteLine(pizzaMussarela.Opcionais());
            Console.WriteLine($"Preço R$ {pizzaMussarela.Preco()}\n");

            Console.WriteLine("Tecle algo para aplicar o padrão Decorator");
            Console.ReadKey();
            Console.WriteLine("------ Aplicando o Decorator ------------------");

            IPizza massaEspecial = new MassaEspecialDecorator(pizzaMussarela);
            IPizza baconDecorator = new BaconDecorator(massaEspecial);
            IPizza bordaDecorator = new BordaRecheadaDecorator(baconDecorator);

            //exibe o preco e o tipo
         
  Console.WriteLine(bordaDecorator.Opcionais());
            Console.WriteLine($"Preço Total R$ : {bordaDecorator.Preco()}\n");

            Console.ReadKey();
        }

A execução do projeto irá apresentar o seguinte resultado:

Observe que o padrão Decorador é parecido  com o padrão Composite pois ambos usam o princípio de recursividade.

Assim, o Decorator pode ser visto como uma versão simplificada do padrão Composite,  porém a diferença é que o Decorator  apenas adiciona responsabilidades e não é usado para agregar objetos.

Pegue o código do projeto aqui :    Decorator_Exemplo.zip

"Quanto ao mais, irmãos, tudo o que é verdadeiro, tudo o que é honesto, tudo o que é justo, tudo o que é puro, tudo o que é amável, tudo o que é de boa fama, se há alguma virtude, e se há algum louvor, nisso pensai."
Filipenses 4:8

Referências:


José Carlos Macoratti