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
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
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
Podemos testar:
config1 = Configuracao()
config2 = Configuracao()
print(config1 is config2)
Resultado:
True
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
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()
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")
Agora podemos simplesmente fazer:
notificacao = NotificacaoFactory.criar("email")
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
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
Agora podemos escolher qual estratégia utilizar:
estrategia = DescontoVIP()
valor = 1000
desconto = estrategia.calcular(valor)
print(desconto)
Resultado:
200.0
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
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)
Agora podemos criar diferentes observadores:
class Dashboard:
def atualizar(self, mensagem):
print("Dashboard:", mensagem)
class Alerta:
def atualizar(self, mensagem):
print("ALERTA:", mensagem)
Quando acontece alguma coisa:
sistema.notificar("Risco elevado detectado")
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
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
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
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
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
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)