DEV Community

Design Patterns na prática: entendendo padrões de projeto sem complicação

Se você já começou a desenvolver projetos maiores, provavelmente já percebeu uma coisa:

fazer o código funcionar é apenas uma parte do problema.

No começo, é comum colocar algumas funções, criar algumas classes e adicionar vários if até tudo funcionar.

E tudo bem.

O problema aparece quando o projeto começa a crescer.

Uma alteração simples passa a quebrar outra parte do sistema, uma classe começa a ter responsabilidades demais e você percebe que não sabe mais onde deveria colocar uma nova funcionalidade.

É nesse momento que os Design Patterns, ou padrões de projeto, começam a fazer sentido.

Neste artigo, vamos entender o que são Design Patterns, por que eles existem e como alguns padrões bastante conhecidos podem ser utilizados na prática.


Afinal, o que é um Design Pattern?

De forma simples, um Design Pattern é uma solução conhecida para um problema recorrente no desenvolvimento de software.

Não é uma biblioteca.

Não é um código que você precisa copiar.

E também não é uma regra que deve ser seguida sempre.

Pense em Design Patterns como uma espécie de "receita de arquitetura".

Você encontra um problema que já é conhecido e utiliza uma estrutura de solução que já foi testada por outros desenvolvedores.

Podemos pensar assim:

Problema recorrente
        ↓
Solução conhecida
        ↓
Design Pattern
        ↓
Código mais organizado
Enter fullscreen mode Exit fullscreen mode

O objetivo não é necessariamente escrever menos código.

O objetivo é organizar melhor as responsabilidades do sistema e facilitar sua evolução.


Por que isso é importante?

Imagine que você está desenvolvendo um sistema de vendas.

No início, talvez tenha uma classe assim:

class Sistema:

    def cadastrar_usuario(self):
        pass

    def realizar_pagamento(self):
        pass

    def enviar_email(self):
        pass

    def gerar_relatorio(self):
        pass

    def salvar_banco(self):
        pass
Enter fullscreen mode Exit fullscreen mode

Funciona?

Sim.

É uma boa arquitetura?

Provavelmente não.

Essa classe está começando a fazer coisas demais.

Agora imagine que o projeto cresça e passe a ter milhares de linhas.

Uma alteração no sistema de pagamento pode acabar afetando o cadastro de usuários.

Uma alteração no envio de e-mail pode acabar afetando outra funcionalidade.

É aí que começamos a pensar em separação de responsabilidades, baixo acoplamento e organização do código.

E os Design Patterns podem ajudar justamente nesses cenários.


Os 3 grandes grupos

Os Design Patterns tradicionais são normalmente divididos em três grupos.

Creational Patterns

Relacionados à criação de objetos.

Alguns exemplos:

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

Structural Patterns

Relacionados à estrutura e composição dos objetos.

Alguns exemplos:

  • Adapter
  • Decorator
  • Facade
  • Composite
  • Proxy

Behavioral Patterns

Relacionados à comunicação e ao comportamento dos objetos.

Alguns exemplos:

  • Observer
  • Strategy
  • Command
  • State
  • Iterator

Agora vamos para alguns exemplos práticos.


1. Singleton

O Singleton é utilizado quando queremos garantir que determinada classe tenha apenas uma instância.

Imagine que sua aplicação tenha uma configuração central que deve ser compartilhada por diferentes partes do sistema.

Uma implementação simples em Python seria:

class Configuracao:
    _instancia = None

    def __new__(cls):
        if cls._instancia is None:
            cls._instancia = super().__new__(cls)

        return cls._instancia
Enter fullscreen mode Exit fullscreen mode

Podemos testar:

config1 = Configuracao()
config2 = Configuracao()

print(config1 is config2)
Enter fullscreen mode Exit fullscreen mode

Resultado:

True
Enter fullscreen mode Exit fullscreen mode

Isso significa que config1 e config2 estão utilizando a mesma instância.

Mas cuidado

Singleton parece simples e pode ser útil em alguns casos, mas não deve ser utilizado simplesmente porque existe.

Ele pode criar um estado global difícil de controlar e tornar testes mais complicados.

Por isso, antes de utilizar Singleton, vale perguntar:

"Eu realmente preciso garantir uma única instância?"

Se a resposta for não, provavelmente existe uma solução mais simples.


2. Factory Method

Agora imagine um sistema que envia notificações.

Podemos ter:

Notificação
├── E-mail
├── SMS
└── Push
Enter fullscreen mode Exit fullscreen mode

Sem uma estrutura adequada, podemos acabar espalhando vários if pelo sistema:

