O Decorator é um padrão estrutural da programação orientada a objetos, descrito originalmente no livro “Design Patterns” (1994), de Erich Gamma e outros membros da Gang of Four (GoF). Ele permite adicionar funcionalidades a objetos de forma dinâmica, sem alterar a estrutura original das classes. Por exemplo, podemos adicionar funcionalidades como registro de logs ou envio de notificações extras a um objeto já existente, sem modificar seu código base. A principal vantagem do padrão Decorator é sua flexibilidade ao estender o comportamento de objetos em tempo de execução, em contraste com a herança, que pode tornar o código rígido e difícil de manter. Com o Decorator, é possível adicionar funcionalidades de forma incremental, enquanto a herança exige uma estrutura fixa que nem sempre é fácil de adaptar.
Descrição Geral
O padrão Decorator permite adicionar funcionalidades a um objeto de forma flexível e dinâmica, sem modificar a classe base. Ele envolve o uso de uma classe decoradora que “encapsula” o objeto original e adiciona novos comportamentos. Isso permite que comportamentos adicionais sejam combinados ou empilhados, criando variações de funcionalidade sem precisar alterar o código original ou criar subclasses específicas. Por exemplo, um componente gráfico pode ser decorado para adicionar funcionalidades visuais como bordas, sombra ou rolagem, permitindo criar novas combinações de aparência de forma incremental.
O Decorator resolve o problema de evitar a explosão de subclasses em cenários onde várias combinações de comportamentos diferentes são necessárias. ‘Explosão de subclasses’ refere-se à criação de muitas subclasses para cobrir todas as possíveis combinações de funcionalidades, o que torna o código difícil de manter e compreender. Em vez de criar uma nova subclasse para cada combinação de comportamento, ele permite que objetos sejam decorados (ou seja, envoltos) com funcionalidades adicionais em tempo de execução.
Origem e Desenvolvimento
O padrão Decorator foi formalizado pela Gang of Four no contexto da programação orientada a objetos, para atender à necessidade de adicionar funcionalidades de forma flexível, sem as limitações da herança. Embora a composição de objetos para reutilização de código não fosse um conceito novo, o padrão Decorator consolidou essa prática no design de software, ajudando a evitar problemas comuns como a herança excessiva e a dependência de subclasses, promovendo maior modularidade e reutilização de código.
Componentes Principais
O padrão Decorator é composto por alguns elementos centrais:
- Componente: Interface ou classe abstrata que define a funcionalidade que será decorada ou estendida. Por exemplo, em um sistema de notificações, o componente pode ser uma interface
INotification
que define um métodosend()
para enviar notificações. - Componente Concreto: Implementa a interface do Componente e representa o objeto original que será decorado. No contexto de notificações, um
SimpleNotification
que apenas envia uma mensagem básica seria um exemplo de componente concreto. - Decorator Abstrato: Classe abstrata ou interface que também implementa a interface do Componente, mas contém uma referência ao objeto que será decorado. Ela delega chamadas ao objeto original, podendo adicionar ou modificar comportamentos. Um exemplo seria um
NotificationDecorator
que adiciona funcionalidades extras, como o envio de notificações por diferentes meios. - Decorators Concretos: São subclasses do Decorator Abstrato que implementam novos comportamentos adicionais ao componente original. Cada um adiciona uma funcionalidade específica. Por exemplo, um
EmailNotification
pode adicionar a funcionalidade de enviar a mensagem por e-mail, enquanto umSMSNotification
pode enviar a mensagem por SMS.
Metodologia e Abordagem
O Decorator segue a composição em vez de herança. O comportamento de um objeto pode ser modificado ao ser “decorado” com um ou mais objetos decoradores, onde cada decorador adiciona uma funcionalidade específica ao objeto envolvido. Ao usar decoradores de forma empilhada, o comportamento pode ser ajustado dinamicamente em tempo de execução, sem a necessidade de modificar o código original.
A filosofia por trás do padrão é aderir ao princípio de aberto/fechado (Open/Closed Principle) da programação orientada a objetos, que afirma que as classes devem ser abertas para extensão, mas fechadas para modificação.
Aplicabilidade e Casos de Uso
O padrão Decorator é útil nos seguintes cenários:
- Quando há a necessidade de adicionar funcionalidades a objetos de forma dinâmica, sem afetar outros objetos da mesma classe.
- Quando a criação de subclasses para representar cada combinação de comportamento adicional seria impraticável ou complexa.
Exemplos:
- Sistema de Notificações: Um objeto de notificação pode ter funcionalidades adicionais decoradas, como enviar notificações por e-mail, SMS ou logar a mensagem em um arquivo.
- Interface Gráfica (GUI): Um objeto gráfico pode ser decorado para adicionar funcionalidades como bordas, rolagem ou sombreamento, sem modificar a classe de base.
- Streams de I/O: Na linguagem Java, a implementação de streams de entrada e saída utiliza o padrão Decorator, permitindo que fluxos de dados sejam decorados com funcionalidades como buffering, compressão, ou criptografia.
Exemplos de Código
Example in C\
using System;
// Base component
public interface INotification
{
void Send(string message);
}
// Concrete component
public class SimpleNotification : INotification
{
public void Send(string message)
{
Console.WriteLine($"Notification: {message}");
}
}
// Abstract decorator
public abstract class NotificationDecorator : INotification
{
protected INotification _notification;
public NotificationDecorator(INotification notification)
{
_notification = notification;
}
public virtual void Send(string message)
{
_notification.Send(message);
}
}
// Concrete decorator - Send by Email
public class EmailNotification : NotificationDecorator
{
public EmailNotification(INotification notification) : base(notification) { }
public override void Send(string message)
{
base.Send(message);
Console.WriteLine($"Sending email: {message}");
}
}
// Concrete decorator - Send by SMS
public class SMSNotification : NotificationDecorator
{
public SMSNotification(INotification notification) : base(notification) { }
public override void Send(string message)
{
base.Send(message);
Console.WriteLine($"Sending SMS: {message}");
}
}
// Using the Decorator
public class Program
{
public static void Main(string[] args)
{
INotification notification = new SimpleNotification();
notification = new EmailNotification(notification);
notification = new SMSNotification(notification);
notification.Send("This is an example of the Decorator pattern.");
}
}
Python
from abc import ABC, abstractmethod
# Base component
class Notification(ABC):
@abstractmethod
def send(self, message: str) -> None:
pass
# Concrete component
class SimpleNotification(Notification):
def send(self, message: str) -> None:
print(f"Notification: {message}")
# Abstract decorator
class NotificationDecorator(Notification):
def __init__(self, notification: Notification) -> None:
self._notification = notification
def send(self, message: str) -> None:
self._notification.send(message)
# Concrete decorator - Send by Email
class EmailNotification(NotificationDecorator):
def send(self, message: str) -> None:
super().send(message)
print(f"Sending email: {message}")
# Concrete decorator - Send by SMS
class SMSNotification(NotificationDecorator):
def send(self, message: str) -> None:
super().send(message)
print(f"Sending SMS: {message}")
# Using the Decorator
if __name__ == "__main__":
notification = SimpleNotification()
notification = EmailNotification(notification)
notification = SMSNotification(notification)
notification.send("This is an example of the Decorator pattern.")
Benefícios e Vantagens
- Extensibilidade Dinâmica: Permite a adição de funcionalidades em tempo de execução, sem necessidade de modificar o código base.
- Evita Herança Excessiva: Reduz a necessidade de criar múltiplas subclasses para representar variações de comportamento.
- Composição Flexível: Permite empilhar decoradores, criando diferentes combinações de comportamentos.
Limitações e Considerações
- Complexidade Aumentada: O uso excessivo de decoradores pode tornar o código mais difícil de entender e depurar, devido ao grande número de classes envolvidas.
- Ordem dos Decoradores: A funcionalidade final depende da ordem em que os decoradores são aplicados, o que pode adicionar complexidade ao design do sistema.
Comparação com Outros Padrões
O Decorator é frequentemente comparado ao padrão Adapter, mas enquanto o Adapter foca em adaptar a interface de um objeto para ser compatível com outra, o Decorator adiciona funcionalidades ao objeto sem alterar sua interface. Também pode ser comparado ao padrão Proxy, mas o Proxy geralmente controla o acesso ao objeto, enquanto o Decorator adiciona funcionalidades.
Implementação e Adaptação
Para implementar o Decorator, começa-se definindo a interface ou classe abstrata que será decorada. Em seguida, cria-se o decorador abstrato que implementa a mesma interface e inclui uma referência ao objeto original. A partir daí, novos decoradores concretos podem ser implementados, cada um adicionando funcionalidades específicas.
Passos Iniciais:
- Defina a interface base (componente).
- Crie classes concretas que implementem a interface.
- Crie o Decorator Abstrato que “envolve” o objeto concreto.
- Crie decoradores concretos que estendam o Decorator Abstrato, implementando funcionalidades adicionais.