Padrões de Projeto (Design Patterns): conceitos, classificações e implementações em C#

No desenvolvimento de software, é comum encontrarmos problemas semelhantes em diferentes projetos. Um sistema de comércio eletrônico precisa organizar pagamentos, enquanto um sistema hospitalar deve gerenciar atendimentos e notificações. Embora sejam aplicações distintas, ambas podem apresentar desafios relacionados à criação de objetos, à organização de responsabilidades e à comunicação entre componentes.

Os padrões de projeto, conhecidos como Design Patterns, são soluções gerais e reutilizáveis para problemas recorrentes de projeto de software. Eles descrevem maneiras de organizar classes, objetos e suas interações, favorecendo características como flexibilidade, manutenção e reutilização de código (Gamma et al., 1994).

Diferentemente de algoritmos, que estabelecem procedimentos computacionais para resolver problemas específicos, os padrões de projeto descrevem estruturas de colaboração entre componentes de um sistema. Um padrão não representa necessariamente um código pronto, mas uma orientação que pode ser adaptada às necessidades de diferentes aplicações (Sommerville, 2011).

Imagine o desenvolvimento de um sistema para uma clínica veterinária. Inicialmente, o software permite cadastrar animais e realizar consultas. Com o crescimento da clínica, surgem novas funcionalidades: agendamento de cirurgias, emissão de notificações, cálculo de descontos, geração de relatórios e integração com serviços externos.

Se todas essas funcionalidades forem implementadas diretamente em uma única classe, o sistema poderá se tornar difícil de modificar. Uma alteração no mecanismo de notificação, por exemplo, poderá exigir modificações no código responsável pelo agendamento de consultas. Os padrões de projeto ajudam a enfrentar problemas dessa natureza ao estabelecer formas conhecidas de distribuir responsabilidades e reduzir dependências desnecessárias entre componentes (Gamma et al., 1994; Fowler, 2002).

Neste artigo, estudaremos os fundamentos dos padrões de projeto, sua origem histórica, suas principais classificações e algumas implementações em C#. Os exemplos utilizarão um cenário contínuo: o desenvolvimento de um sistema de atendimento para uma clínica veterinária.

Breve histórico dos padrões de projeto

A origem dos padrões de projeto está relacionada à arquitetura e não diretamente à programação de computadores. Na década de 1970, o arquiteto Christopher Alexander e seus colaboradores desenvolveram estudos sobre padrões recorrentes na organização de espaços físicos. A proposta consistia em identificar problemas frequentes de projeto e documentar soluções que pudessem ser reutilizadas em diferentes contextos (Alexander et al., 1977). Essa abordagem influenciou pesquisadores da engenharia de software, especialmente aqueles interessados no desenvolvimento orientado a objetos.

Em 1987, Kent Beck e Ward Cunningham apresentaram trabalhos sobre a utilização de padrões na construção de interfaces orientadas a objetos. Posteriormente, outros pesquisadores passaram a investigar como documentar soluções recorrentes para problemas de programação.

O marco mais conhecido ocorreu em 1994, com a publicação do livro Design Patterns: Elements of Reusable Object-Oriented Software, escrito por Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides. A obra sistematizou 23 padrões de projeto, organizados em três categorias: criacionais, estruturais e comportamentais (Gamma et al., 1994). Por conta dessa obra, os autores ficaram conhecidos como Gang of Four (GoF), ou Grupo dos Quatro.

Desde então, os padrões de projeto passaram a integrar o conhecimento fundamental da engenharia de software orientada a objetos. Seu estudo também influenciou práticas de refatoração, desenvolvimento de frameworks e construção de arquiteturas extensíveis (Fowler, 2002; Sommerville, 2011).

É importante observar que os padrões não constituem regras obrigatórias. Sua utilização depende das características do problema e dos benefícios esperados para a aplicação.

Características fundamentais dos padrões de projeto

Um padrão de projeto documenta uma solução que pode ser aplicada repetidamente a uma determinada classe de problemas. Gamma et al. (1994) apresentam quatro elementos essenciais para descrever um padrão:

  • Nome: identifica o padrão e facilita a comunicação entre desenvolvedores. Expressões como Factory Method, Observer e Strategy representam soluções reconhecidas pela comunidade.
  • Problema: descreve a situação em que o padrão pode ser utilizado, incluindo as condições que justificam sua aplicação.
  • Solução: apresenta a organização dos componentes, suas responsabilidades e as relações entre eles. Não corresponde necessariamente a uma implementação específica.
  • Consequências: descreve os benefícios, limitações e compromissos envolvidos na adoção do padrão.

Por exemplo, quando um desenvolvedor afirma que utilizará o padrão Observer para implementar notificações, outros profissionais familiarizados com esse padrão conseguem antecipar uma estrutura baseada na comunicação entre um objeto observado e seus observadores.

Essa linguagem compartilhada facilita a discussão de decisões de projeto e contribui para a documentação arquitetural do sistema (Gamma et al., 1994).

Relação com os princípios de orientação a objetos