if tipo == "email":
    notificacao = Email()

elif tipo == "sms":
    notificacao = SMS()

elif tipo == "push":
    notificacao = Push()
Enter fullscreen mode Exit fullscreen mode

Conforme o sistema cresce, esse código pode aparecer em vários lugares.

Uma alternativa é centralizar a criação utilizando uma Factory:

class NotificacaoFactory:

    @staticmethod
    def criar(tipo):

        if tipo == "email":
            return Email()

        if tipo == "sms":
            return SMS()

        if tipo == "push":
            return Push()

        raise ValueError("Tipo inválido")
Enter fullscreen mode Exit fullscreen mode

Agora podemos simplesmente fazer:

notificacao = NotificacaoFactory.criar("email")
Enter fullscreen mode Exit fullscreen mode

A parte do sistema que utiliza a notificação não precisa saber exatamente como o objeto foi criado.

A responsabilidade de criação fica centralizada.


3. Strategy

O Strategy é um dos padrões que considero mais fáceis de visualizar.

Imagine um sistema de descontos.

Temos:

Desconto
├── Cliente comum
├── Cliente Premium
└── Cliente VIP
Enter fullscreen mode Exit fullscreen mode

Cada cliente possui uma regra diferente.

Podemos criar estratégias separadas:

class DescontoComum:

    def calcular(self, valor):
        return valor * 0.05


class DescontoPremium:

    def calcular(self, valor):
        return valor * 0.10


class DescontoVIP:

    def calcular(self, valor):
        return valor * 0.20
Enter fullscreen mode Exit fullscreen mode

Agora podemos escolher qual estratégia utilizar:

estrategia = DescontoVIP()

valor = 1000

desconto = estrategia.calcular(valor)

print(desconto)
Enter fullscreen mode Exit fullscreen mode

Resultado:

200.0
Enter fullscreen mode Exit fullscreen mode

A grande vantagem é que podemos trocar a estratégia sem precisar modificar toda a lógica da aplicação.

Isso é especialmente útil quando temos vários algoritmos diferentes para resolver o mesmo tipo de problema.


4. Observer

Agora vamos imaginar uma situação um pouco diferente.

Temos um sistema de monitoramento.

Quando alguma coisa acontece, vários componentes precisam ser avisados.

Por exemplo:

             Sistema
                │
                ▼
          Risco detectado
                │
       ┌────────┼────────┐
       ▼        ▼        ▼
    Dashboard  Alerta   Registro
Enter fullscreen mode Exit fullscreen mode

Quando um risco é detectado:

  • o Dashboard precisa atualizar;
  • o sistema de alertas precisa avisar alguém;
  • o sistema de registro precisa salvar o acontecimento.

É justamente nesse tipo de situação que o Observer pode ser interessante.

Uma implementação simples:

class Sistema:

    def __init__(self):
        self.observadores = []

    def adicionar(self, observador):
        self.observadores.append(observador)

    def notificar(self, mensagem):
        for observador in self.observadores:
            observador.atualizar(mensagem)
Enter fullscreen mode Exit fullscreen mode

Agora podemos criar diferentes observadores:

class Dashboard:

    def atualizar(self, mensagem):
        print("Dashboard:", mensagem)


class Alerta:

    def atualizar(self, mensagem):
        print("ALERTA:", mensagem)
Enter fullscreen mode Exit fullscreen mode

Quando acontece alguma coisa:

sistema.notificar("Risco elevado detectado")
Enter fullscreen mode Exit fullscreen mode

Os componentes interessados recebem a informação.

Esse padrão aparece bastante em sistemas orientados a eventos.


E se juntarmos isso com Inteligência Artificial?

Aqui começa uma parte interessante.

Design Patterns não servem apenas para sistemas tradicionais.

Eles também podem ajudar na organização de aplicações que utilizam Inteligência Artificial, APIs, sensores e processamento de dados.

Imagine um sistema que monitora um carro:

Sensores
   ↓
Coleta de dados
   ↓
Processamento
   ↓
Modelo de IA
   ↓
Análise de risco
   ↓
Alerta
Enter fullscreen mode Exit fullscreen mode

Podemos utilizar diferentes padrões em diferentes partes dessa arquitetura.

Por exemplo:

Strategy

Podemos utilizar diferentes estratégias para analisar tipos diferentes de dados:

Modelo de análise
├── Análise de pneus
├── Análise do motor
└── Análise de segurança
Enter fullscreen mode Exit fullscreen mode

Factory

Podemos utilizar uma Factory para criar o analisador correto.

Observer

