Padrões de Projeto com Python
Antônio Ricart
Posted on May 7, 2021
Padrões de Projeto
Nesse artigo vamos abordar diversos padrões de projetos aplicados à linguagem python, e buscar entender seu funcionamento e suas particularidades.
Padrões Comportamentais
Padrões comportamentais tendem a determinar o comportamento de objetos, classes e a forma como esses se comunicam.
Padrão Stategy
O objetivo desse padrão é evitar que uma classe possua maneiras diferentes de lidar com algo específico, dividindo cada maneira e criando uma classe individual para cada estratégia.
Em python o melhor jeito de implementar esse padrão é usando o módulo abc(Abstract Base Classes) que nos fornece funcionalidades, classes e decorators para a criação de classes e métodos abstratos.
Para importar o essencial para a construção de classes abstratas fazemos da seguinte maneira:
from abc import ABC, abstractmethod
Para um exemplo de código, vamos imaginar um sistema que armazene livros em uma biblioteca, e a biblioteca deve fornecer uma lista ordenada pelo título da obra, autor ou o isbn. Nossa classe biblioteca poderia ser implementada da seguinte maneira:
class Biblioteca:
def __init__(self, livros: List[Livro]):
self.livros = livros
def ordena_livros(self, campo_de_ordenacao: str):
if len(self.livros) == 0:
# Lógica para quando a lista de livros estiver vazia
if campo_de_ordenacao == 'titulo':
# Lógica para ordenar os livros por título
elif campo_de_ordenacao == 'isbn':
# Lógica para ordenar os livros por isbn
elif campo_de_ordenacao == 'autor':
# Lógica para ordenar os livros por autor
Mas o exemplo acima não é bom, podemos dividir as maneiras como a lista de livros é ordenada. Nesse caso podemos gerar uma classe abstrata para a estratégia de ordenação que ficaria mais ou menos assim:
class EstrategiaDeOrdenacao(ABC):
@abstractmethod
def ordena_livros(self, lista_livros: Lista[Livro]):
pass
class EstrategiaDeOrdenacaoPorAutor(EstrategiaDeOrdenacao):
def ordena_livros(self, lista_livros: Lista[Livro]):
# Lógica para ordenar lista de livros pelo autor
A classe Biblioteca então poderia ser reescrita de uma maneira muito mais elegante da seguinte maneira:
class Biblioteca:
def __init__(self, livros: List[Livro], metodo_de_ordenacao: EstrategiaDeOrdenacao):
self.livros = livros
self.metodo_de_ordenacao = metodo_de_ordenacao
def ordena_livros(self):
if len(self.livros) == 0:
# Lógica para quando a lista de livros estiver vazia
return self.metodo_de_ordenacao.ordena_livros(self.livros)
Desse jeito, não importa que o método de ordenação seja baseado no autor, título ou qualquer outra coisa, pois a estratégia para ordenar a lista de livros foi abstraída.
Padrão Command
O objetivo desse padrão é diminuir o nível de acoplamento entre classes que se relacionam entre si de maneiras muito distintas. O que o padrão propõe é a criação de uma camada de comando única para todas as classes, mas que seja implementado individualmente para cada uma destas.
Num cenário em que estivesse sendo desenvolvido um jogo, no qual o jogador, através de um cliente, pudesse consertar objetos e/ou curar outros personagens, poderíamos representar tais ações como a classe a seguir que invoca cada ação:
class Invocador:
def __init__(self, item: Equipamento, alvo: Personagem):
self.item = item
self.alvo = alvo
def ativar(self):
if self.item is not None:
self.item.set_integridade(True)
if self.alvo is not None:
self.alvo.cura(20)
O problema dessa abordagem é que gera um acoplamento muito grande entre a classe que invoca as ações, e as classes que lidam com as ações. A medida que o jogo fosse sendo atualizado, mais ações seriam adicionadas e a classe invocador precisaria ser modificada para lidar com elas, mas isso geraria mais acoplamento ainda.
Para solucionar isso aplicando o padrão Command podemos criar uma classe abstrata para todas as ações, e fazer com que cada ação seja uma classe que implementa o método executar.
class Comando(ABC):
@abstractmethod
def executar(self):
pass
Dessa forma todos os atributos que precisamos ficam armazenados nas classes de comando, garantindo que o acoplamento seja o menor possível:
class CuraPelasMaos(Comando):
def __init__(self, alvo: Personagem, valor_cura: int = 20):
self.alvo = alvo
self.valor_cura = valor_cura
def executar(self):
self.alvo.cura(self.valor_cura)
class Consertar(Comando):
def __init__(self, item: Equipamento):
self.item = item
def executar(self):
self.item.set_integridade(True)
Desse modo, podemos adequar a nossa classe invocador para receber uma lista de comandos, ao invés de modificar a classe para lidar com cada ação que pode ser executada.
class Invocador:
def __init__(self):
self.lista_de_comandos: List[Comando] = []
def add_comando(self, comando: Comando):
self.lista_de_comandos.append(comando)
def rem_comando(self, comando: Comando):
self.lista_de_comandos.remove(comando)
def ativar(self):
for comando in self.lista_de_comandos:
comando.executar()
Por fim temos um invocador que não precisa saber nada sobre os comandos que executa, garantindo um nível de acoplamento ideal para termos uma boa escalabilidade. Caso o nosso jogador possa executar uma ação que quebra objetos ou causa dano aos inimigos, tudo que precisamos fazer é adicionar uma nova classe de comando e adicionar a lista de comandos do invocador, tudo isso de maneira elegante.
Padrões Criacionais
Padrões criacionais tendem a determinar maneiras de se lidar com a criação de objetos, para que sistemas não sejam dependentes da criação, composição e representação de seus objetos.
Padrão Singleton
O objetivo desse padrão é garantir que no sistema exista uma única instância da classe. Isso serve, mas não apenas, para garantir centralização de responsabilidade.
Em python a maneira convencional de implementarmos o padrão singleton é usando o recurso de metaclass. Metaclass é a solução em python para quando uma classe precisa ter o conhecimento de manipular a si mesma. De maneira geral esse recurso sempre é usado por baixo dos panos, mas para o nosso padrão vamos precisar usá-lo explicitamente.
Primeiramente vamos criar nossa metaclass:
class SingletonMeta(type):
_instancias = {}
def __call__(classe, *args, **kwargs):
if classe not in classe._instancias:
instancia = super().__call__(*args, **kwargs)
classe._instancias[classe] = instancia
return classe._instancias[classe]
Basicamente o que fazemos é definir um atributo "privado", do tipo dicionário, para a nossa classe, que deve armazenar as instâncias da nossa própria classe.
Fazemos isso ao sobrescrever o método __call__
da nossa classe customizada, de modo que sempre que seja criado uma instância da classe (através da chamada '()') seja verificado no nosso dicionários se a nossa classe já existe.
Se a classe já existir, nós retornamos a instância armazenada no dicionário, caso contrário criamos nossa primeira instância, chamando o método __call__
da nossa classe mãe repassando quaisquer argumentos que recebemos (os nossos *args e **kwargs), e armazenamos no nosso dicionário.
Por fim podemos ter uma classe que tenha a SingletonMeta como metaclass garantindo que o mesmo só possua uma instância em todo o sistema. Como por exemplo, uma classe para o spooler de impressão de um escritório:
class Spooler(metaclass=SingletonMeta):
def __init__(self):
self.impressoes: List[Impressao] = []
def add_impressao(self, impressao: Impressao):
self.impressoes.append(impressao)
Agora, não importa quantas vezes seja instanciada a classe Spooler, a referência vai ser sempre para a primeira instância criada.
Padrão Prototype
O objetivo desse padrão é garantir que não haja dependência do código do sistema nas situações em que seja preciso criar cópias de um determinado objeto. Pra tal o padrão determina que cada classe deva ser responsável por implementar seu próprio método de clonagem.
class Produto:
__nome: str
__preco: float
def __init__(self):
pass
# Getters e Setters
def clonar(self):
produto = Produto()
produto.set_nome(self.__nome)
produto.set_preco(self.__preco)
return produto
Da maneira como foi implementado, não importa para outros módulos do sistema saber o que compões a classe produto, pois, caso precise de uma cópia de um objeto desse tipo, só precisa da chamada do método clonar()
.
Padrões Estruturais
Padrões estruturais dizem respeito à composição de classes e objetos, e apontam meios de como estes podem se unir para gerar estrutura maiores.
Padrão Adapter
O objetivo desse padrão é garantir que seu código não tenha dependência com uma determinada biblioteca. Para isso é proposto a criação de uma interface adaptador que padronize as chamadas de métodos, e que seja criado uma classe que adapta cada biblioteca utilizada herdando seus métodos.
Em python, para lidarmos com o conceito de interfaces, usamos classes abstrata, e a classe que herda a classe abstrata deve implementar todos os métodos abstratos. Além disso em python possuímos uma funcionalidade que não é presente em muitas outras linguagens: a herança múltipla.
Herança múltipla nada mais é do que a capacidade de uma classe herdar de duas classes mãe diferentes, herdando seus atributos e métodos. Em caso de atributos ou métodos com mesmo nome, o python desempata usando o critério de qual classe foi herdade primeiro. Com isso explicado podemos prosseguir para nosso exemplo.
Vamos imaginar um cenário em que nosso sistema precise calcular a área de um triângulo. Para isso existem várias maneiras diferentes, e encontramos duas classes que entregam o resultado seguindo por abordagens diferentes.
class Matemagica:
def area_triangulo_tradicional(self, base: float, altura: float):
# lógica para calcular área do triangulo
class Trigonomaestria:
def area_triangulo_por_angulo(self, angulo_interno: float, lado1: float, lado2: float):
# lógica para calcular área do triangulo com base no angulo interno
Se optarmos por usar uma delas diretamente, nosso código ficará dependente de uma maneira, que, no futuro, caso seja necessário mudar para a outra classe, muitas alterações precisem ser feitar no nosso código.
A solução já foi apresentada, vamos criar uma classe abstrata para adaptar o que esperamos que nossa classe, independente de qual escolheremos, faça.
class ManipuladorMatematico(ABC):
@abstractmethod
def area_triangulo(self, base: float, altura: float, angulo_interno: float, lado1: float, lado2: float):
pass
Em seguida, podemos criar uma classe que vai ser responsável por manipular cada uma das bibliotecas que utilizaremos, usando o recurso de herança múltipla podemos herdar tanto da nossa classe abstrata, quanto da classe da biblioteca.
class ManipuladorMatemagica(ManipuladorMatematico, Matemagica):
def area_triangulo(self, base: float, altura: float, angulo_interno: float, lado1: float, lado2: float):
return super().area_triangulo_tradicional(base, altura)
class ManipuladorTrigonomaestria(ManipuladorMatematico, Trigonomaestria):
def area_triangulo(self, base: float, altura: float, angulo_interno: float, lado1: float, lado2: float):
return super().area_triangulo_por_angulo(angulo_interno, lado1, lado2)
E pronto, criamos nossos próprios adaptadores, e quando for necessário calcular a área de um triângulo no nosso sistema teremos um jeito único independente de qual classe usemos.
matematica: ManipuladorMatematico = ManipuladorTrigonomaestria()
print(matematica.area_triangulo(5, 6, 45, 7, 8))
matematica: ManipuladorMatematico = ManipuladorMatemagica()
print(matematica.area_triangulo(5, 6, 45, 7, 8))
Padrão Bridge
O objetivo desse padrão é dividir uma classe em hierarquias, normalmente entre Abstração, a classe que irá delegar funções, e Implementação, as classes que irão conter as funcionalidades específicas de uma parte do todo.
Para o nosso exemplo, vamos pensar em um sistema que organiza regras de um jogo de RPG de mesa. Pensando num nicho de RPG medieval, é comum jogos de RPG possuírem classes, ou caminhos, para o personagem, mas em cada jogo de rpg isso funciona de maneira diferente.
Com os recursos já apresentados até agora nesse artigo, podemos implementar esse padrão em python da seguinte maneira.
Primeiro criamos a classe de implementação, nesse exemplo vamos usar o guerreiro (o caminho mais comum em RPGs de mesa), e definir sua representação como uma classe abstrata, pois independente do jogo de RPG todos eles terão habilidades a serem listadas.
class Guerreiro(ABC):
@abstractmethod
def implementacao_listar_habilidades(self):
pass
Em seguida vamos criar duas classes concretas que representam guerreiros de RPGs diferentes.
class GuerreiroT20(Guerreiro):
def implementacao_listar_habilidades(self):
print("Ataque Especial")
class GuerreiroDnD5E(Guerreiro):
def implementacao_listar_habilidades(self):
print("Estilo de Luta e Retomar o Fôlego")
Com as implementações concretas feitas, podemos criar nossa classe de abstração e criar a ponte de fato.
class GuerreiroAbstrato(ABC):
def __init__(self, implementacao: Guerreiro):
self.implementacao = implementacao
def listar_habilidades(self):
self.implementacao.implementacao_listar_habilidades()
@abstractmethod
def usar_habilidade(self):
pass
O nosso GuerreiroAbstrato possui uma referência para uma implementaçaoo de guerreiro, qualquer que seja ela. No exemplo a seguir, na classe GuerreiroIniciante temos a abstração do uso de habilidades dos personagens pra qualquer RPG, enquanto a classe GuerreiroNPC abstrai que todos os NPCs não podem usar habilidades no jogo.
class GuerreiroIniciante(GuerreiroAbstrato):
def usar_habilidade(self):
print("Usando: ", end="")
self.listar_habilidades()
class GuerreiroNPC(GuerreiroAbstrato):
def usar_habilidade(self):
print("NPCs não podem usar habilidades")
Com nossa implementação, a nossa representação de Guerreiro não depende do jogo de RPG utilizado para usar suas próprias habilidades.
Conclusão
Nesse artigo abordamos diversos e padrões de projetos de também diversos tipos. Espero ter alcançado o objetivo de conseguir explicar o funcionamento e as particularidades dos padrões implementados na linguagem Python. Até a próxima.
Posted on May 7, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.