A aplicação de padrões de projeto está relacionada a conceitos fundamentais da programação orientada a objetos. São eles:

  • O encapsulamento permite organizar dados e comportamentos dentro de componentes com responsabilidades definidas.
  • A abstração possibilita representar comportamentos por contratos, como interfaces e classes abstratas, sem exigir que os consumidores conheçam todos os detalhes de implementação.
  • O polimorfismo permite utilizar diferentes implementações por meio de um mesmo contrato.
  • A composição permite construir comportamentos complexos combinando objetos, sem depender exclusivamente de hierarquias de herança.

Gamma et al. (1994) destacam a importância de programar voltado para interfaces e favorecer a composição de objetos em situações nas quais ela proporciona maior flexibilidade.

Essas características também se relacionam aos princípios SOLID, especialmente ao princípio da responsabilidade única, ao princípio aberto/fechado e ao princípio da inversão de dependência (Martin, 2003). Caso você queira conhecer mais sobre SOLID, clique aqui para ler o artigo.

Entretanto, aplicar um padrão não garante automaticamente que todos esses princípios sejam respeitados. O resultado depende da qualidade da implementação e da adequação da solução ao problema.

Classificação dos padrões de projeto

Os 23 padrões apresentados por Gamma et al. (1994) são classificados de acordo com seu propósito principal.

Padrões criacionais (Creational Patterns)

Os padrões criacionais tratam dos mecanismos de criação de objetos. Seu objetivo é reduzir a dependência entre o código que utiliza um objeto e os detalhes necessários para construí-lo.

Os cinco padrões criacionais clássicos são:

PadrãoFinalidade
SingletonRestringir a criação a uma única instância de uma classe.
Factory MethodDelegar a subclasses a decisão sobre qual objeto concreto criar.
Abstract FactoryCriar famílias de objetos relacionados sem especificar suas classes concretas.
BuilderConstruir objetos complexos por meio de etapas organizadas.
PrototypeCriar novos objetos a partir da cópia de objetos existentes.

Em uma clínica veterinária, padrões criacionais podem ser empregados na criação de diferentes modalidades de atendimento, na configuração de relatórios ou na construção de objetos que representam prontuários.

Padrões estruturais (Structural Patterns)

Os padrões estruturais descrevem maneiras de combinar classes e objetos para construir estruturas maiores. Eles ajudam a organizar relacionamentos e a integrar componentes com interfaces diferentes (Gamma et al., 1994).

PadrãoFinalidade
AdapterAdaptar a interface de um componente para outra interface esperada.
BridgeSeparar uma abstração de sua implementação.
CompositeTratar objetos individuais e composições de objetos de maneira uniforme.
DecoratorAcrescentar responsabilidades dinamicamente a objetos.
FacadeDisponibilizar uma interface simplificada para um subsistema complexo.
FlyweightCompartilhar estados para reduzir o consumo de memória.
ProxyControlar o acesso a outro objeto.

Na clínica veterinária, o Adapter pode integrar um serviço externo de notificações, enquanto o Facade pode simplificar a execução das operações necessárias para registrar um atendimento.

Padrões comportamentais (Behavioral Patterns)

Os padrões comportamentais tratam da distribuição de responsabilidades e da comunicação entre objetos. Seu foco está na organização dos algoritmos, no fluxo de execução e na colaboração entre componentes (Gamma et al., 1994).

PadrãoFinalidade
Chain of ResponsibilityEncaminhar solicitações por uma cadeia de responsáveis.
CommandEncapsular uma solicitação em um objeto.
InterpreterInterpretar expressões de uma linguagem definida.
IteratorPercorrer elementos de uma coleção sem expor sua representação interna.
MediatorCentralizar a comunicação entre componentes.
MementoCapturar e restaurar estados anteriores de um objeto.
ObserverNotificar objetos interessados quando determinado evento ocorre.
StateAlterar comportamentos conforme o estado interno de um objeto.
StrategyEncapsular algoritmos intercambiáveis.
Template MethodDefinir a estrutura de um algoritmo e permitir a especialização de etapas.
VisitorAcrescentar operações a estruturas de objetos sem modificar suas classes.

No sistema veterinário, o Strategy pode representar diferentes políticas de desconto, enquanto o Observer pode comunicar alterações no agendamento de consultas.

Preparando o ambiente de desenvolvimento

Os exemplos serão escritos em C# moderno, utilizando recursos disponíveis no .NET 8 ou superior, como instruções de nível superior (top-level statements), interpolação de strings e interfaces.

Considerando que você já tem o ambiente pronto para compilar códigos C# na sua máquina, vamos começar um projeto. Para criar um projeto do tipo console, execute:

dotnet new console -n ClinicaVeterinaria
cd ClinicaVeterinaria

O comando cria um projeto contendo o arquivo Program.cs, no qual podemos escrever os exemplos. Para executar o programa, utilize:

dotnet run

Cada exemplo apresentado a seguir é independente. Portanto, para experimentá-los, substitua o conteúdo de Program.cs pelo código correspondente.

OBSERVAÇÃO: Os exemplos aqui priorizam a compreensão dos padrões. Em aplicações reais, as classes e interfaces normalmente seriam distribuídas em arquivos separados, conforme as responsabilidades de cada componente.