Podemos notificar diferentes componentes quando a IA identificar uma situação de risco.

Isso poderia resultar em algo como:

                  Sensores
                     │
                     ▼
              Coleta de dados
                     │
                     ▼
                  Factory
                     │
          ┌──────────┼──────────┐
          ▼          ▼          ▼
       Análise     Análise    Análise
       de pneus    do motor   segurança
          │          │          │
          └──────────┼──────────┘
                     ▼
                  Strategy
                     │
                     ▼
              Análise de risco
                     │
                     ▼
                  Observer
               ┌─────┼─────┐
               ▼     ▼     ▼
          Dashboard Alerta Registro
Enter fullscreen mode Exit fullscreen mode

Essa é apenas uma arquitetura conceitual, mas mostra como diferentes padrões podem trabalhar juntos.


Mas existe um problema: Design Patterns demais

Aqui está uma das coisas mais importantes para quem está começando.

Você não precisa usar Design Patterns em tudo.

Às vezes, vejo projetos em que uma funcionalidade extremamente simples possui:

  • 5 interfaces;
  • 8 classes;
  • 3 Factories;
  • 2 Managers;
  • 1 Singleton;
  • e várias abstrações.

Tudo isso para fazer uma operação que poderia ser resolvida com uma função simples.

Nesse caso, o padrão está criando mais problemas do que resolvendo.

Design Patterns devem ser utilizados quando realmente ajudam a resolver um problema.

Não devemos utilizar um padrão apenas para dizer que nosso projeto utiliza Design Patterns.


Como escolher o padrão certo?

Em vez de começar perguntando:

"Qual Design Pattern eu posso usar?"

Comece perguntando:

"Qual problema eu estou tentando resolver?"

Por exemplo:

Preciso criar diferentes tipos de objetos?

Talvez Factory Method seja uma boa opção.

Tenho diferentes algoritmos para realizar uma tarefa?

Talvez Strategy seja adequado.

Vários componentes precisam receber uma notificação?

Talvez Observer seja interessante.

Preciso adaptar uma classe existente para outra interface?

Talvez Adapter resolva o problema.

Preciso controlar uma única instância?

Talvez Singleton seja uma possibilidade.

O importante é entender que o padrão deve surgir da necessidade do sistema.


Design Pattern não é receita de bolo

É fácil cair na ideia de que existe um padrão perfeito para cada situação.

Na prática, não é tão simples.

Um mesmo problema pode ser resolvido de várias maneiras.

A escolha depende de fatores como:

  • tamanho do projeto;
  • linguagem utilizada;
  • equipe;
  • requisitos;
  • facilidade de manutenção;
  • necessidade de testes;
  • complexidade da aplicação.

Por isso, estudar Design Patterns não significa decorar uma lista de nomes.

Significa aprender a identificar problemas de projeto e conhecer diferentes formas de solucioná-los.


O que eu levaria desse estudo?

Se você está começando a estudar Design Patterns, não tente decorar todos os padrões de uma vez.

Comece por alguns:

Singleton
    ↓
Factory
    ↓
Strategy
    ↓
Observer
    ↓
Adapter
Enter fullscreen mode Exit fullscreen mode

Entenda:

qual problema cada um resolve,

quando ele faz sentido,

quando não utilizá-lo,

e principalmente:

como ele muda a organização do código.

Depois disso, fica muito mais fácil estudar os outros padrões.


Conclusão

Design Patterns são uma forma de compartilhar soluções para problemas recorrentes no desenvolvimento de software.

Eles podem ajudar a criar sistemas mais organizados, flexíveis e fáceis de manter.

Mas existe uma coisa mais importante do que conhecer dezenas de padrões:

saber identificar quando você realmente precisa de um.

Não devemos usar Design Patterns para deixar o código mais complicado ou para demonstrar conhecimento.

Devemos utilizá-los quando eles tornam o projeto mais fácil de entender, modificar, testar e evoluir.

No final, o objetivo não é ter o código mais sofisticado.

É ter um código que resolva o problema da melhor maneira possível.


Para continuar estudando

Se você está começando agora, uma boa sequência seria:

Programação Orientada a Objetos
            ↓
Princípios SOLID
            ↓
Design Patterns
            ↓
Arquitetura de Software
            ↓
Clean Code
Enter fullscreen mode Exit fullscreen mode

Quanto melhor entendermos os problemas de projeto, mais fácil fica escolher a ferramenta certa para cada situação.

E talvez essa seja a principal lição dos Design Patterns:

Não procure um padrão para encaixar no seu código. Procure entender o problema e então escolha a solução mais adequada.

Top comments (0)