O que é um Design Pattern?

Um design pattern, ou padrão de projeto, é uma solução reutilizável para um problema recorrente de design de software. Ele descreve como organizar classes, objetos e responsabilidades para resolver um problema comum de forma clara, flexível e sustentável.

Padrões de projeto não são códigos prontos. Eles são modelos conceituais que você adapta ao seu contexto, à sua linguagem e à sua necessidade.

Por isso, o mesmo padrão pode ganhar implementações diferentes em C#, Java ou qualquer outra linguagem orientada a objetos. O que se mantém é a ideia central, e não a sintaxe.

Na prática, um padrão de projeto ajuda você a tomar melhores decisões de design. Dessa forma, você evita soluções improvisadas que funcionam hoje, mas quebram amanhã.

O que é um design pattern na prática

Todo design pattern tem quatro elementos essenciais: um nome, o problema que ele resolve, a solução proposta e as consequências de aplicá-la. Essa estrutura vem do livro Design Patterns: Elements of Reusable Object-Oriented Software, publicado em 1994.

O nome cria um vocabulário comum entre os desenvolvedores. Assim, quando alguém diz "aqui cabe um Strategy", o time inteiro entende a proposta sem precisar desenhar cada classe.

O problema descreve quando aplicar o padrão. Já a solução mostra os participantes e como eles colaboram, enquanto as consequências deixam claros os custos e os benefícios da escolha.

Esse último ponto costuma passar despercebido. No entanto, todo padrão tem um preço, geralmente pago em mais classes, mais indireção e mais conceitos para o leitor do código.

Por isso, um design pattern nunca é bom ou ruim de forma absoluta. Ele é adequado ou inadequado para um problema específico, em um contexto específico.

Pense nele como uma receita de engenharia. A receita orienta o caminho, mas você ainda decide os ingredientes, as quantidades e o momento de servir, de acordo com o que o projeto realmente pede.

De onde vieram os design patterns

A ideia de padrões nasceu na arquitetura civil, com o arquiteto Christopher Alexander, que catalogou soluções recorrentes para construções e cidades. Mais tarde, a engenharia de software adotou a mesma abordagem.

O marco definitivo veio com Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides, autores conhecidos como Gang of Four, ou GoF. O livro deles catalogou 23 design patterns que até hoje servem de referência.

Se você quiser aprofundar essa trajetória, o artigo a história dos design patterns conta essa evolução em detalhes.

Ainda é relevante estudar design patterns?

Linguagens modernas incorporaram vários padrões ao próprio idioma. Por exemplo, o C# tem eventos, delegates e LINQ, que resolvem problemas que antes exigiam um Observer ou um Iterator escrito à mão.

Ainda assim, entender o problema por trás de cada padrão continua valioso. O vídeo abaixo discute justamente essa pergunta.

Para outros argumentos sobre o tema, vale ler também por que aprender design patterns.

As três famílias de padrões GoF

O catálogo do GoF divide os padrões em três categorias, de acordo com o propósito de cada um. Essa divisão ajuda a localizar o design pattern certo a partir do tipo de problema.

Padrões criacionais

Os padrões criacionais tratam da criação de objetos. Eles escondem a lógica de instanciação e, com isso, tornam o sistema independente de como seus objetos são criados e compostos.

Fazem parte desse grupo Abstract Factory, Builder, Factory Method, Prototype e Singleton. O exemplo de notificações deste artigo pertence a essa família.

Padrões estruturais

Os padrões estruturais tratam da composição de classes e objetos em estruturas maiores. Por exemplo, o Adapter faz duas interfaces incompatíveis conversarem, e o Decorator adiciona comportamento sem alterar a classe original.

Também estão nesse grupo Bridge, Composite, Facade, Flyweight e Proxy.

Padrões comportamentais

Os padrões comportamentais tratam da comunicação e da distribuição de responsabilidades entre objetos. Entre eles estão Strategy, Observer, Command, State, Template Method, Chain of Responsibility e Mediator.

O exemplo de cálculo de frete, mais adiante, usa o Strategy, que é um dos design patterns mais populares desse grupo.

Um exemplo prático de design pattern

Imagine um código simples para enviar notificações.

var notification = new EmailNotification();
notification.Send("Hello!");

Funciona, mas agora surge um novo requisito.

Você precisa suportar SMS, Push e WhatsApp.

Rapidamente, o código começa a crescer.

if (type == "email")
    notification = new EmailNotification();
else if (type == "sms")
    notification = new SmsNotification();

Esse é um problema recorrente: a criação de objetos depende de regras de negócio. Além disso, cada novo canal obriga você a mexer em todos os pontos que repetem esse if.

É exatamente aqui que entra um padrão de projeto, como o Factory Method. Esse design pattern isola a decisão sobre qual classe instanciar.

Aplicando um padrão de criação

Em vez de espalhar if e new pelo código, você encapsula a criação. Primeiro, defina um contrato comum para todos os canais.

public interface INotification
{
    void Send(string message);
}

Em seguida, concentre a decisão em um único lugar.