Factory Method: criação de diferentes atendimentos

O Factory Method é um padrão criacional que define uma interface para criar objetos, permitindo que subclasses decidam qual classe concreta será instanciada. Seu principal objetivo é separar a utilização dos objetos da decisão sobre suas classes concretas (Gamma et al., 1994).

Considere que a clínica veterinária oferece consultas e cirurgias. Uma implementação inicial poderia utilizar condicionais para identificar qual atendimento deve ser realizado.

Antes do Factory Method

using System;

string tipoAtendimento = "cirurgia";

if (tipoAtendimento == "consulta")
{
    Console.WriteLine("Realizando consulta veterinária.");
}
else if (tipoAtendimento == "cirurgia")
{
    Console.WriteLine("Realizando cirurgia veterinária.");
}
else
{
    Console.WriteLine("Tipo de atendimento inválido.");
}

O programa funciona, mas apresenta uma limitação: a escolha do atendimento e a execução de seu comportamento estão concentradas no mesmo trecho de código.

Se novos atendimentos forem adicionados, como vacinação ou exames laboratoriais, será necessário modificar essa estrutura condicional.

Em sistemas maiores, quando essa lógica está distribuída em diferentes módulos, a manutenção pode se tornar trabalhosa.

Aplicando o padrão Factory Method

Podemos separar a criação dos atendimentos de sua utilização.

using System;

// Contrato dos atendimentos
interface IAtendimento
{
    void Executar();
}

// Implementações concretas
class Consulta : IAtendimento
{
    public void Executar()
    {
        Console.WriteLine("Realizando consulta veterinária.");
    }
}

class Cirurgia : IAtendimento
{
    public void Executar()
    {
        Console.WriteLine("Realizando cirurgia veterinária.");
    }
}

// Classe criadora
abstract class CriadorAtendimento
{
    public abstract IAtendimento CriarAtendimento();

    public void RealizarAtendimento()
    {
        IAtendimento atendimento = CriarAtendimento();
        atendimento.Executar();
    }
}

// Criadores concretos
class CriadorConsulta : CriadorAtendimento
{
    public override IAtendimento CriarAtendimento()
    {
        return new Consulta();
    }
}

class CriadorCirurgia : CriadorAtendimento
{
    public override IAtendimento CriarAtendimento()
    {
        return new Cirurgia();
    }
}

// Programa principal
class Program
{
    static void Main()
    {
        CriadorAtendimento criador = new CriadorCirurgia();

        criador.RealizarAtendimento();
    }
}

Saída esperada para este cenário é:

Realizando cirurgia veterinária.

Analisando a implementação

A interface IAtendimento estabelece o contrato que os diferentes atendimentos devem cumprir., enquanto as classes Consulta e Cirurgia implementam esse contrato, fornecendo comportamentos específicos.

A classe abstrata CriadorAtendimento declara o método CriarAtendimento(), responsável por produzir um objeto compatível com a interface. As subclasses CriadorConsulta e CriadorCirurgia implementam esse método e decidem qual objeto concreto será criado. Por fim, o método RealizarAtendimento() utiliza o atendimento produzido sem precisar conhecer sua classe concreta.

Essa organização representa o Factory Method clássico, pois a responsabilidade pela criação é delegada às subclasses (Gamma et al., 1994).

O principal benefício deste padrão é que novos tipos de atendimento podem ser adicionados por meio de novas implementações e criadores, preservando o código que utiliza a abstração. Entretanto, a solução exige mais classes e pode ser desnecessária quando a criação dos objetos é simples e não apresenta variações relevantes. Isso se torna uma desvantagem da estratégia.

Singleton: uma instância compartilhada

O Singleton é um padrão criacional que busca garantir a existência de uma única instância de determinada classe e disponibilizar um ponto de acesso a ela (Gamma et al., 1994).

Considere que o sistema veterinário mantém configurações globais, como o nome da clínica e seu horário de funcionamento.

Antes do Singleton

using System;

class ConfiguracaoClinica
{
    public string Nome { get; set; } = "Clínica Vida Animal";
}

class Program
{
    static void Main()
    {
        var configuracao1 = new ConfiguracaoClinica();
        var configuracao2 = new ConfiguracaoClinica();

        configuracao1.Nome = "Clínica Pet Feliz";

        Console.WriteLine(configuracao1.Nome);
        Console.WriteLine(configuracao2.Nome);
    }
}

Saída esperada:

Clínica Pet Feliz
Clínica Vida Animal

Observe que os objetos possuem estados independentes. Alterar o nome em configuracao1 não modifica o conteúdo de configuracao2. Esse comportamento é adequado para objetos comuns, mas pode ser indesejado quando o requisito determina que uma única instância de configuração deve ser compartilhada.

Aplicando o padrão Singleton

Refatorando o código para aplicarmos o padrão, teríamos:

using System;

sealed class ConfiguracaoClinica
{
    private static readonly ConfiguracaoClinica instancia = new();

    public static ConfiguracaoClinica Instancia => instancia;

