Design Patterns em Python: um guia para aplicar padrões de projeto

No desenvolvimento de software, é comum nos depararmos com desafios que se repetem de projeto em projeto, como organizar o código de forma escalável, facilitar a manutenção ou implementar funcionalidades complexas.
Esses problemas são como enigmas que, se resolvidos de maneira eficaz, tornam o trabalho mais eficiente e menos propenso a erros.
Assim como em uma receita de bolo já testada, com a qual sabemos exatamente os ingredientes, quantidades e o passo a passo para obter um bom resultado, na programação também existem padrões que ajudam a construir sistemas de forma eficiente e confiável, resolvendo esses desafios comuns.
Esses padrões, conhecidos como Design Pattern ou Padrões de Projeto, foram desenvolvidos ao longo do tempo por pessoas que experimentaram diferentes abordagens e encontraram soluções que funcionam, permitindo que hoje possamos utilizá-las para economizar tempo de desenvolvimento com problemas comuns.
O mais interessante é que esses padrões são independentes de linguagem e se adaptam a diferentes contextos.
No caso do Python, a flexibilidade da linguagem simplifica a aplicação desses padrões, permitindo soluções elegantes e práticas.
Então, se é isso o que você está buscando, a resposta está neste artigo em que falaremos sobre os principais conceitos e princípios dos Design Patterns e sobre como aplicá-los em Python.
Bora lá?!
Categorias de Design Patterns
No desenvolvimento de software, os problemas enfrentados podem variar amplamente, desde a maneira como os objetos são criados até como diferentes partes do sistema se comunicam e colaboram.
Sem uma estrutura bem definida, essas questões podem resultar em códigos complicados, pouco flexíveis e difíceis de evoluir.
Para lidar com esses desafios eficientemente, os designs patterns foram organizados em categorias, cada uma focada em resolver um tipo específico de problema de design, promovendo clareza e boas práticas no desenvolvimento. São elas:
- Padrões Criacionais
- Padrões Estruturais
- Padrões Comportamentais
Essas categorias não apenas facilitam a escolha da solução mais adequada, mas também oferecem uma base sólida para enfrentar desafios recorrentes no design de software.
Por isso, estruturamos este artigo com base nessas categorias, destacando os principais padrões de design associados a elas para o desenvolvimento em Python.
Vamos mergulhar em cada uma delas e entender como podem transformar como estruturamos nossos sistemas.
Padrões Criacionais
A criação de objetos é um aspecto fundamental no desenvolvimento de sistemas.
Imagine qualquer sistema, como o de uma loja virtual de móveis, por exemplo: cada produto no catálogo (como "Cadeira" ou "Mesa") é representado como um objeto com atributos específicos, como nome, preço e estoque. Isso permite gerenciar dados de forma estruturada e reutilizável.
No entanto, à medida que os sistemas se tornam mais complexos, a maneira como os objetos são instanciados pode impactar diretamente a flexibilidade, escalabilidade e manutenção do código.
Quando a lógica de criação de objetos está espalhada pelo código ou fortemente acoplada a classes específicas, o sistema enfrenta uma série de desafios.
A adição de novos tipos de objetos, por exemplo, pode exigir alterações em diversas partes do código, aumentando a dificuldade de manutenção e a chance de ocorrerem erros.
Além disso, garantir que os objetos sejam criados com configurações consistentes torna-se uma tarefa complexa e sujeita a falhas, especialmente em sistemas grandes ou dinâmicos.
O acoplamento e repetição não somente dificultam a reutilização do código, mas também comprometem sua flexibilidade, tornando o sistema menos preparado para lidar com mudanças ou expansões futuras.
Sendo assim, os padrões criacionais fornecem abordagens bem definidas para resolver os desafios associados à criação de objetos.
Eles abstraem ou encapsulam o processo de instanciação, permitindo que devs criem objetos de forma flexível e independente da implementação concreta, permitindo que o sistema seja mais adaptável às mudanças e promovendo a reutilização do código.
Cada um deles oferece uma abordagem específica para lidar com diferentes cenários. Exploramos a seguir os principais padrões criacionais, entendendo como eles funcionam e como podem ser aplicados para melhorar o design de software.
Abstract Factory
Imagine um sistema que precisa criar interfaces diferentes para web e desktop, onde botões, janelas e menus devem seguir o mesmo estilo em cada plataforma, como mostrado no código a seguir:
class Botao:
def renderizar(self):
pass
class BotaoDesktop(Botao):
def renderizar(self):
return "Botão de Desktop"
class BotaoWeb(Botao):
def renderizar(self):
return "Botão de Web"
A classe base Botao
define o método renderizar
, e as duas classes derivadas (BotaoDesktop
e BotaoWeb
) implementam esse método para retornar representações específicas de botões ("Botão de Desktop" e "Botão de Web").
Com isso, sem o Abstract Factory (ou Fábrica Abstrata), você teria código espalhado por várias partes do sistema decidindo qual tipo de objeto criar.
Isso levaria à duplicação de lógica e a um código difícil de manter, como mostrado a seguir, em que o código verifica o valor de plataforma
e imprime o botão correspondente – "Botão de Desktop" se for "desktop" ou "Botão de Web" caso contrário:
plataforma = "web"
if plataforma == "desktop":
print(BotaoDesktop().renderizar())
else:
print(BotaoWeb().renderizar())
No desenvolvimento de sistemas, são frequentes essas situações em que é necessário criar diferentes tipos de objetos que pertencem a uma mesma família (como os citados botões, menus e janelas), mantendo a consistência entre si.
Então, em vez de criar cada objeto individualmente em diferentes partes do sistema, o padrão Abstract Factory permite centralizar a criação desses objetos relacionados em uma fábrica, garantindo que todos sigam o mesmo conjunto de regras e evitando a duplicação de código. Por exemplo:
class Fabrica:
def criar_botao(self):
pass
class FabricaDesktop(Fabrica):
def criar_botao(self):
return BotaoDesktop()
class FabricaWeb(Fabrica):
def criar_botao(self):
return BotaoWeb()
def cliente(fabrica: Fabrica):
botao = fabrica.criar_botao()
print(botao.renderizar())
cliente(FabricaDesktop()) # Saída: Botão de Desktop
cliente(FabricaWeb()) # Saída: Botão de Web
No código acima, usamos o padrão Factory Method para criar objetos específicos (BotaoDesktop
ou BotaoWeb
) por meio de fábricas (FabricaDesktop
e FabricaWeb
).
A função cliente
chama a fábrica para criar e renderizar o botão, imprimindo "Botão de Desktop" ou "Botão de Web" conforme a fábrica utilizada.
Ou seja, com o Abstract Factory, você encapsula a lógica de criação em uma fábrica que cria famílias de objetos compatíveis, centralizando a lógica de instanciação e tornando o sistema mais modular.
Builder
Suponha que você precisa criar objetos complexos, como computadores personalizados, com diferentes componentes como CPU, memória, entre outros. Sem uma abordagem estruturada, a criação desses objetos pode rapidamente se tornar confusa e difícil de manter.
Por exemplo, sem o Builder (ou Construtor), você precisaria configurar manualmente cada atributo do objeto em várias partes do sistema:
class Computador:
def __init__(self):
self.cpu = None
self.memoria = None
computador = Computador()
computador.cpu = "Intel i5"
computador.memoria = 16
print(f"CPU: {computador.cpu}, Memória: {computador.memoria}GB")
Nesse caso, a classe base Computador
define os atributos cpu
e memoria
, mas a lógica de construção está espalhada pelo código, pois cada instância está sendo configurada manualmente em diferentes partes do sistema.
Isso pode levar a repetições, dificultando a manutenção e aumentando a chance de erros ao criar novos objetos.
Sendo assim, torna-se útil no desenvolvimento de sistemas uma forma clara e reutilizável para criar objetos complexos.
O Builder é o padrão criado para resolver esse tipo de problema, separando a construção do objeto de sua representação, o que permite que você configure o objeto passo a passo, de forma organizada e legível. Por exemplo:
class Computador:
def __init__(self):
self.cpu = None
self.memoria = None
def __str__(self):
return f"CPU: {self.cpu}, Memória: {self.memoria}GB"
class ComputadorBuilder:
def __init__(self):
self.computador = Computador()
def definir_cpu(self, cpu):
self.computador.cpu = cpu
return self
def definir_memoria(self, memoria):
self.computador.memoria = memoria
return self
def construir(self):
return self.computador
# Uso do Builder
builder = ComputadorBuilder()
pc_gamer = (
builder.definir_cpu("Intel i7")
.definir_memoria(32)
.construir()
)
print(pc_gamer) # Saída: CPU: Intel i7, Memória: 32GB
A classe ComputadorBuilder
encapsula a lógica da construção do objeto. Ela fornece métodos definir_cpu
e definir_memoria
, que permitem configurar os atributos de forma estruturada e encadeada. Isso torna a criação do objeto mais organizada e reutilizável.
O método construir
retorna um objeto Computador
completamente configurado, garantindo que a construção siga um padrão definido, facilitando modificações futuras sem impactar o restante do código.
Esse padrão melhora a legibilidade, evita duplicação de código e torna o sistema mais flexível para futuras expansões.
Factory
Em um sistema de desenho, diferentes formas geométricas, como círculos, quadrados e triângulos, precisam ser criadas com comportamentos específicos, como “calcular a área” ou “desenhar”.
Por exemplo, sem o padrão Factory, você poderia ter um código que decide diretamente qual forma criar em cada parte do sistema:
class Circulo:
def desenhar(self):
return "Desenhando um círculo"
class Quadrado:
def desenhar(self):
return "Desenhando um quadrado"
forma = "circulo"
if forma == "circulo":
circulo = Circulo()
print(circulo.desenhar())
elif forma == "quadrado":
quadrado = Quadrado()
print(quadrado.desenhar())
A classe Circulo
e a classe Quadrado
implementam o método desenhar
, retornando uma string correspondente à forma desenhada.
A variável forma
define qual objeto será criado. A estrutura condicional verifica seu valor e instancia a classe correspondente.
Aqui, a lógica de criação da forma está espalhada pelo sistema, o que dificulta adicionar novas formas no futuro e pode levar a duplicação de código, dificultando a manutenção e a extensibilidade.
No desenvolvimento de sistemas, frequentemente nos deparamos com a necessidade de criar diferentes tipos de objetos que pertencem a uma mesma família, mas que podem variar em características específicas.
Esses objetos precisam ser consistentes entre si, seguindo um mesmo padrão ou estilo, tornando a criação desses objetos mais desafiadora quando a lógica está dispersa por várias partes do sistema.
Ao invés de criar objetos diretamente, a utilização de uma Fábrica ajuda a centralizar a criação e garantir que os objetos de uma mesma família sejam consistentes e corretamente configurados, permitindo que você crie instâncias sem se preocupar com a lógica interna.
Com a Factory, você pode criar um padrão de fábrica que abstrai a criação das formas:
class Forma:
def desenhar(self):
pass
class Circulo(Forma):
def desenhar(self):
return "Desenhando um círculo"
class Quadrado(Forma):
def desenhar(self):
return "Desenhando um quadrado"
# Fábrica de Formas
class FabricaDeFormas:
def criar_forma(self, tipo):
if tipo == "circulo":
return Circulo()
elif tipo == "quadrado":
return Quadrado()
# Uso da Factory
fabrica = FabricaDeFormas()
forma = fabrica.criar_forma("circulo")
print(forma.desenhar()) # Saída: Desenhando um círculo
forma = fabrica.criar_forma("quadrado")
print(forma.desenhar()) # Saída: Desenhando um quadrado
A classe base Forma
define o método desenhar
, que será implementado pelas classes derivadas Circulo
e Quadrado
.
Cada uma dessas classes fornece sua própria implementação do método, retornando uma mensagem correspondente à forma desenhada.
A classe FabricaDeFormas
centraliza a criação dos objetos. O método criar_forma
recebe um tipo de forma como argumento e retorna a instância apropriada (Circulo
ou Quadrado
).
Nesse padrão, um objeto FabricaDeFormas
é criado e utilizado para gerar instâncias necessárias sem expor a lógica interna de criação.
Sendo assim, a criação de objetos é centralizada em uma única classe, tornando o código mais modular e fácil de expandir.
Ao adicionar novas formas, você só precisa modificar a fábrica, sem impactar o restante do código, facilitando a manutenção e a escalabilidade do sistema.
Prototype
Imagine que você está desenvolvendo o sistema de um e-commerce de móveis e precisa gerenciar um catálogo de produtos.
Cada produto tem atributos como nome, material, cor, preço, entre outros, e esse conjunto de características forma uma espécie de protótipo.
Muitos dos produtos vendidos nessa loja compartilham dessas características comuns, como uma cadeira ou um sofá, que possuem igualmente suas especificações de material, cor, preço e assim por diante.
Portanto, esses produtos podem ser copiados para criar novos itens, mas mantendo características e atributos.
Para fazer isso sem o Prototype (ou protótipo), você precisaria criar uma nova instância de cada produto e configurar manualmente seus atributos em várias partes do código, por exemplo:
class Produto:
def __init__(self, nome, material, cor, preco):
self.nome = nome
self.cor = cor
self.material = material
self.preco = preco
# Criando um produto manualmente
produto1 = Produto("Cadeira", "Madeira", "Branca",150.00)
# Criando outro produto similar
produto2 = Produto("Sofá de Luxo", "Suede", "Azul", 2500.00)
A classe Produto
define os atributos nome
, material
, cor
e preco
. Os objetos produto1
e produto2
são criados passando os valores diretamente na instância.
Nesse caso, você tem que criar e configurar manualmente novos objetos em várias partes do código, o que torna a manutenção mais difícil e propensa a erros, especialmente se você precisar criar muitos produtos com características semelhantes.
No desenvolvimento de sistemas, o padrão de design Prototype oferece uma solução mais eficaz, permitindo criar novos objetos baseados em cópias de um protótipo existente.
Ou seja, ao invés de criar objetos do zero, você pode simplesmente clonar um objeto já configurado, o que economiza tempo e facilita a criação de instâncias com atributos similares.
Com o Prototype, você poderia, então, criar um protótipo de um produto e cloná-lo para gerar novos objetos com as mesmas características:
import copy
class Produto:
def __init__(self, nome, material, cor, preco):
self.nome = nome
self.material = material
self.cor = cor
self.preco = preco
def clonar(self):
return copy.deepcopy(self) # Faz uma cópia do objeto
# Criando um protótipo de produto básico
produto_prototipo = Produto(
nome="Cadeira de Madeira",
material="Madeira",
cor="Branca",
preco=150.00
)
# Clonando o protótipo
## Criando um produto com todas as características básicas do protótipo, mudando apenas o nome
produto1 = produto_prototipo.clonar()
produto1.cor = "Cadeira de Madeira Básica”
## Criando um produto com todas as características básicas do protótipo, mudando alguns detalhes de modelo
produto2 = produto_prototipo.clonar()
produto2.nome = "Cadeira de Madeira Luxo"
produto2.cor = "Marrom Escuro"
produto2.preco = 250.00
## Criando um produto com todas as características básicas do protótipo diferentes, mas com os mesmos atributos
produto3 = produto_prototipo.clonar()
produto3.nome = "Sofá Conforto"
produto3.cor = "Azul"
produto3.preco = 2500.00
O código utiliza o método deepcopy
para clonar o objeto produto_prototipo
, criando cópias independentes com os mesmos atributos. Cada cópia pode ser alterada sem afetar o protótipo original.
O método clonar
permite gerar novos produtos com características similares, mas com modificações específicas, como mudanças no nome, cor ou preço, sem precisar recriar todos os atributos do zero.
Ou seja, você pode facilmente criar novos objetos a partir de um protótipo existente, mantendo as características consistentes e evitando a duplicação de código.
Isso torna a criação de novos objetos mais eficiente e a manutenção do código mais simples, já que a lógica de criação é centralizada em um único objeto de origem.
Singleton
Você precisa gerenciar uma conexão com um banco de dados em seu sistema, mas quer garantir que apenas uma instância dessa conexão seja criada, independentemente de quantas vezes ela for solicitada ao longo do tempo.
Sem um padrão claro, você pode acabar criando múltiplas instâncias dessa conexão, o que pode gerar problemas de desempenho e até mesmo inconsistências no gerenciamento de dados.
Sem o Singleton, ou “carta única”, você pode ter o código criando novas instâncias da classe de conexão cada vez que for necessário, como no exemplo abaixo:
class ConexaoBanco:
def __init__(self):
print("Conexão com o banco de dados criada!")
# Criando várias instâncias da classe
conexao1 = ConexaoBanco()
conexao2 = ConexaoBanco()
Neste caso, sempre que a classe ConexaoBanco
é chamada, uma nova instância é criada, o que pode ser ineficiente, especialmente se a conexão ao banco de dados for custosa.
Além disso, isso pode gerar múltiplas conexões simultâneas, dificultando o controle.
O padrão Singleton resolve esse problema garantindo que uma classe tenha apenas uma instância e fornecendo um ponto global de acesso a essa instância.
A solução é criar uma única instância da classe e reutilizá-la ao longo do código, evitando que múltiplas instâncias sejam criadas.
Com o Singleton, a criação da conexão seria feita da seguinte forma:
class ConexaoBanco:
_instancia = None
def __new__(cls):
if cls._instancia is None:
cls._instancia = super().__new__(cls)
print("Conexão com o banco de dados criada!")
return cls._instancia
# Criando as conexões
conexao1 = ConexaoBanco()
conexao2 = ConexaoBanco()
# Verificando se as instâncias são as mesmas
print(conexao1 is conexao2) # Saída: True
Neste exemplo, quando a classe ConexaoBanco
é chamada pela primeira vez, uma nova instância é criada. Nas chamadas subsequentes, a mesma instância é retornada, garantindo que apenas uma conexão seja estabelecida.
O método __new__
verifica se a instância já existe. Se não, ela é criada e armazenada em _instancia
; caso contrário, a instância existente é retornada.
Quando as variáveis conexao1
e conexao2
são criadas, ambas se referem à mesma instância, e a mensagem "Conexão com o banco de dados criada!" é exibida apenas uma vez.
A verificação conexao1 is conexao2
retorna True
, confirmando que as duas variáveis apontam para a mesma instância.
Com isso, você resolve o problema da criação desnecessária de múltiplas instâncias e garante que apenas uma conexão ao banco de dados seja usada durante a execução do programa, melhorando a eficiência do sistema e simplifica o controle de recursos compartilhados.
Padrões Estruturais
Podemos comparar a estrutura e organização dos componentes de um sistema à construção de uma casa.
Sem um projeto arquitetônico claro, os cômodos podem ser construídos de forma desordenada, com portas e janelas mal posicionadas, dificultando a circulação.
Além disso, usar materiais inadequados ou instalar sistemas elétricos e hidráulicos sem planejamento pode levar a reparos frequentes, aumento de custos e até riscos de colapso. Da mesma forma, em um sistema de software, sem um design estruturado, os componentes podem se conectar de maneira desordenada, gerando dependências rígidas, inconsistências, duplicações e dificuldade para realizar alterações ou adicionar novas funcionalidades.
Ou seja, a estrutura e a organização dos componentes de um sistema desempenham um papel crucial no desenvolvimento de software.
Sem uma abordagem estruturada, que pode levar a componentes excessivamente acoplados, dificultando testes unitários, manutenção e reutilização do código. Com isso, os sistemas podem rapidamente se tornar confusos e difíceis de escalar.
Os padrões estruturais surgem como uma solução para esses desafios, assim como os projetos arquitetônicos das casas, fornecendo estratégias para organizar os componentes de forma que suas relações sejam claras, flexíveis e escaláveis.
Eles tratam especificamente de como as classes e objetos são compostos para formar estruturas maiores e mais robustas, facilitando a interação entre diferentes partes do sistema.
Esses padrões permitem que as pessoas desenvolvedoras criem sistemas onde os componentes possam ser facilmente substituídos ou reutilizados, reduzindo o acoplamento e promovendo um design modular e coeso.
Cada padrão estrutural aborda um problema específico de design e oferece uma solução que combina simplicidade, reutilização e clareza.
A seguir, exploraremos os principais padrões estruturais, mostrando como eles podem ser aplicados para melhorar a arquitetura de sistemas e promover um código mais eficiente e sustentável.
Adapter
Imagine que você está desenvolvendo um sistema que precisa integrar diferentes APIs ou classes que possuem interfaces incompatíveis.
Por exemplo, considere um sistema de pagamento que deve suportar múltiplos provedores, como PayPal e Stripe, onde cada um possui seu próprio formato de API para processar pagamentos.
Sem um padrão estruturado, o código que faz a integração pode se tornar um emaranhado de lógica condicional e adaptações espalhadas, dificultando a manutenção e a escalabilidade. Por exemplo:
class PayPal:
def pagar(self, valor):
return f"Pagamento de ${valor} realizado com PayPal"
class Stripe:
def processar_pagamento(self, valor):
return f"Pagamento de ${valor} processado com Stripe"
# Uso direto e inconsistente
metodo = "paypal"
if metodo == "paypal":
paypal = PayPal()
print(paypal.pagar(100))
elif metodo == "stripe":
stripe = Stripe()
print(stripe.processar_pagamento(200))
O código mostra uma implementação inconsistente para lidar com diferentes métodos de pagamento, utilizando as classes PayPal
e Stripe
, porque cada classe tem um método diferente para processar pagamentos (pagar
e processar_pagamento
, respectivamente).
Esse sistema usa uma estrutura condicional para escolher o método apropriado com base na variável metodo
.
Essa abordagem resulta em uma solução desorganizada e difícil de manter, ao requerer modificações sempre que um novo método de pagamento é adicionado, além de não fornecer uma interface consistente para os diferentes sistemas de pagamento.
O uso do padrão Adapter poderia resolver esse problema, criando uma interface única para todos os métodos.
Nesse caso, o padrão Adapter (ou adaptador) resolve esse problema ao fornecer uma interface intermediária que traduz as chamadas feitas por um sistema para o formato esperado por outro.
Seguindo esse padrão, você encapsula as diferenças entre as interfaces:
# Classes existentes
class PayPal:
def pagar(self, valor):
return f"Pagamento de ${valor} realizado com PayPal"
class Stripe:
def processar_pagamento(self, valor):
return f"Pagamento de ${valor} processado com Stripe"
# Adapter para unificar as interfaces
class PagamentoAdapter:
def __init__(self, metodo):
self.metodo = metodo
def realizar_pagamento(self, valor):
if isinstance(self.metodo, PayPal):
return self.metodo.pagar(valor)
elif isinstance(self.metodo, Stripe):
return self.metodo.processar_pagamento(valor)
# Uso do Adapter
paypal_adapter = PagamentoAdapter(PayPal())
print(paypal_adapter.realizar_pagamento(100)) # Saída: Pagamento de $100 realizado com PayPal
stripe_adapter = PagamentoAdapter(Stripe())
print(stripe_adapter.realizar_pagamento(200)) # Saída: Pagamento de $200 processado com Stripe
O código utiliza o padrão Adapter para unificar diferentes interfaces de pagamento. As classes PayPal
e Stripe
possuem métodos distintos (pagar
e processar_pagamento
, respectivamente) para realizar pagamentos.
O PagamentoAdapter
atua como um intermediário, aceitando qualquer um desses métodos e oferecendo uma interface comum através do método realizar_pagamento
.
Ao criar instâncias do PagamentoAdapter
com PayPal
ou Stripe
, é possível chamar o método realizar_pagamento
consistentemente, sem se preocupar com as diferenças nas implementações de cada classe, garantindo flexibilidade e extensibilidade no código.
Dessa maneira, as diferenças entre as interfaces são abstraídas em uma única classe. Isso permite integrar facilmente novos métodos de pagamento sem alterar o restante do sistema, promovendo um design mais limpo, flexível e preparado para mudanças.
Composite
Você está desenvolvendo um sistema que precisa lidar com estruturas hierárquicas, como uma organização de pastas e arquivos.
Cada pasta pode conter tanto arquivos quanto outras pastas, e você deseja executar operações como calcular o tamanho total ou listar os conteúdos de qualquer nível dessa estrutura.
Sem um padrão estruturado, o código para manipular essa hierarquia pode se tornar complicado e cheio de condições para tratar diferentes tipos de objetos:
class Arquivo:
def __init__(self, nome, tamanho):
self.nome = nome
self.tamanho = tamanho
def obter_tamanho(self):
return self.tamanho
class Pasta:
def __init__(self, nome):
self.nome = nome
self.conteudo = []
def adicionar(self, item):
self.conteudo.append(item)
def obter_tamanho(self):
tamanho_total = 0
for item in self.conteudo:
if isinstance(item, Arquivo):
tamanho_total += item.obter_tamanho()
elif isinstance(item, Pasta):
tamanho_total += item.obter_tamanho()
return tamanho_total
# Estrutura manual para calcular tamanho
pasta_raiz = Pasta("Raiz")
pasta_documentos = Pasta("Documentos")
pasta_imagens = Pasta("Imagens")
arquivo1 = Arquivo("doc1.txt", 100)
arquivo2 = Arquivo("img1.jpg", 500)
pasta_documentos.adicionar(arquivo1)
pasta_imagens.adicionar(arquivo2)
pasta_raiz.adicionar(pasta_documentos)
pasta_raiz.adicionar(pasta_imagens)
print(f"Tamanho total: {pasta_raiz.obter_tamanho()}") # Saída: Tamanho total: 600
O código cria uma estrutura de pastas e arquivos, onde cada pasta pode conter arquivos e outras pastas.
A classe Arquivo
possui um método obter_tamanho
que retorna o tamanho de um arquivo, enquanto a classe Pasta
tem um método obter_tamanho
que calcula o tamanho total de todos os itens dentro dela, incluindo arquivos e subpastas.
A estrutura é construída com pastas e arquivos, e cada item é adicionado à pasta correspondente.
Ao final, o código calcula o tamanho total da pasta raiz, somando os tamanhos dos arquivos e das subpastas.
Embora funcional, esse código exige que você trate manualmente cada tipo de objeto na hierarquia, o que pode levar a código duplicado e difícil de manter.
Para resolver esse tipo de problema, em que precisamos manipular objetos individuais e composições de objetos de maneira uniforme, usamos o padrão Composite (ou Árvore de objetos) para tratar objetos individuais e composições de objetos de forma consistente.
Você pode unificar o tratamento de arquivos e pastas usando uma interface comum:
# Interface comum para Componentes
class Componente:
def obter_tamanho(self):
pass
# Classe Arquivo
class Arquivo(Componente):
def __init__(self, nome, tamanho):
self.nome = nome
self.tamanho = tamanho
def obter_tamanho(self):
return self.tamanho
# Classe Pasta
class Pasta(Componente):
def __init__(self, nome):
self.nome = nome
self.conteudo = []
def adicionar(self, item):
self.conteudo.append(item)
def obter_tamanho(self):
return sum(item.obter_tamanho() for item in self.conteudo)
# Uso do Composite
pasta_raiz = Pasta("Raiz")
pasta_documentos = Pasta("Documentos")
pasta_imagens = Pasta("Imagens")
arquivo1 = Arquivo("doc1.txt", 100)
arquivo2 = Arquivo("img1.jpg", 500)
pasta_documentos.adicionar(arquivo1)
pasta_imagens.adicionar(arquivo2)
pasta_raiz.adicionar(pasta_documentos)
pasta_raiz.adicionar(pasta_imagens)
print(f"Tamanho total: {pasta_raiz.obter_tamanho()}") # Saída: Tamanho total: 600
O código acima implementa o padrão Composite, que permite tratar objetos compostos e individuais de maneira uniforme.
A classe Componente
define uma interface comum com o método obter_tamanho
, que é implementado tanto pela classe Arquivo
quanto pela classe Pasta
.
A classe Arquivo
retorna seu próprio tamanho, enquanto a classe Pasta
calcula o tamanho total somando os tamanhos dos itens que contém, sejam arquivos ou outras pastas.
Dessa forma, tanto arquivos quanto pastas podem ser tratados da mesma forma ao chamar obter_tamanho
. O código cria uma estrutura de pastas e arquivos, adiciona os itens e, ao final, calcula o tamanho total da pasta raiz.
Dessa forma, você pode adicionar novos tipos de componentes ou alterar a hierarquia sem precisar modificar a lógica de como os tamanhos são calculados.
Decorator
Ao desenvolver um sistema de notificação, é necessário lidar com diferentes canais de envio, como e-mail ou SMS.
Com o tempo, novas funcionalidades podem ser adicionadas, como registrar as notificações em um log ou enviá-las com uma mensagem de prioridade, tornando o sistema mais robusto e flexível.
Sem um padrão estruturado, você poderia acabar criando classes para cada combinação de funcionalidades, resultando em um crescimento exponencial no número de classes e na complexidade, por exemplo:
class NotificacaoEmail:
def enviar(self, mensagem):
return f"Enviando e-mail: {mensagem}"
class NotificacaoSMS:
def enviar(self, mensagem):
return f"Enviando SMS: {mensagem}"
class NotificacaoEmailComPrioridade:
def __init__(self, prioridade):
self.prioridade = prioridade
def enviar(self, mensagem):
mensagem_com_prioridade = f"[Prioridade {self.prioridade}] {mensagem}"
return f"Enviando e-mail: {mensagem_com_prioridade}"
# Combinação de outros tipos de notificações e funcionalidades…
O código define três classes para enviar notificações: NotificacaoEmail
, NotificacaoSMS
e NotificacaoEmailComPrioridade
. Cada classe tem um método enviar
que retorna uma string informando o tipo de notificação e a mensagem.
A classe NotificacaoEmailComPrioridade
adiciona um nível de prioridade à mensagem antes de enviá-la por e-mail.
Assim, o código cobre notificações básicas por e-mail e SMS, além de permitir o envio de e-mails com prioridade.
Com essa abordagem, cada nova funcionalidade ou combinação exigiria a criação de uma nova classe, tornando o código difícil de manter e expandir.
O padrão Decorator (ou decorador), resolve esse problema ao permitir que você adicione funcionalidades a um objeto dinamicamente, sem modificar a classe base. Com ele, você pode criar componentes independentes que podem ser combinados conforme necessário.
Você pode implementar as funcionalidades adicionais de forma modular:
# Componente base
class Notificacao:
def enviar(self, mensagem):
pass
# Componente concreto
class NotificacaoEmail(Notificacao):
def enviar(self, mensagem):
return f"Enviando e-mail: {mensagem}"
class NotificacaoSMS(Notificacao):
def enviar(self, mensagem):
return f"Enviando SMS: {mensagem}"
# Decorator base
class NotificacaoDecorator(Notificacao):
def __init__(self, notificacao):
self.notificacao = notificacao
def enviar(self, mensagem):
return self.notificacao.enviar(mensagem)
# Decorator para registrar log
class LogDecorator(NotificacaoDecorator):
def enviar(self, mensagem):
log = "Registrando log..."
return f"{log}\n{self.notificacao.enviar(mensagem)}"
# Decorator para prioridade
class PrioridadeDecorator(NotificacaoDecorator):
def enviar(self, mensagem):
mensagem_prioritaria = f"[PRIORITÁRIO] {mensagem}"
return self.notificacao.enviar(mensagem_prioritaria)
# Uso do Decorator
notificacao = NotificacaoEmail()
notificacao_com_log = LogDecorator(notificacao)
notificacao_prioritaria = PrioridadeDecorator(notificacao_com_log)
mensagem = "Sistema fora do ar!"
print(notificacao_prioritaria.enviar(mensagem))
A classe Notificacao
é a base para diferentes tipos de notificações, com a classe NotificacaoEmail
e NotificacaoSMS
implementando a lógica de envio de mensagens específicas.
O NotificacaoDecorator
é uma classe base para os decoradores, permitindo modificar o comportamento das notificações sem alterar suas classes concretas.
O LogDecorator
adiciona a funcionalidade de registrar um log antes de enviar a notificação, enquanto o PrioridadeDecorator
adiciona uma tag de prioridade à mensagem.
No uso final, a notificação passa por ambos os decoradores, registrando o log e destacando a prioridade antes de ser enviada.
Utilizando esse padrão, o sistema de notificações se torna mais modular, flexível e fácil de estender, evitando a explosão de classes e permitindo que você combine funcionalidades conforme necessário.
Facade
Suponha que você precisa lidar com um sistema complexo que envolve vários subsistemas para realizar tarefas, como gerenciar pedidos em um e-commerce. Esses subsistemas podem incluir estoques, processamento de pagamentos e envio de notificações.
Sem um padrão estruturado, o código pode se tornar disperso, com chamadas diretas a esses subsistemas em várias partes do sistema.
Por exemplo, você poderia ter algo como:
class Estoque:
def verificar_estoque(self, produto):
return f"Estoque do produto {produto} verificado."
class Pagamento:
def processar_pagamento(self, valor):
return f"Pagamento de R${valor:.2f} processado."
class Notificacao:
def enviar_notificacao(self, mensagem):
return f"Notificação enviada: {mensagem}"
# Uso direto dos subsistemas
estoque = Estoque()
pagamento = Pagamento()
notificacao = Notificacao()
produto = "Notebook"
valor = 3000.00
print(estoque.verificar_estoque(produto))
print(pagamento.processar_pagamento(valor))
print(notificacao.enviar_notificacao(f"Pedido do produto {produto} realizado com sucesso!"))
No código acima, o cliente precisa interagir com cada subsistema individualmente, o que torna o código mais difícil de gerenciar e mais propenso a erros.
O código mostra a utilização de três subsistemas (Estoque
, Pagamento
e Notificacao
) para gerenciar o processo de um pedido.
A classe Estoque
tem o método verificar_estoque
, que simula a verificação do estoque de um produto. Já a classe Pagamento
processa o pagamento de um valor com o método processar_pagamento
.
A classe Notificacao
envia uma mensagem de notificação com o método enviar_notificacao
.
O uso direto desses subsistemas implica que cada parte do sistema é chamada separadamente para realizar suas funções, o que pode resultar em um código mais fragmentado e difícil de manter à medida que o sistema cresce, já que as interações entre subsistemas são explícitas e não centralizadas.
No desenvolvimento de sistemas, frequentemente precisamos simplificar a interação com múltiplos subsistemas, fornecendo uma interface única para encapsular a complexidade.
O padrão Facade, ou fachada, resolve esse problema ao oferecer uma única interface que coordena as interações entre os subsistemas.
Com o Facade, você pode criar uma classe que encapsula os subsistemas, simplificando sua utilização:
# Subsystems
class Estoque:
def verificar_estoque(self, produto):
return f"Estoque do produto {produto} verificado."
class Pagamento:
def processar_pagamento(self, valor):
return f"Pagamento de R${valor:.2f} processado."
class Notificacao:
def enviar_notificacao(self, mensagem):
return f"Notificação enviada: {mensagem}"
# Facade
class GerenciadorDePedidos:
def __init__(self):
self.estoque = Estoque()
self.pagamento = Pagamento()
self.notificacao = Notificacao()
def realizar_pedido(self, produto, valor):
resultado_estoque = self.estoque.verificar_estoque(produto)
resultado_pagamento = self.pagamento.processar_pagamento(valor)
resultado_notificacao = self.notificacao.enviar_notificacao(
f"Pedido do produto {produto} realizado com sucesso!"
)
return f"{resultado_estoque}\n{resultado_pagamento}\n{resultado_notificacao}"
# Uso do Facade
gerenciador = GerenciadorDePedidos()
resultado = gerenciador.realizar_pedido("Notebook", 3000.00)
print(resultado)
O código acima define três subsistemas: Estoque
, Pagamento
e Notificacao
, responsáveis por funções específicas, como verificar estoque, processar pagamentos e enviar notificações, respectivamente.
A classe GerenciadorDePedidos
funciona como a fachada, reunindo e centralizando as operações desses subsistemas.
O método realizar_pedido
da fachada chama os métodos de cada subsistema para executar as etapas necessárias do processo de pedido (verificar estoque, processar pagamento e enviar a notificação).
Isso simplifica o uso do sistema, pois o cliente interage somente com a fachada, sem precisar se preocupar com a complexidade dos subsistemas internos.