public static class NotificationFactory
{
    public static INotification Create(string type)
        => type switch
        {
            "email" => new EmailNotification(),
            "sms"   => new SmsNotification(),
            _       => throw new ArgumentException("Invalid type")
        };
}

O uso fica assim.

var notification = NotificationFactory.Create("email");
notification.Send("Hello!");

Aqui, o padrão:

  • centraliza a criação;
  • reduz o acoplamento;
  • facilita a extensão;
  • deixa clara a intenção do design.

Factory Method ou simple factory

Vale um detalhe técnico importante. A versão estática acima costuma receber o nome de simple factory, porque concentra a criação em um único método.

O Factory Method descrito pelo GoF vai além e delega a criação para subclasses, que sobrescrevem um método de fábrica. Dessa forma, cada subclasse decide qual produto concreto instanciar.

public abstract class NotificationSender
{
    // Método de fábrica: cada subclasse escolhe o canal
    protected abstract INotification CreateNotification();

    public void Notify(string message)
    {
        var notification = CreateNotification();
        notification.Send(message);
    }
}

public sealed class EmailSender : NotificationSender
{
    protected override INotification CreateNotification()
        => new EmailNotification();
}

As duas variações resolvem o mesmo problema de fundo. Portanto, escolha a versão do design pattern mais simples que atende ao seu cenário.

Outro exemplo rápido: comportamento variável

Suponha um cálculo de frete que muda conforme a estratégia escolhida.

decimal CalculateShipping(Order order)
{
    // lógica fixa e difícil de mudar
}

Com o Strategy Pattern, você separa o comportamento em uma interface.

public interface IShippingStrategy
{
    decimal Calculate(Order order);
}

Em seguida, cada forma de frete vira uma classe própria.

public class ExpressShipping : IShippingStrategy
{
    public decimal Calculate(Order order) => 50;
}

O uso fica assim.

var strategy = new ExpressShipping();
var price = strategy.Calculate(order);

O padrão resolve um problema comum: variar comportamento sem encher o código de condicionais. Além disso, cada estratégia pode ganhar testes unitários isolados.

Repare também que adicionar um frete econômico não exige alterar nenhuma classe existente. Basta criar uma nova implementação de IShippingStrategy, o que respeita o princípio aberto e fechado.

Um design pattern estrutural: Decorator

Os exemplos anteriores cobriram criação e comportamento. Para fechar as três famílias, vale ver um design pattern estrutural em ação.

Suponha que você queira registrar um log sempre que uma notificação for enviada, sem alterar as classes de e-mail ou SMS. O Decorator resolve isso envolvendo o objeto original com outro que implementa a mesma interface.

public sealed class LoggingNotification(INotification inner, ILogger logger) : INotification
{
    public void Send(string message)
    {
        // Comportamento extra antes de delegar ao objeto original
        logger.LogInformation("Sending notification: {Message}", message);
        inner.Send(message);
    }
}

O uso combina o Decorator com a fábrica que você já viu.

INotification notification = new LoggingNotification(
    NotificationFactory.Create("email"),
    logger);

notification.Send("Hello!");

Com isso, você empilha comportamentos como log, retry ou métricas sem tocar nas classes concretas. Além disso, cada decorador continua pequeno e fácil de testar.

Design pattern, padrão arquitetural e idioma

É comum confundir um design pattern com outros tipos de padrão. Por isso, vale separar os três níveis em que essa ideia aparece.

Os padrões arquiteturais atuam no nível do sistema inteiro. Por exemplo, MVC, arquitetura em camadas, microsserviços e CQRS definem como grandes partes da aplicação se organizam.

O design pattern atua em um nível intermediário, entre algumas classes e objetos. Ele resolve um problema localizado, como criar objetos de forma flexível ou trocar um algoritmo em tempo de execução.

Já os idiomas são padrões específicos de uma linguagem. No C#, por exemplo, o uso de using para liberar recursos é um idioma, e não um padrão do GoF.

Essa distinção ajuda a escolher a ferramenta certa. Afinal, nenhum design pattern resolve sozinho um problema de arquitetura, assim como nenhuma arquitetura dispensa boas decisões no nível das classes.

Quando a linguagem já traz o padrão

Alguns padrões do GoF praticamente viraram recursos do C#. O Observer é o exemplo mais claro, porque a linguagem oferece eventos nativamente.

public sealed class OrderService
{
    // O evento substitui a lista manual de observadores
    public event EventHandler<Order>? OrderPlaced;

    public void Place(Order order)
    {
        // Regras de negócio do pedido
        OrderPlaced?.Invoke(this, order);
    }
}

var service = new OrderService();
service.OrderPlaced += (_, order) => Console.WriteLine($"Order {order.Id} placed");

Nesse caso, você aplica o design pattern sem escrever as interfaces ISubject e IObserver do livro original. Ainda assim, conhecer o padrão ajuda a entender por que o evento existe e quando ele faz sentido.

O mesmo vale para o Iterator, que o C# entrega com IEnumerable<T> e yield return. Portanto, estudar padrões também é uma forma de entender melhor a própria linguagem.

Benefícios de um design pattern para o time