    public string Nome { get; set; } = "Clínica Vida Animal";

    private ConfiguracaoClinica()
    {
    }
}

class Program
{
    static void Main()
    {
        var configuracao1 = ConfiguracaoClinica.Instancia;
        var configuracao2 = ConfiguracaoClinica.Instancia;

        configuracao1.Nome = "Clínica Pet Feliz";

        Console.WriteLine(configuracao2.Nome);

        Console.WriteLine(
            ReferenceEquals(configuracao1, configuracao2)
        );
    }
}

Saída esperada:

Clínica Pet Feliz
True

Analisando a implementação

O construtor privado impede que outras classes utilizem new ConfiguracaoClinica(). Enquanto o atributo estático instancia mantém a única instância criada pela classe, a propriedade Instancia permite acessar esse objeto.

A palavra-chave sealed impede que outras classes herdem de ConfiguracaoClinica e a inicialização estática utilizada pelo C# também garante a segurança da criação da instância diante de múltiplas threads. Além disso, o método ReferenceEquals() confirma que as duas variáveis apontam para o mesmo objeto.

A aplicação do padrão permite compartilhar uma instância quando essa restrição faz parte dos requisitos. Contudo, o uso excessivo de Singleton pode introduzir estado global compartilhado, dificultando testes automatizados e aumentando o acoplamento.

Cabe destacar que em aplicações ASP.NET Core, por exemplo, serviços com tempo de vida Singleton geralmente são administrados pelo mecanismo de injeção de dependências. Assim, não é necessário implementar manualmente esse padrão em todas as situações (Microsoft, s.d.).

Strategy: diferentes políticas de desconto

O Strategy é um padrão comportamental que define uma família de algoritmos, encapsula cada um deles e permite substituí-los durante a execução do programa (Gamma et al., 1994).

Imagine que a clínica veterinária oferece descontos conforme o perfil do cliente. Nesse caso, clientes comuns não recebem desconto, clientes cadastrados em um plano de fidelidade recebem 10% e organizações parceiras recebem 20%.

Antes do Strategy

Uma implementação inicial poderia concentrar todas as regras em um único método.

using System;

decimal valorConsulta = 200m;
string tipoCliente = "fidelidade";

decimal valorFinal = valorConsulta;

if (tipoCliente == "fidelidade")
{
    valorFinal = valorConsulta * 0.90m;
}
else if (tipoCliente == "parceiro")
{
    valorFinal = valorConsulta * 0.80m;
}

Console.WriteLine($"Valor final: R$ {valorFinal:F2}");

Saída esperada:

Valor final: R$ 180,00

O valor numérico está correto, pois a pontuação decimal exibida depende da configuração regional do ambiente. Mas, o problema dessa abordagem aparece quando novas políticas são adicionadas. O código responsável pelo cálculo precisa conhecer todas as modalidades de desconto e, consequentemente, novas regras exigem alterações nas condicionais existentes.

Aplicando o padrão Strategy

using System;

// Contrato das estratégias
interface IDesconto
{
    decimal Calcular(decimal valor);
}

// Estratégia para clientes comuns
class SemDesconto : IDesconto
{
    public decimal Calcular(decimal valor)
    {
        return valor;
    }
}

// Estratégia para clientes fidelidade
class DescontoFidelidade : IDesconto
{
    public decimal Calcular(decimal valor)
    {
        return valor * 0.90m;
    }
}

// Estratégia para parceiros
class DescontoParceiro : IDesconto
{
    public decimal Calcular(decimal valor)
    {
        return valor * 0.80m;
    }
}

// Contexto que utiliza a estratégia
class Pagamento
{
    private readonly IDesconto desconto;

    public Pagamento(IDesconto desconto)
    {
        this.desconto = desconto;
    }

    public decimal CalcularTotal(decimal valor)
    {
        return desconto.Calcular(valor);
    }
}

class Program
{
    static void Main()
    {
        decimal valorConsulta = 200m;

        var pagamento = new Pagamento(
            new DescontoFidelidade()
        );

        decimal total = pagamento.CalcularTotal(valorConsulta);

        Console.WriteLine($"Valor final: {total:F2}");
    }
}

Saída esperada:

Valor final: 180,00

Analisando a implementação

  • A interface IDesconto define o contrato que todas as estratégias precisam implementar.
  • As classes SemDesconto, DescontoFidelidade e DescontoParceiro representam diferentes algoritmos de cálculo.
  • A classe Pagamento recebe uma estratégia por meio do construtor.

Esse mecanismo é conhecido como injeção de dependência por construtor. Em vez de decidir internamente qual desconto aplicar, Pagamento utiliza a implementação recebida.

Para alterar a política de desconto, basta fornecer outra estratégia:

var pagamento = new Pagamento(
    new DescontoParceiro()
);

Com isso, a classe Pagamento permanece inalterada.

Essa característica aproxima a implementação do princípio aberto/fechado, segundo o qual componentes devem favorecer a extensão de comportamentos sem exigir modificações frequentes no código existente (Martin, 2003).