Padrões Comportamentais
Imagine uma equipe organizando um evento para dezenas de pessoas, onde cada membro tem uma responsabilidade específica: um cuida do local, outro do buffet, e outro da lista de convidados.
Se cada um souber claramente o que precisa fazer e tiver uma forma eficiente de se comunicar, o evento flui sem problemas.
Porém, se não houver um planejamento estruturado e cada pessoa começar a agir de forma independente ou a assumir tarefas de outros sem avisar, informações podem se perder, convidados podem ser esquecidos, e o evento pode acabar sendo um caos.
No desenvolvimento de software, é exatamente assim: cada componente do sistema precisa ter um papel bem definido e interagir de forma coordenada para evitar confusões e falhas no funcionamento.
A interação entre os objetos e as responsabilidades de cada componente desempenham um papel fundamental para a eficiência e a clareza do sistema.
Sem um método organizado, a comunicação entre diferentes partes pode se tornar confusa, resultando em código difícil de entender, manter e escalar.
Por exemplo, a falta de um padrão para gerenciar como os objetos interagem pode levar a dependências desnecessárias e comportamentos imprevisíveis, complicando a adição de novas funcionalidades ou alterações nos requisitos existentes.
Além disso, o código pode acabar com lógica duplicada ou dispersa, aumentando o risco de erros.
Os padrões comportamentais surgem como uma solução para organizar a comunicação e a distribuição de responsabilidades entre objetos, garantindo que cada componente desempenhe sua função de maneira clara e eficiente.
Esses padrões se concentram no fluxo de controle e na maneira como as interações são orquestradas dentro do sistema.
Por meio dos padrões comportamentais, os desenvolvedores podem criar sistemas flexíveis e robustos, nos quais os objetos colaboram de forma previsível e independente. Isso reduz o acoplamento entre componentes e promove um design mais modular, facilitando a manutenção e a extensão do sistema.
Cada padrão comportamental aborda um problema específico de interação ou responsabilidade, fornecendo uma solução que combina clareza, escalabilidade e reutilização.
A seguir, exploraremos os principais padrões comportamentais, mostrando como eles podem ser aplicados para melhorar a interação entre componentes e promover um design mais coeso e sustentável.
Command
Suponha que você está desenvolvendo um sistema para um editor de texto que permite realizar ações como copiar, colar, desfazer e refazer.
Cada ação precisa ser tratada isoladamente para facilitar sua execução, desfazer ou armazenar em uma lista de histórico.
Sem uma diretriz estruturada, você pode acabar com código que acopla diretamente a lógica das ações aos componentes da interface, dificultando adicionar novos comandos ou modificar os existentes:
class EditorDeTexto:
def __init__(self):
self.conteudo = ""
def copiar(self):
return self.conteudo
def colar(self, texto):
self.conteudo += texto
# Uso direto e acoplado
editor = EditorDeTexto()
editor.colar("Olá, mundo!")
print(editor.copiar()) # Saída: Olá, mundo!
O código acima apresenta um sistema acoplado onde as operações de copiar e colar texto estão diretamente ligadas à classe EditorDeTexto
.
A classe contém um atributo conteudo
, que armazena o texto, e dois métodos: copiar()
, que retorna o conteúdo atual, e colar(texto)
, que adiciona o texto ao conteúdo existente.
No uso direto, a instância editor
recebe um texto por meio do método colar()
, e o método copiar()
retorna esse conteúdo.
O código que executa as ações está acoplado diretamente à lógica do editor, dificultando a introdução de um histórico de comandos ou a implementação de novas funcionalidades, como desfazer.
No desenvolvimento de sistemas, é comum precisarmos encapsular uma solicitação como um objeto, permitindo o armazenamento, log e desfazer dessas solicitações.
O padrão Command (ou comando) resolve esse problema ao encapsular cada operação em uma classe separada, promovendo um design mais flexível e extensível.
Seguindo esse padrão, você cria uma interface para os comandos e encapsula a lógica das ações:
# Comando abstrato
class Comando:
def executar(self):
pass
# Implementações de comandos
class ComandoColar(Comando):
def __init__(self, editor, texto):
self.editor = editor
self.texto = texto
def executar(self):
self.editor.conteudo += self.texto
class ComandoCopiar(Comando):
def __init__(self, editor):
self.editor = editor
def executar(self):
return self.editor.conteudo
# Classe principal
class EditorDeTexto:
def __init__(self):
self.conteudo = ""
self.historico = []
def executar_comando(self, comando):
resultado = comando.executar()
self.historico.append(comando)
return resultado
# Uso do Command
editor = EditorDeTexto()
colar = ComandoColar(editor, "Olá, mundo!")
editor.executar_comando(colar)
print(editor.conteudo) # Saída: Olá, mundo!
copiar = ComandoCopiar(editor)
print(editor.executar_comando(copiar)) # Saída: Olá, mundo!
O código desacopla a lógica das ações do editor de texto, tornando os comandos reutilizáveis e flexíveis.
A classe abstrata Comando
define a estrutura básica para qualquer comando. ComandoColar
recebe uma referência ao editor e adiciona um texto ao conteúdo, enquanto ComandoCopiar
retorna o conteúdo atual.
A classe EditorDeTexto
mantém um histórico de comandos e executa cada um por meio do método executar_comando()
.
No uso, um comando de colar adiciona o texto ao editor, e um comando de copiar retorna esse conteúdo.
Assim, você pode adicionar novos comandos sem alterar o código existente, implementar facilmente funcionalidades como desfazer e refazer e gerenciar o histórico de ações. Esse padrão promove um design modular, limpo e preparado para evolução.
Iterator
Ao desenvolver um sistema para manipular coleções de dados, como listas ou conjuntos, surge a necessidade de percorrê-los de maneira uniforme.
Em um gerenciamento de produtos de supermercado, por exemplo, é fundamental acessar os itens individualmente para exibir informações ou realizar cálculos, garantindo um fluxo organizado e consistente na navegação dos dados.
Sem uma abordagem organizada, o código para percorrer essas coleções pode ficar acoplado às estruturas internas de dados, dificultando modificar ou substituir essas estruturas no futuro:
class ListaDeProdutos:
def __init__(self, produtos):
self.produtos = produtos
# Percorrendo diretamente a estrutura de dados
produtos = ListaDeProdutos(["Produto A", "Produto B", "Produto C"])
for i in range(len(produtos.produtos)):
print(produtos.produtos[i])
O código define uma classe ListaDeProdutos
, que armazena uma lista de produtos. A iteração sobre os produtos é feita diretamente acessando a lista interna produtos.produtos
com um loop for
, utilizando range()
para percorrer os índices.
Nesse caso, o código está diretamente ligado à implementação da lista. Isso dificulta reutilizar o mesmo algoritmo de iteração para outras estruturas de dados, como conjuntos ou árvores.
No desenvolvimento de sistemas, frequentemente precisamos percorrer coleções de forma padronizada, independentemente de como estão implementadas internamente.
O padrão Iterator (ou iterador) oferece uma abordagem consistente para acessar os elementos de uma coleção, mantendo a estrutura interna da coleção oculta.
Seguindo esse padrão, você cria um iterador que abstrai a lógica de iteração:
# Classe de coleção
class ListaDeProdutos:
def __init__(self, produtos):
self.produtos = produtos
def __iter__(self):
return IteradorLista(self.produtos)
# Implementação do iterador
class IteradorLista:
def __init__(self, produtos):
self.produtos = produtos
self.indice = 0
def __iter__(self):
return self
def __next__(self):
if self.indice < len(self.produtos):
produto = self.produtos[self.indice]
self.indice += 1
return produto
raise StopIteration
# Uso do Iterator
produtos = ListaDeProdutos(["Produto A", "Produto B", "Produto C"])
for produto in produtos:
print(produto) # Saídas: Produto A, Produto B, Produto C
A classe ListaDeProdutos
representa a coleção e define o método __iter__()
, que retorna um objeto IteradorLista
, responsável por gerenciar a iteração.
O IteradorLista
mantém um índice para rastrear a posição atual e implementa __next__()
, que retorna o próximo produto ou levanta StopIteration
ao final da lista.
Isso permite que a classe ListaDeProdutos
seja usada diretamente em loops for
, sem expor sua estrutura interna, tornando o código mais flexível e desacoplado.
Com isso, a lógica de iteração é encapsulada em uma classe específica, permitindo que diferentes coleções sejam percorridas de forma uniforme.
Isso promove um design mais modular, facilita a substituição de estruturas de dados e torna o código mais legível e reutilizável.
Observer
Considere que você está desenvolvendo um sistema onde vários componentes precisam ser notificados sobre mudanças em um objeto central, como um sistema de monitoramento de temperatura.
O objeto central, nesse caso, poderia ser um termômetro, e os componentes interessados poderiam ser diferentes displays ou alarmes que devem ser atualizados sempre que a temperatura mudar.
Sem um padrão estruturado, o código para gerenciar essas notificações pode se tornar desorganizado, com cada componente verificando e atualizando manualmente os outros sempre que há uma mudança. Isso pode resultar em código difícil de manter e escalar.
Por exemplo, você poderia ter algo assim:
class Termometro:
def __init__(self):
self.temperatura = 0
self.display = None
self.alarme = None
def registrar_display(self, display):
self.display = display
def registrar_alarme(self, alarme):
self.alarme = alarme
def set_temperatura(self, temperatura):
self.temperatura = temperatura
if self.display:
self.display.atualizar(self.temperatura)
if self.alarme:
self.alarme.verificar_alarme(self.temperatura)
class Display:
def atualizar(self, temperatura):
print(f"Temperatura atual: {temperatura}°C")
class Alarme:
def verificar_alarme(self, temperatura):
if temperatura > 30:
print("Alerta! Temperatura alta!")
# Uso direto e acoplado
termometro = Termometro()
display = Display()
alarme = Alarme()
termometro.registrar_display(display)
termometro.registrar_alarme(alarme)
termometro.set_temperatura(32) # Saída: Temperatura atual: 32°C \n Alerta! Temperatura alta!
Aqui, o Termometro
precisa saber sobre todos os seus observadores (que, neste caso, são o Display
e o Alarme
) e atualizar cada um manualmente, o que pode dificultar a adição de novos tipos de observadores no futuro.
O padrão Observer (ou observador) resolve esse problema ao criar uma relação de dependência entre o objeto central – o sujeito – e os objetos dependentes – os observadores –, onde o sujeito notifica automaticamente os observadores sempre que ocorre uma mudança.
No padrão Observer, você não precisa de uma lógica manual de notificação e sempre que o estado muda os observadores são notificados:
# Observador
class Observador:
def atualizar(self, temperatura):
pass
# Sujeito
class Termometro:
def __init__(self):
self.temperatura = 0
self.observadores = []
def adicionar_observador(self, observador):
self.observadores.append(observador)
def remover_observador(self, observador):
self.observadores.remove(observador)
def notificar_observadores(self):
for observador in self.observadores:
observador.atualizar(self.temperatura)
def set_temperatura(self, temperatura):
self.temperatura = temperatura
self.notificar_observadores()
# Observadores concretos
class Display(Observador):
def atualizar(self, temperatura):
print(f"Temperatura atual: {temperatura}°C")
class Alarme(Observador):
def atualizar(self, temperatura):
if temperatura > 30:
print("Alerta! Temperatura alta!")
# Uso do Observer
termometro = Termometro()
display = Display()
alarme = Alarme()
termometro.adicionar_observador(display)
termometro.adicionar_observador(alarme)
termometro.set_temperatura(32) # Saída: Temperatura atual: 32°C \n Alerta! Temperatura alta!
O código implementa um observador, onde um objeto, nesse caso o Termometro
, notifica automaticamente seus observadores, Display
e Alarme
, sempre que sua temperatura muda.
A classe Observador
define uma interface com o método atualizar()
, que os observadores concretos implementam para reagir às mudanças de temperatura.
O Termometro
mantém uma lista de observadores e os notifica ao chamar notificar_observadores()
.
O Display
apenas exibe a temperatura, enquanto o Alarme
verifica se ela ultrapassa 30°C e emite um alerta. Assim, ao definir a temperatura em um valor superior, ambos os observadores são atualizados automaticamente.
A lógica de notificação é desacoplada dos observadores, e o sujeito não precisa conhecer detalhes sobre os objetos que o observam.
Isso facilita a adição de novos tipos de observadores e melhora a escalabilidade do sistema. O padrão promove um design mais flexível e permite uma maior reutilização do código.
State
Um sistema precisa gerenciar o comportamento de um objeto conforme seu estado interno muda. Pense em um reprodutor de mídia, que pode alternar entre diferentes estados, como pausado
, reproduzindo
e parado
.
Cada um desses estados exige ações específicas: enquanto pausado, o reprodutor só pode continuar a reprodução após retornar ao estado reproduzindo
.
Já no estado parado
, o comportamento muda, exigindo uma nova abordagem para iniciar a reprodução.
Sem um padrão de organização, o código que gerencia essas mudanças de estado pode se tornar um amontoado de lógica condicional espalhada por várias partes do sistema, dificultando a manutenção e a escalabilidade. Por exemplo:
class Reprodutor:
def __init__(self):
self.estado = "parado"
def pressionar_botao(self):
if self.estado == "parado":
self.estado = "reproduzindo"
print("Iniciando reprodução...")
elif self.estado == "reproduzindo":
self.estado = "pausado"
print("Reprodução pausada.")
elif self.estado == "pausado":
self.estado = "reproduzindo"
print("Retomando reprodução.")
# Uso direto e com lógica condicional
reprodutor = Reprodutor()
reprodutor.pressionar_botao() # Saída: Iniciando reprodução...
reprodutor.pressionar_botao() # Saída: Reprodução pausada.
reprodutor.pressionar_botao() # Saída: Retomando reprodução...
O código define um reprodutor de mídia com os três estados. O método pressionar_botao()
alterna entre esses estados com base no atual, usando o condicional.
Se estiver "parado"
, começa a reprodução; se já estiver "reproduzindo"
, pausa; e se estiver "pausado"
, retoma a reprodução.
O sistema está diretamente acoplado à lógica de transição de estados, tornando o código difícil de escalar e de manter, especialmente se forem necessários mais estados ou comportamentos.
O padrão State (ou estado) resolve esse problema ao encapsular os diferentes comportamentos de estado em classes separadas, permitindo que o objeto altere seu comportamento conforme muda de estado sem precisar de múltiplas verificações condicionais.
Seguindo este padrão, você pode modelar cada estado como uma classe separada e permitir que o objeto delegue a responsabilidade de agir ao estado atual:
# Estados concretos
class Estado:
def pressionar_botao(self, reprodutor):
pass
class Parado(Estado):
def pressionar_botao(self, reprodutor):
reprodutor.estado = Reproduzindo()
print("Iniciando reprodução...")
class Reproduzindo(Estado):
def pressionar_botao(self, reprodutor):
reprodutor.estado = Pausado()
print("Reprodução pausada.")
class Pausado(Estado):
def pressionar_botao(self, reprodutor):
reprodutor.estado = Reproduzindo()
print("Retomando reprodução...")
# Classe Contexto
class Reprodutor:
def __init__(self):
self.estado = Parado()
def pressionar_botao(self):
self.estado.pressionar_botao(self)
# Uso do State
reprodutor = Reprodutor()
reprodutor.pressionar_botao() # Saída: Iniciando reprodução...
reprodutor.pressionar_botao() # Saída: Reprodução pausada.
reprodutor.pressionar_botao() # Saída: Retomando reprodução...
O código separa os comportamentos do reprodutor de mídia em classes específicas para cada estado: Parado
, Reproduzindo
e Pausado
.
A classe Reprodutor
mantém um estado atual e delega a lógica de mudança para a classe correspondente. Cada estado sabe para qual outro deve transicionar ao pressionar o botão, evitando condicionais dentro do reprodutor.
Isso melhora a legibilidade do código e permite a fácil adição de novos estados e comportamentos sem afetar outras partes do sistema.
Strategy
Você está desenvolvendo um sistema de recomendação de conteúdo, onde o algoritmo de recomendação pode variar dependendo de fatores como preferências do usuário, tipo de conteúdo ou contexto da recomendação.
Sem um padrão estruturado, você poderia criar uma lógica condicional diretamente dentro da classe de recomendação para escolher qual algoritmo usar, o que faria o código ficar pesado, difícil de manter e escalar:
class Recomendacao:
def __init__(self, tipo):
self.tipo = tipo
def recomendar(self, usuario):
if self.tipo == "colaborativa":
print("Recomendando com filtragem colaborativa...")
elif self.tipo == "conteudo":
print("Recomendando com filtragem baseada em conteúdo...")
elif self.tipo == "hibrida":
print("Recomendando com algoritmo híbrido...")
# Uso direto e com lógica condicional
recomendacao = Recomendacao("colaborativa")
recomendacao.recomendar("usuario1") # Saída: Recomendando com filtragem colaborativa...
Esse código implementa uma classe Recomendacao
que permite recomendar algo a um usuário baseado no tipo de algoritmo escolhido.
O método recomendar
verifica o tipo e imprime a mensagem correspondente ao tipo de recomendação.
Nesse código, o sistema está diretamente acoplado à lógica dos diferentes algoritmos de recomendação, o que dificulta adicionar novos algoritmos ou fazer modificações sem alterar o código central. Isso prejudica a escalabilidade e a manutenção do sistema.
O padrão Strategy (ou estratégia) resolve esse problema ao permitir que você defina diferentes algoritmos de recomendação como classes separadas e forneça uma interface comum para elas.
Dessa forma, o sistema pode escolher qual algoritmo usar em tempo de execução sem precisar de lógica condicional espalhada.
Seguindo o padrão, você encapsula os diferentes algoritmos em classes distintas e permite que o contexto, como o tipo de recomendação, altere o comportamento sem modificar a estrutura principal do código:
# Estratégias de recomendação
class RecomendacaoColaborativa:
def recomendar(self, usuario):
print("Recomendando com filtragem colaborativa...")
class RecomendacaoConteudo:
def recomendar(self, usuario):
print("Recomendando com filtragem baseada em conteúdo...")
class RecomendacaoHibrida:
def recomendar(self, usuario):
print("Recomendando com algoritmo híbrido...")
# Contexto de recomendação
class SistemaRecomendacao:
def __init__(self, estrategia):
self.estrategia = estrategia
def recomendar(self, usuario):
self.estrategia.recomendar(usuario)
# Uso do Strategy
recomendacao_colaborativa = SistemaRecomendacao(RecomendacaoColaborativa())
recomendacao_colaborativa.recomendar("usuario1") # Saída: Recomendando com filtragem colaborativa...
recomendacao_conteudo = SistemaRecomendacao(RecomendacaoConteudo())
recomendacao_conteudo.recomendar("usuario2") # Saída: Recomendando com filtragem baseada em conteúdo...
recomendacao_hibrida = SistemaRecomendacao(RecomendacaoHibrida())
recomendacao_hibrida.recomendar("usuario3") # Saída: Recomendando com algoritmo híbrido...
No exemplo acima, cada algoritmo de recomendação é encapsulado em uma classe separada, implementando uma interface comum.
A classe SistemaRecomendacao
permite alterar a estratégia de recomendação em tempo de execução, sem precisar modificar a lógica interna da classe principal.
Esse padrão promove um design mais limpo, flexível e escalável, permitindo que você altere facilmente o comportamento do sistema sem a necessidade de modificar o código que utiliza essas estratégias.
Template Method
Um sistema está sendo desenvolvido para validar documentos como RG e CPF. A validação de cada tipo de documento segue um processo comum: checar a formatação e garantir que o número seja válido.
No entanto, as verificações detalhadas diferem de acordo com o tipo de documento.
Sem um padrão estruturado, o código pode ficar repetitivo e difícil de manter:
class ValidarRG:
def validar(self, documento):
print(f"Verificando formato do RG {documento}...")
print(f"Validando número do RG {documento}...")
class ValidarCPF:
def validar(self, documento):
print(f"Verificando formato do CPF {documento}...")
print(f"Validando número do CPF {documento}...")
O código define duas classes responsáveis por validar documentos: ValidarRG
e ValidarCPF
. Ambas têm um método validar
que recebe um número de documento como parâmetro e realiza duas ações: primeiro, verifica o formato do documento e, em seguida, valida o número do documento.
A única diferença entre as classes é que uma valida o formato e o número do RG, enquanto a outra faz o mesmo para o CPF.
Com o padrão Template, você pode definir uma estrutura geral para a validação e permitir que as subclasses implementem as verificações específicas:
from abc import ABC, abstractmethod
class ValidadorDocumento(ABC):
def validar(self, documento):
self.verificar_formato(documento)
self.validar_numero(documento)
@abstractmethod
def verificar_formato(self, documento):
pass
@abstractmethod
def validar_numero(self, documento):
pass
class ValidarRG(ValidadorDocumento):
def verificar_formato(self, documento):
print(f"Verificando formato do RG {documento}...")
def validar_numero(self, documento):
print(f"Validando número do RG {documento}...")
class ValidarCPF(ValidadorDocumento):
def verificar_formato(self, documento):
print(f"Verificando formato do CPF {documento}...")
def validar_numero(self, documento):
print(f"Validando número do CPF {documento}...")
# Uso do Template
validar_rg = ValidarRG()
validar_rg.validar("12.345.678-9")
validar_cpf = ValidarCPF()
validar_cpf.validar("123.456.789-00")
A classe ValidadorDocumento
é uma classe abstrata que define o esqueleto do método validar
, que chama dois métodos abstratos: verificar_formato
e validar_numero
.
As subclasses, como ValidarRG
e ValidarCPF
, devem implementar esses métodos abstratos conforme a lógica específica de cada tipo de documento.
Ao criar objetos dessas subclasses e chamar o método validar
, o sistema executa a sequência de passos definida na classe base, com comportamentos específicos para RG e CPF, como verificar o formato e validar o número do documento.
Dessa maneira, o Template permite que você defina a estrutura do processo de validação e delegue as etapas específicas para as subclasses.
Isso torna o código mais organizado, fácil de entender e de estender para novos tipos de documentos.
Quando utilizar Design Patterns em Python?
Design patterns podem ser utilizados para melhorar a estrutura do código, tornando-o mais legível, escalável e fácil de manter. Você deve considerar usá-los quando enfrentar desafios de:
Complexidade: Quando um sistema envolve muitas regras e variáveis, o código pode ficar confuso e difícil de entender. Nesse caso, utilizar esses padrões pode ajudar a organizar o código, separando responsabilidades e tornando o sistema mais modular e fácil de entender.
Flexibilidade para Mudanças: Sistemas que precisam de alterações frequentes podem se beneficiar de padrões de design que permitem mudanças sem afetar grandes partes do código. Isso torna o sistema mais adaptável a novas funcionalidades ou ajustes nas regras de negócio.
Escalabilidade: À medida que o sistema cresce, ele precisa lidar com mais dados, mais usuários ou mais tipos de documentos. Esses padrões podem ajudar a escalar o sistema eficientemente, dividindo as tarefas em componentes menores e mais gerenciáveis, além de facilitar a adição de novas funcionalidades.
Manutenção: Em sistemas atualizados regularmente, é importante garantir que o código seja fácil de manter e modificar. Padrões de design, como a separação de preocupações, permitem que mudanças sejam feitas sem a necessidade de refazer grandes partes do sistema, facilitando a manutenção a longo prazo.
Integração: Quando o sistema precisa interagir com outros sistemas ou APIs, os padrões podem ser usados para simplificar a comunicação e esconder a complexidade dessas integrações, tornando o código mais limpo e com menos dependências externas.
Redundância: Em sistemas grandes, pode haver repetição de lógica em várias partes do código. Podemos usar padrões para centralizar funções comuns e garantir que o código seja mais eficiente e menos propenso a erros.
No entanto, é importante evitar usar design patterns em excesso ou em situações simples, onde o código já é claro e direto, como validar um CPF ou calcular um desconto fixo.
Implementar um padrão sem necessidade pode adicionar complexidade desnecessária, tornando o código mais difícil de entender e manter. Quando o problema é simples e a solução direta é eficiente, é melhor não usar um padrão de design.
Sendo assim, é importante usar design patterns quando eles agregarem valor ao seu código, mas evitar usá-los somente por convenção ou em problemas que não exigem tal estrutura.
Conclusão
Exploramos diversos tipos de padrões de design em Python, destacando como cada um pode ser aplicado para resolver problemas comuns eficientemente e estruturada.
Compreender quando e como usar esses padrões pode melhorar significativamente a organização e manutenção do código, especialmente em projetos mais complexos.
Para um aprendizado mais aprofundado na linguagem Python e boas práticas como padrões de projeto, recomendamos os seguintes conteúdos: