DEV Community

Joao Gabriel
Joao Gabriel

Posted on

# Design Patterns: como padrões de projeto ajudam a criar códigos mais organizados e flexíveis

Durante o desenvolvimento de software, é muito comum encontrarmos problemas que parecem diferentes, mas que na verdade possuem soluções bastante parecidas.

Com o tempo, desenvolvedores perceberam que algumas dessas soluções poderiam ser organizadas e documentadas para serem reutilizadas em diferentes projetos. Foi assim que surgiu o conceito de Design Patterns, ou Padrões de Projeto.

Neste artigo, vou apresentar o conceito de Design Patterns, suas principais categorias e alguns exemplos utilizando Java, mostrando como esses padrões podem ajudar no desenvolvimento de sistemas mais organizados, reutilizáveis e fáceis de manter.

O que são Design Patterns?

Design Patterns são soluções reutilizáveis para problemas recorrentes no desenvolvimento de software.

Isso não significa que um Design Pattern seja simplesmente um código pronto para copiar e colar. Na verdade, ele representa uma forma de estruturar as classes e os objetos para resolver determinado tipo de problema.

Uma maneira simples de entender é pensar em uma receita.

Uma receita não é a comida pronta. Ela apresenta uma sequência de ideias e passos que podem ser utilizados para preparar determinado prato.

Da mesma forma, um Design Pattern não é um programa completo. Ele apresenta uma solução que pode ser adaptada às necessidades de um projeto.

Design Pattern não é uma biblioteca ou framework. É uma estratégia de organização do código.

Por que utilizar Design Patterns?

Um dos principais benefícios dos padrões de projeto é ajudar o desenvolvedor a evitar soluções improvisadas para problemas que já são conhecidos.

Entre os principais benefícios estão:

  • Reutilização de soluções
  • Maior organização do código
  • Facilidade de manutenção
  • Maior flexibilidade
  • Redução do acoplamento entre classes
  • Facilidade para trabalhar em equipe
  • Comunicação mais clara entre desenvolvedores

Por exemplo, quando um desenvolvedor fala que determinada parte do sistema utiliza o padrão Observer, outro desenvolvedor que conhece esse padrão já consegue ter uma ideia de como aquela estrutura funciona.

Isso facilita bastante a comunicação dentro de uma equipe.

Categorias dos Design Patterns

Os Design Patterns mais conhecidos podem ser divididos em três grandes categorias:

1. Criacionais

Os padrões criacionais estão relacionados à criação de objetos.

Alguns exemplos são:

  • Singleton
  • Factory Method
  • Abstract Factory
  • Builder
  • Prototype

Eles são úteis principalmente quando a criação de objetos precisa seguir determinadas regras ou quando queremos evitar que o código fique fortemente dependente de classes específicas.

2. Estruturais

Os padrões estruturais estão relacionados à organização e composição das classes e objetos.

Entre eles podemos citar:

  • Adapter
  • Decorator
  • Facade
  • Composite
  • Proxy

Eles ajudam a organizar relacionamentos entre diferentes partes do sistema.

3. Comportamentais

Os padrões comportamentais estão relacionados à comunicação e distribuição de responsabilidades entre objetos.

Alguns exemplos são:

  • Observer
  • Strategy
  • Command
  • State
  • Iterator

Eles são especialmente úteis quando existem diferentes comportamentos que precisam ser organizados de maneira flexível.

Exemplo 1: Singleton

O Singleton é um padrão criacional utilizado quando precisamos garantir que determinada classe tenha apenas uma instância durante a execução da aplicação.

Um exemplo clássico é uma classe responsável por armazenar configurações de uma aplicação.

Um exemplo simplificado seria:

public class Configuracao {

    private static Configuracao instancia;

    private Configuracao() {
    }