O padrão permite acrescentar algoritmos e políticas de negócio sem concentrar todas as variações em condicionais. Em contrapartida, a utilização de várias estratégias pode aumentar a quantidade de classes. Para regras muito pequenas e estáveis, uma solução direta pode ser suficiente.

Observer: notificações sobre agendamentos

O Observer é um padrão comportamental que estabelece uma dependência de um-para-muitos entre objetos. Quando o estado de um objeto observado é alterado, os objetos interessados são notificados (Gamma et al., 1994).

Considere que, ao confirmar uma consulta veterinária, o sistema precisa notificar a recepção e o tutor do animal.

Antes do Observer

using System;

class Agendamento
{
    public void Confirmar(string animal)
    {
        Console.WriteLine($"Consulta de {animal} confirmada.");

        Console.WriteLine("Recepção notificada.");
        Console.WriteLine("Tutor notificado.");
    }
}

class Program
{
    static void Main()
    {
        var agendamento = new Agendamento();

        agendamento.Confirmar("Rex");
    }
}

Saída esperada:

Consulta de Rex confirmada.
Recepção notificada.
Tutor notificado.

Embora funcione, a classe Agendamento acumula responsabilidades, pois ela realiza a confirmação e também determina quais notificações devem ser executadas.

Se a clínica decidir acrescentar notificações para o veterinário, será necessário modificar essa classe.

Aplicando o padrão Observer

using System;
using System.Collections.Generic;

// Contrato dos observadores
interface IObservador
{
    void Atualizar(string animal);
}

// Observador da recepção
class NotificacaoRecepcao : IObservador
{
    public void Atualizar(string animal)
    {
        Console.WriteLine($"Recepção: consulta de {animal} confirmada.");
    }
}

// Observador do tutor
class NotificacaoTutor : IObservador
{
    public void Atualizar(string animal)
    {
        Console.WriteLine($"Tutor: consulta de {animal} confirmada.");
    }
}

// Objeto observado
class Agendamento
{
    private readonly List<IObservador> observadores = new();

    public void AdicionarObservador(IObservador observador)
    {
        observadores.Add(observador);
    }

    public void Confirmar(string animal)
    {
        Console.WriteLine($"Consulta de {animal} confirmada.");

        foreach (var observador in observadores)
        {
            observador.Atualizar(animal);
        }
    }
}

class Program
{
    static void Main()
    {
        var agendamento = new Agendamento();

        agendamento.AdicionarObservador(
            new NotificacaoRecepcao()
        );

        agendamento.AdicionarObservador(
            new NotificacaoTutor()
        );

        agendamento.Confirmar("Rex");
    }
}

Saída esperada:

Consulta de Rex confirmada.
Recepção: consulta de Rex confirmada.
Tutor: consulta de Rex confirmada.

Analisando a implementação

A interface IObservador estabelece o método Atualizar() e cada observador implementa uma ação específica. A classe Agendamento mantém uma coleção de observadores interessados no evento de confirmação. Quando o método Confirmar() é executado, todos os observadores cadastrados recebem a notificação.

Para adicionar uma notificação ao veterinário, podemos criar uma nova classe que implemente IObservador, sem alterar a lógica de confirmação.

O padrão reduz a dependência entre o objeto que produz o evento e os componentes que reagem a ele. Contudo, sistemas com muitos observadores podem apresentar fluxos de execução difíceis de acompanhar. Também é necessário definir como lidar com erros, remoção de observadores e notificações repetidas.

A linguagem C# oferece recursos nativos, como eventos e delegados, que podem ser utilizados para implementar mecanismos semelhantes de notificação (Microsoft, s.d.).

Adapter: integração com um serviço externo

O Adapter é um padrão estrutural que converte a interface de um componente em outra interface esperada pelo sistema. Sua utilização permite integrar classes que não poderiam colaborar diretamente devido a diferenças entre seus contratos (Gamma et al., 1994).

Imagine que a clínica utiliza um serviço externo para enviar mensagens aos tutores. O sistema da clínica trabalha com o método EnviarMensagem(), enquanto a biblioteca externa disponibiliza DispararTexto().

Antes do Adapter

using System;

class ServicoExterno
{
    public void DispararTexto(string numero, string texto)
    {
        Console.WriteLine($"Mensagem enviada para {numero}: {texto}");
    }
}

class Program
{
    static void Main()
    {
        var servico = new ServicoExterno();

        servico.DispararTexto(
            "31999999999",
            "Consulta confirmada para Rex."
        );
    }
}

O código funciona, mas o programa passa a depender diretamente da interface oferecida pelo fornecedor externo. Se a clínica trocar de serviço, os componentes que utilizam DispararTexto() poderão precisar de modificações.

Aplicando o padrão Adapter

using System;

// Interface esperada pela clínica
interface INotificador
{
    void EnviarMensagem(string destino, string mensagem);
}

// Serviço externo
class ServicoExterno
{
    public void DispararTexto(string numero, string texto)
    {
        Console.WriteLine($"Mensagem enviada para {numero}: {texto}");
    }
}

// Adaptador
class AdaptadorNotificacao : INotificador
{
    private readonly ServicoExterno servico;