O ganho mais subestimado de um design pattern é a comunicação. Quando o time compartilha o mesmo vocabulário, revisões de código e discussões técnicas ficam mais curtas e objetivas.

Além disso, padrões conhecidos reduzem a curva de aprendizado de quem chega ao projeto. Um desenvolvedor novo reconhece um Strategy ou um Decorator e entende rapidamente onde mexer.

Os padrões também facilitam testes. Como a maioria deles depende de interfaces, você substitui implementações reais por dublês de teste com pouco esforço.

Por fim, eles tornam as mudanças mais previsíveis. Um novo canal de notificação ou um novo tipo de frete vira uma classe nova, e não uma alteração arriscada em código que já funciona.

Design patterns e injeção de dependência no .NET

No .NET moderno, o container de injeção de dependência assume parte do trabalho que antes ficava com fábricas manuais. Por exemplo, os keyed services permitem registrar várias implementações da mesma interface, cada uma com uma chave.

builder.Services.AddKeyedSingleton<IShippingStrategy, ExpressShipping>("express");
builder.Services.AddKeyedSingleton<IShippingStrategy, StandardShipping>("standard");

public class CheckoutService(
    [FromKeyedServices("express")] IShippingStrategy shipping)
{
    public decimal GetShippingPrice(Order order) => shipping.Calculate(order);
}

Repare que o Strategy continua presente, e o container apenas assume o papel de fábrica. A documentação oficial de injeção de dependência do .NET detalha esse recurso, disponível a partir do .NET 8.

Para entender melhor a base desse mecanismo, o artigo sobre dependency injection explica o conceito do zero.

O ponto-chave dos padrões de projeto

Todo design pattern nasce para resolver problemas que se repetem, como:

  • criação de objetos;
  • variação de comportamento;
  • comunicação entre componentes;
  • composição de estruturas complexas.

Eles ajudam você a escrever código que:

  • expressa melhor a intenção;
  • é mais fácil de manter;
  • está preparado para mudanças futuras.

Mais importante do que decorar padrões é entender quando o problema existe. Só então faz sentido aplicar o padrão certo, no momento certo.

Quando não usar um design pattern

Aplicar um design pattern sem um problema real é um erro comum entre quem acabou de estudar o assunto. Nesse caso, o código ganha camadas, interfaces e classes que ninguém pediu.

Se você tem apenas um tipo de notificação e nenhuma previsão de novos canais, o new EmailNotification() direto basta. Afinal, a simplicidade também é uma decisão de design.

Outro sinal de alerta aparece quando o time precisa de um diagrama para entender uma funcionalidade trivial. Nesse caso, provavelmente o design pattern adicionou mais complexidade do que removeu.

Uma boa regra é esperar o problema aparecer pelo menos duas ou três vezes. Então, refatore em direção ao padrão, já com exemplos concretos na mão.

Os princípios SOLID também ajudam nessa avaliação. Muitas vezes, respeitar a responsabilidade única e a inversão de dependência já leva o código naturalmente para um padrão conhecido.

Como estudar design patterns

Comece pelos padrões que você já usa sem perceber, como Iterator, Observer e Strategy. Em seguida, procure exemplos no seu próprio projeto antes de partir para exemplos de livro.

Além disso, estude cada padrão a partir do problema, e não da solução. Pergunte sempre o que daria errado sem ele e qual custo ele adiciona.

Também ajuda muito ler código de bibliotecas conhecidas. O próprio ASP.NET Core usa Builder na configuração da aplicação, Chain of Responsibility no pipeline de middlewares e Options para configurações tipadas.

Por fim, implemente o mesmo padrão em mais de um contexto. Essa prática mostra que o design pattern é uma ideia flexível, e não um molde rígido de classes.

Conclusão

Um design pattern é uma solução reutilizável para um problema recorrente de design, descrita de forma conceitual e adaptável a qualquer linguagem orientada a objetos. Ele não entrega código pronto, mas entrega um caminho já testado por muitos desenvolvedores.

Neste artigo, você viu a origem dos padrões, as três famílias do GoF e dois exemplos práticos em C#, com Factory Method e Strategy. Também viu como o .NET moderno incorpora parte dessas ideias no container de injeção de dependência.

Portanto, use os padrões como vocabulário e como ferramenta, nunca como meta. O objetivo continua sendo um código claro, fácil de manter e pronto para mudar.

FAQ

Design pattern e padrão de projeto são a mesma coisa?

Sim. Padrão de projeto é a tradução em português do termo design pattern.

Quantos padrões existem no catálogo do GoF?

O livro do GoF catalogou 23 padrões. Eles se dividem em criacionais, estruturais e comportamentais.

Design pattern é código pronto?

Não. Ele é um modelo conceitual que você adapta à linguagem, ao contexto e ao problema.

Qual a diferença entre Factory Method e simple factory?

A simple factory centraliza a criação em um único método. O Factory Method delega essa decisão a subclasses que sobrescrevem o método de fábrica.

Preciso usar padrões em todo projeto?

Não. Aplique um padrão apenas quando o problema que ele resolve realmente existir no seu código.