    public static Configuracao getInstancia() {
        if (instancia == null) {
            instancia = new Configuracao();
        }

        return instancia;
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora podemos obter a instância da classe dessa forma:

Configuracao config1 = Configuracao.getInstancia();
Configuracao config2 = Configuracao.getInstancia();

System.out.println(config1 == config2);
Enter fullscreen mode Exit fullscreen mode

O resultado será:

true
Enter fullscreen mode Exit fullscreen mode

Isso acontece porque config1 e config2 apontam para a mesma instância.

Quando utilizar?

O Singleton pode ser interessante quando realmente precisamos controlar a quantidade de instâncias de determinado recurso.

Porém, é importante não utilizar o padrão simplesmente porque ele parece conveniente. Em algumas situações, o Singleton pode aumentar o acoplamento do sistema e dificultar testes.

Exemplo 2: Factory Method

Outro padrão bastante conhecido é o Factory Method.

A ideia principal é separar a criação de um objeto da utilização desse objeto.

Imagine um sistema que precisa trabalhar com diferentes tipos de notificações.

Podemos criar uma interface:

public interface Notificacao {

    void enviar(String mensagem);
}
Enter fullscreen mode Exit fullscreen mode

Depois podemos criar diferentes implementações:

public class EmailNotificacao implements Notificacao {

    @Override
    public void enviar(String mensagem) {
        System.out.println("Enviando e-mail: " + mensagem);
    }
}
Enter fullscreen mode Exit fullscreen mode

E também:

public class SmsNotificacao implements Notificacao {

    @Override
    public void enviar(String mensagem) {
        System.out.println("Enviando SMS: " + mensagem);
    }
}
Enter fullscreen mode Exit fullscreen mode

Uma fábrica pode ser responsável por criar o tipo correto:

public class NotificacaoFactory {

    public static Notificacao criar(String tipo) {

        if (tipo.equalsIgnoreCase("email")) {
            return new EmailNotificacao();
        }

        if (tipo.equalsIgnoreCase("sms")) {
            return new SmsNotificacao();
        }

        throw new IllegalArgumentException(
            "Tipo de notificação inválido"
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Assim, o código que utiliza a notificação não precisa conhecer diretamente todos os detalhes de criação.

Notificacao notificacao =
        NotificacaoFactory.criar("email");

notificacao.enviar("Olá, usuário!");
Enter fullscreen mode Exit fullscreen mode

Esse tipo de abordagem pode deixar o sistema mais flexível quando novos tipos de objetos precisam ser adicionados.

Exemplo 3: Strategy

O padrão Strategy é um dos meus exemplos favoritos porque mostra bem como podemos evitar vários if e else espalhados pelo sistema.

Imagine um sistema de pagamento que pode trabalhar com diferentes formas de pagamento:

  • Pix
  • Cartão
  • Boleto

Primeiro podemos definir uma interface:

public interface FormaPagamento {

    void pagar(double valor);
}
Enter fullscreen mode Exit fullscreen mode

Depois criamos as estratégias:

public class PagamentoPix implements FormaPagamento {

    @Override
    public void pagar(double valor) {
        System.out.println(
            "Pagamento de R$ " + valor + " realizado via Pix."
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Outra implementação:

public class PagamentoCartao implements FormaPagamento {

    @Override
    public void pagar(double valor) {
        System.out.println(
            "Pagamento de R$ " + valor + " realizado via cartão."
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora podemos criar uma classe responsável por utilizar qualquer estratégia:

public class Pagamento {

    private FormaPagamento formaPagamento;

    public Pagamento(FormaPagamento formaPagamento) {
        this.formaPagamento = formaPagamento;
    }

    public void realizar(double valor) {
        formaPagamento.pagar(valor);
    }
}
Enter fullscreen mode Exit fullscreen mode

Podemos então escolher a estratégia no momento da utilização:

FormaPagamento pix = new PagamentoPix();

Pagamento pagamento = new Pagamento(pix);

pagamento.realizar(100.00);
Enter fullscreen mode Exit fullscreen mode

Se amanhã o sistema precisar aceitar outro tipo de pagamento, podemos criar uma nova implementação da interface sem precisar modificar toda a estrutura existente.

Essa é uma das grandes vantagens do padrão Strategy.

Exemplo 4: Observer

O Observer é um padrão comportamental utilizado quando um objeto precisa notificar outros objetos sobre alterações em seu estado.

Um exemplo bastante conhecido é o sistema de notificações.

Imagine que um pedido seja criado em uma loja virtual. Depois que o pedido é criado, diferentes partes do sistema podem precisar ser avisadas:

  • serviço de e-mail;
  • serviço de estoque;
  • serviço de pagamento;
  • serviço de entrega.

Em vez de fazer o pedido conhecer diretamente todos esses serviços, podemos utilizar uma estrutura baseada em observadores.

Uma interface poderia ser:

public interface Observador {

    void atualizar(String mensagem);
}
Enter fullscreen mode Exit fullscreen mode

Um observador específico:

public class EmailService implements Observador {

    @Override
    public void atualizar(String mensagem) {
        System.out.println(
            "E-mail enviado: " + mensagem
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Outro observador:

public class EstoqueService implements Observador {

    @Override
    public void atualizar(String mensagem) {
        System.out.println(
            "Estoque atualizado: " + mensagem
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Quando um evento acontece, os observadores podem ser notificados.

Essa ideia é muito utilizada em sistemas que precisam reagir a eventos sem criar um forte acoplamento entre os componentes.

Design Patterns e orientação a objetos

Uma das razões pelas quais Design Patterns são tão importantes é a relação com os princípios da programação orientada a objetos.

Conceitos como:

  • encapsulamento;
  • abstração;
  • herança;
  • polimorfismo;

aparecem frequentemente na implementação dos padrões.

Além disso, muitos padrões ajudam a aplicar princípios de projeto, como o SOLID.

Por exemplo, ao utilizar interfaces e separar responsabilidades, podemos tornar o sistema mais fácil de modificar sem precisar alterar várias classes ao mesmo tempo.

Quando não utilizar um Design Pattern?

Uma coisa importante que aprendi estudando padrões de projeto é que nem todo problema precisa de um Design Pattern.

É possível exagerar na utilização desses padrões.

Se um problema é simples e pode ser resolvido de maneira clara com poucas classes, adicionar várias abstrações apenas para dizer que estamos utilizando um padrão pode deixar o código mais difícil de entender.

Por isso, antes de utilizar um padrão, é importante perguntar:

Esse padrão realmente resolve um problema que existe no meu sistema?

Se a resposta for não, provavelmente ele não é necessário.

O objetivo não deve ser utilizar o maior número possível de Design Patterns, mas sim utilizar a solução adequada para cada situação.

Conclusão

Design Patterns são uma ferramenta importante para quem trabalha com desenvolvimento de software.

Eles não fornecem uma solução pronta para todos os problemas, mas apresentam maneiras já conhecidas de organizar o código e resolver situações recorrentes.

Neste artigo vimos alguns exemplos:

Padrão Categoria Principal objetivo
Singleton Criacional Controlar a criação de uma única instância
Factory Criacional Centralizar ou abstrair a criação de objetos
Strategy Comportamental Permitir diferentes estratégias de comportamento
Observer Comportamental Notificar objetos quando determinado evento acontece

O mais importante é entender que Design Patterns não existem para deixar o código mais complicado ou mais sofisticado.

Eles existem para ajudar a criar sistemas que possam evoluir com menos dificuldade.

À medida que começamos a reconhecer problemas recorrentes no desenvolvimento, os padrões de projeto passam a ser menos uma "receita decorada" e mais uma forma de pensar na arquitetura e na organização do software.

No fim das contas, conhecer Design Patterns não significa saber todos os padrões de memória. Significa saber identificar um problema, conhecer possíveis soluções e escolher a alternativa mais adequada para aquele contexto.


Referências

  • Gamma, Erich; Helm, Richard; Johnson, Ralph; Vlissides, John. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
  • Freeman, Eric; Robson, Elisabeth. Head First Design Patterns. O'Reilly Media.
  • Documentação oficial da linguagem Java.

Tags sugeridas no Dev.to: java, designpatterns, programming, beginners

Top comments (0)