    public AdaptadorNotificacao(ServicoExterno servico)
    {
        this.servico = servico;
    }

    public void EnviarMensagem(string destino, string mensagem)
    {
        servico.DispararTexto(destino, mensagem);
    }
}

// Componente da clínica
class Agendamento
{
    private readonly INotificador notificador;

    public Agendamento(INotificador notificador)
    {
        this.notificador = notificador;
    }

    public void Confirmar(string animal, string telefone)
    {
        notificador.EnviarMensagem(
            telefone,
            $"Consulta confirmada para {animal}."
        );
    }
}

class Program
{
    static void Main()
    {
        var servicoExterno = new ServicoExterno();

        INotificador adaptador = new AdaptadorNotificacao(
            servicoExterno
        );

        var agendamento = new Agendamento(adaptador);

        agendamento.Confirmar("Rex", "31999999999");
    }
}

Saída esperada:

Mensagem enviada para 31999999999: Consulta confirmada para Rex.

Analisando a implementação

Na refatoração, a interface INotificador representa o contrato utilizado pelo sistema da clínica. A classe ServicoExterno possui uma interface diferente, que não pode ser modificada diretamente. A classe AdaptadorNotificacao implementa o contrato esperado e traduz as chamadas para o formato compreendido pelo serviço externo. Dessa maneira, a classe Agendamento não precisa conhecer o método DispararTexto().

O padrão permite substituir fornecedores ou integrar bibliotecas externas com menor impacto nos componentes que utilizam a abstração, mas em troca introduz uma camada adicional de código, cuja utilização precisa ser justificada pela incompatibilidade entre interfaces.

Facade: simplificando o processo de atendimento

O Facade é um padrão estrutural que fornece uma interface unificada para um conjunto de interfaces existentes em um subsistema. Seu objetivo é simplificar a utilização de operações que envolvem vários componentes (Gamma et al., 1994).

Considere que o registro de um atendimento veterinário exige três operações:

  1. Cadastrar o animal.
  2. Registrar a consulta.
  3. Emitir um comprovante.

Antes do Facade

using System;

class CadastroAnimal
{
    public void Cadastrar(string nome)
    {
        Console.WriteLine($"Animal {nome} cadastrado.");
    }
}

class RegistroConsulta
{
    public void Registrar(string nome)
    {
        Console.WriteLine($"Consulta de {nome} registrada.");
    }
}

class EmissorComprovante
{
    public void Emitir(string nome)
    {
        Console.WriteLine($"Comprovante de {nome} emitido.");
    }
}

class Program
{
    static void Main()
    {
        var cadastro = new CadastroAnimal();
        var consulta = new RegistroConsulta();
        var comprovante = new EmissorComprovante();

        cadastro.Cadastrar("Rex");
        consulta.Registrar("Rex");
        comprovante.Emitir("Rex");
    }
}

O programa executa as operações corretamente. Entretanto, o código cliente precisa conhecer todos os componentes envolvidos no processo e a ordem em que devem ser utilizados.

Aplicando o padrão Facade

using System;

class CadastroAnimal
{
    public void Cadastrar(string nome)
    {
        Console.WriteLine($"Animal {nome} cadastrado.");
    }
}

class RegistroConsulta
{
    public void Registrar(string nome)
    {
        Console.WriteLine($"Consulta de {nome} registrada.");
    }
}

class EmissorComprovante
{
    public void Emitir(string nome)
    {
        Console.WriteLine($"Comprovante de {nome} emitido.");
    }
}

// Fachada do subsistema
class AtendimentoFacade
{
    private readonly CadastroAnimal cadastro = new();
    private readonly RegistroConsulta consulta = new();
    private readonly EmissorComprovante comprovante = new();

    public void RealizarAtendimento(string animal)
    {
        cadastro.Cadastrar(animal);
        consulta.Registrar(animal);
        comprovante.Emitir(animal);
    }
}

class Program
{
    static void Main()
    {
        var atendimento = new AtendimentoFacade();

        atendimento.RealizarAtendimento("Rex");
    }
}

Saída esperada:

Animal Rex cadastrado.
Consulta de Rex registrada.
Comprovante de Rex emitido.

Analisando a implementação

A classe AtendimentoFacade oferece um único método para executar um processo composto por várias operações.

O código cliente não precisa conhecer individualmente as classes CadastroAnimal, RegistroConsulta e EmissorComprovante.

A fachada (facade) coordena a execução, enquanto os componentes internos continuam responsáveis por suas respectivas funcionalidades.

Essa estratégia simplifica a utilização de subsistemas e reduz o conhecimento necessário para executar operações complexas. Mas, se o(a) desenvolvedor(a) criar uma fachada que concentra funcionalidades demais, ela pode se transformar em uma classe excessivamente grande e difícil de manter.

É importante observar que o Facade não substitui necessariamente as interfaces originais. Os componentes internos podem continuar disponíveis para outras partes da aplicação.

Decorator: adicionando serviços ao atendimento

O Decorator é um padrão estrutural que permite acrescentar responsabilidades a objetos dinamicamente, sem modificar suas classes originais. Ele representa uma alternativa flexível à criação de inúmeras subclasses para cada combinação possível de funcionalidades (Gamma et al., 1994).

