Padrões de Projeto com Python
Tags: python
Published: 2021-05-07 02:44:42

# 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:
```python
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:
```python
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:

```python
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:
```python
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:
```python
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. 

```python
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:
```python
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.

```python
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:
```python
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:
```python
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.

```python
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.

```python
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. 
```python
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.

```python
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.
```python
    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.
```python
class Guerreiro(ABC):

    @abstractmethod
    def implementacao_listar_habilidades(self):
        pass
```
Em seguida vamos criar duas classes concretas que representam guerreiros de RPGs diferentes.
```python
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.
```python
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.

```python
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.