Imagine que uma consulta veterinária custa R$ 150,00, sendo que a clínica também oferece serviços adicionais, como banho e vacinação. Assim, o objetivo é calcular o preço de diferentes combinações de serviços.

Antes do Decorator

using System;

decimal valorConsulta = 150m;
bool incluirBanho = true;
bool incluirVacina = true;

decimal total = valorConsulta;

if (incluirBanho)
{
    total += 50m;
}

if (incluirVacina)
{
    total += 80m;
}

Console.WriteLine($"Total: {total:F2}");

Saída esperada:

Total: 280,00

A implementação é suficiente para poucos serviços. Porém, quando surgem muitas combinações de funcionalidades, o código pode acumular diversas condicionais.

Aplicando o padrão Decorator

using System;

// Contrato dos serviços
interface IServico
{
    decimal CalcularPreco();
}

// Serviço principal
class Consulta : IServico
{
    public decimal CalcularPreco()
    {
        return 150m;
    }
}

// Decorador para banho
class Banho : IServico
{
    private readonly IServico servico;

    public Banho(IServico servico)
    {
        this.servico = servico;
    }

    public decimal CalcularPreco()
    {
        return servico.CalcularPreco() + 50m;
    }
}

// Decorador para vacinação
class Vacinacao : IServico
{
    private readonly IServico servico;

    public Vacinacao(IServico servico)
    {
        this.servico = servico;
    }

    public decimal CalcularPreco()
    {
        return servico.CalcularPreco() + 80m;
    }
}

class Program
{
    static void Main()
    {
        IServico atendimento = new Consulta();

        atendimento = new Banho(atendimento);
        atendimento = new Vacinacao(atendimento);

        Console.WriteLine(
            $"Total: {atendimento.CalcularPreco():F2}"
        );
    }
}

Saída esperada:

Total: 280,00

Analisando a implementação

Na modificação do código, a interface IServico define o contrato comum para todos os componentes; a classe Consulta representa o serviço principal; e as classes Banho e Vacinacao recebem outro objeto que implementa IServico. Assim, cada decorador executa o comportamento do objeto recebido e acrescenta seu próprio valor.

Observe a construção:

IServico atendimento = new Consulta();

atendimento = new Banho(atendimento);
atendimento = new Vacinacao(atendimento);

Primeiro, criamos uma consulta de R$ 150,00. Em seguida, adicionamos o banho, aumentando o valor para R$ 200,00. Finalmente, acrescentamos a vacinação, resultando em R$ 280,00. Essa composição permite combinar funcionalidades sem modificar a classe Consulta.

O padrão favorece a extensão de comportamentos por composição, evitando a necessidade de criar subclasses para cada combinação. Entretanto, cadeias extensas de decoradores podem dificultar a identificação dos comportamentos aplicados e a depuração do programa.

Comparando os padrões estudados

Embora todos os padrões apresentados possam contribuir para a organização do software, cada um resolve uma categoria específica de problema, conforme a tabela abaixo:

PadrãoCategoriaProblema resolvido na clínica
Factory MethodCriacionalCriar diferentes modalidades de atendimento.
SingletonCriacionalCompartilhar uma única instância de configuração.
StrategyComportamentalAlternar políticas de desconto.
ObserverComportamentalNotificar interessados sobre agendamentos.
AdapterEstruturalIntegrar serviços externos de comunicação.
FacadeEstruturalSimplificar operações do processo de atendimento.
DecoratorEstruturalAcrescentar serviços e calcular valores combinados.

Uma diferença importante é que padrões podem utilizar mecanismos semelhantes, mas possuir finalidades distintas. O Strategy e o Decorator, por exemplo, utilizam composição e interfaces. Entretanto, o Strategy busca substituir um algoritmo por outro, enquanto o Decorator busca acrescentar responsabilidades a um objeto.

Da mesma forma, o Adapter e o Facade podem envolver objetos intermediários. Contudo, o Adapter converte interfaces incompatíveis, enquanto o Facade simplifica a interação com um subsistema.

A identificação correta do problema é, portanto, mais importante do que a simples reprodução da estrutura de classes de determinado padrão (Gamma et al., 1994).

Quando utilizar padrões de projeto?

Uma dúvida comum entre estudantes é saber quando aplicar um padrão. Afinal de contas, a existência de um padrão conhecido não significa que ele deva ser utilizado em qualquer situação.

Considere novamente o sistema veterinário: se existe apenas uma modalidade de atendimento, talvez não seja necessário criar uma hierarquia de Factory Method; se a clínica possui uma única política de desconto, uma função simples pode resolver o problema sem exigir o Strategy.

Por outro lado, quando diferentes políticas começam a surgir e são modificadas frequentemente, separar os algoritmos em estratégias pode reduzir o impacto das mudanças.

Essa abordagem está relacionada ao princípio de evitar complexidade desnecessária, frequentemente associado ao conceito de You Aren’t Gonna Need It (YAGNI).

A decisão deve considerar a complexidade atual, as mudanças efetivamente esperadas, a facilidade de testes e os custos de manutenção (Fowler, 2002; Sommerville, 2011).

Padrões de projeto não são arquiteturas completas

Também é importante distinguir padrões de projeto de padrões arquiteturais. Padrões como Strategy, Observer e Adapter normalmente descrevem a colaboração entre componentes em uma parte específica do sistema. Já padrões arquiteturais, como Model-View-Controller (MVC), tratam da organização de responsabilidades em uma escala mais ampla.

Uma aplicação MVC pode utilizar diferentes padrões GoF internamente. Por exemplo, um serviço da camada de negócio pode utilizar Strategy para calcular descontos e Adapter para integrar um provedor externo.

Portanto, padrões de projeto e padrões arquiteturais não são conceitos concorrentes. Eles podem atuar em diferentes níveis de abstração de um mesmo software (Sommerville, 2011).

Boas práticas na utilização de Design Patterns

A literatura de engenharia de software enfatiza que decisões de projeto devem favorecer a compreensão, a manutenção e a evolução das aplicações (Pressman; Maxim, 2016).

Na prática, algumas recomendações são especialmente relevantes.

  • Identifique o problema antes de escolher o padrão. A seleção deve partir de uma necessidade concreta, e não do desejo de utilizar determinada técnica.
  • Prefira soluções simples quando forem suficientes. Um padrão que acrescenta várias classes pode não oferecer benefícios relevantes para funcionalidades pequenas e estáveis.
  • Utilize interfaces quando houver necessidade de abstração. Interfaces são especialmente úteis quando existem diferentes implementações que precisam ser utilizadas de maneira intercambiável.
  • Evite responsabilidades excessivas. Classes que acumulam regras de negócio, acesso a dados, comunicação externa e apresentação tendem a ser mais difíceis de modificar.
  • Considere os custos da abstração. Cada classe, interface e camada adicional aumenta a quantidade de elementos que o desenvolvedor precisa compreender.
  • Escreva testes automatizados. A aplicação de um padrão deve preservar os comportamentos esperados e permitir que futuras alterações sejam verificadas.

Essas recomendações estão alinhadas aos objetivos de modularidade, baixo acoplamento e facilidade de manutenção discutidos por Sommerville (2011) e Pressman e Maxim (2016).

Conclusão

Os padrões de projeto representam um conjunto de conhecimentos consolidados sobre problemas recorrentes na construção de software orientado a objetos. Sua principal contribuição não está na redução da quantidade de linhas de código, mas na possibilidade de organizar responsabilidades, estabelecer contratos e favorecer a evolução dos sistemas.

Os padrões criacionais auxiliam na construção de objetos, os estruturais organizam suas relações e os comportamentais definem formas de colaboração e distribuição de responsabilidades (Gamma et al., 1994). Entretanto, a utilização de padrões exige discernimento. A introdução indiscriminada de abstrações pode tornar um sistema mais complexo sem produzir benefícios proporcionais.

O domínio de Design Patterns envolve, portanto, duas competências complementares: reconhecer problemas que justificam soluções estruturadas e identificar situações nas quais uma implementação mais simples é suficiente. Para o profissional de desenvolvimento de software, esse conhecimento contribui para a construção de aplicações mais compreensíveis, testáveis e preparadas para mudanças, características fundamentais da engenharia de software (Sommerville, 2011; Pressman; Maxim, 2016).

Obrigado pela leitura e bons estudos!

Referências

ALEXANDER, Christopher et al. A pattern language: towns, buildings, construction. New York: Oxford University Press, 1977.

BECK, Kent; CUNNINGHAM, Ward. Using pattern languages for object-oriented programs. In: CONFERENCE ON OBJECT-ORIENTED PROGRAMMING, SYSTEMS, LANGUAGES, AND APPLICATIONS, 1987. Proceedings […]. [S. l.: s. n.], 1987. Disponível em: https://c2.com/doc/oopsla87.html. Acesso em: 23 out. 2026.

FOWLER, Martin. Patterns of enterprise application architecture. Boston: Addison-Wesley, 2002.

FOWLER, Martin. Refactoring: improving the design of existing code. 2. ed. Boston: Addison-Wesley, 2019.

GAMMA, Erich; HELM, Richard; JOHNSON, Ralph; VLISSIDES, John. Design patterns: elements of reusable object-oriented software. Reading: Addison-Wesley, 1994.

MARTIN, Robert C. Agile software development: principles, patterns, and practices. Upper Saddle River: Prentice Hall, 2003.

MICROSOFT. Dependency injection in .NET. Microsoft Learn, [s. d.]. Disponível em: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection/overview. Acesso em: 23 out. 2026.

MICROSOFT. Events (C# programming guide). Microsoft Learn, [s. d.]. Disponível em: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/events/. Acesso em: 23 out. 2026.

PRESSMAN, Roger S.; MAXIM, Bruce R. Engenharia de software: uma abordagem profissional. 8. ed. Porto Alegre: AMGH, 2016.

SOMMERVILLE, Ian. Engenharia de software. 9. ed. São Paulo: Pearson Prentice Hall, 2011.