Classes
Princípio da Responsabilidade Única (SRP)
Robert C. Martin escreve:
Uma classe deve ter apenas um motivo para mudar.
Os "motivos para mudar" são, essencialmente, as responsabilidades gerenciadas por uma classe ou função.
No exemplo a seguir, criamos um elemento HTML que representa um comentário com a versão do documento:
Ruim
from importlib import metadata
class VersionCommentElement:
"""Um elemento que renderiza um comentário HTML com o número da versão do programa."""
def get_version(self) -> str:
"""Obter a versão do pacote"""
return metadata.version("pip")
def render(self) -> None:
print(f'<!-- Versão: {self.get_version()} -->')
VersionCommentElement().render()
Esta classe tem duas responsabilidades:
- Recuperar o número da versão do pacote Python
- Renderizar-se como um elemento HTML
Qualquer alteração em uma dessas responsabilidades pode afetar a outra.
Podemos reescrever a classe e separar essas responsabilidades:
Bom
from importlib import metadata
def get_version(pkg_name: str) -> str:
"""Recuperar a versão de um determinado pacote"""
return metadata.version(pkg_name)
class VersionCommentElement:
"""Um elemento que renderiza um comentário HTML com o número da versão do programa."""
def __init__(self, version: str):
self.version = version
def render(self) -> None:
print(f'<!-- Versão: {self.version} -->')
VersionCommentElement(get_version("pip")).render()
O resultado é que a classe agora só precisa se preocupar em renderizar a si mesma. Ela recebe o texto da versão durante a instanciação, e esse texto é gerado por uma função separada, get_version(). Alterar uma não afeta a outra, desde que o contrato entre elas permaneça o mesmo, ou seja, a função fornece uma string e o método __init__ da classe aceita uma string.
Como bônus adicional, get_version() agora pode ser reutilizada em outros lugares.
Princípio Aberto/Fechado (OCP)
"Incorpore novos recursos estendendo o sistema, não modificando-o."
Uncle Bob.
Objetos devem estar abertos para extensão, mas fechados para modificação. Deve ser possível aumentar a funcionalidade de um objeto (por exemplo, uma classe) sem modificar seus contratos internos. Isso pode ser alcançado ao projetar o objeto para ser estendido de maneira clara.
No exemplo a seguir, tentamos implementar um framework web simples que lida com requisições HTTP e retorna respostas. A classe View possui um único método .get(), que será chamado quando o servidor HTTP receber uma requisição GET de um cliente.
View é intencionalmente simples e retorna respostas text/plain. No entanto, gostaríamos de retornar respostas HTML baseadas em um arquivo de modelo, então criamos uma subclasse chamada TemplateView.
Ruim
from dataclasses import dataclass
@dataclass
class Response:
"""Uma resposta HTTP"""
status: int
content_type: str
body: str
class View:
"""Uma visão simples que retorna respostas em texto puro"""
def get(self, request) -> Response:
"""Lidar com uma requisição GET e retornar uma mensagem na resposta"""
return Response(
status=200,
content_type='text/plain',
body="Bem-vindo ao meu site"
)
class TemplateView(View):
"""Uma visão que retorna respostas HTML baseadas em um arquivo de modelo."""
def get(self, request) -> Response:
"""Lidar com uma requisição GET e retornar um documento HTML na resposta"""
with open("index.html") as fd:
return Response(
status=200,
content_type='text/html',
body=fd.read()
)
A classe TemplateView modificou o comportamento interno de sua classe pai para permitir uma funcionalidade mais avançada. Ao fazer isso, agora depende de View não alterar a implementação do método .get(), que precisa permanecer inalterado. Não podemos, por exemplo, adicionar verificações adicionais a todas as classes derivadas de View, pois o comportamento foi sobrescrito em pelo menos um subtipo, o que exigiria sua atualização.
Vamos redesenhar nossas classes para corrigir esse problema e permitir que View seja estendida (não modificada) de maneira mais limpa:
Bom
from dataclasses import dataclass
@dataclass
class Response:
"""Uma resposta HTTP"""
status: int
content_type: str
body: str
class View:
"""Uma visão simples que retorna respostas em texto puro"""
content_type = "text/plain"
def render_body(self) -> str:
"""Renderizar o corpo da mensagem da resposta"""
return "Bem-vindo ao meu site"
def get(self, request) -> Response:
"""Lidar com uma requisição GET e retornar uma mensagem na resposta"""
return Response(
status=200,
content_type=self.content_type,
body=self.render_body()
)
class TemplateView(View):
"""Uma visão que retorna respostas HTML baseadas em um arquivo de modelo."""
content_type = "text/html"
template_file = "index.html"
def render_body(self) -> str:
"""Renderizar o corpo da mensagem como HTML"""
with open(self.template_file) as fd:
return fd.read()
Observe que precisamos sobrescrever apenas o método render_body(), que tem uma responsabilidade bem definida e permite que subtipos o sobrescrevam. Ele foi projetado para ser estendido.
Outra boa abordagem para combinar os benefícios da herança e da composição de objetos é usar Mixins.
Mixins são classes minimalistas projetadas para serem usadas exclusivamente em conjunto com outras classes relacionadas. Elas são "misturadas" à classe-alvo por meio de herança múltipla para modificar seu comportamento.
Algumas regras:
- Mixins devem sempre herdar de
object. - Mixins sempre vêm antes da classe-alvo,
ex.:class Foo(MixinA, MixinB, TargetClass): ...
Também bom
from dataclasses import dataclass, field
from typing import Protocol
@dataclass
class Response:
"""Uma resposta HTTP"""
status: int
content_type: str
body: str
headers: dict = field(default_factory=dict)
class View:
"""Uma visão simples que retorna respostas em texto puro"""
content_type = "text/plain"
def render_body(self) -> str:
"""Renderizar o corpo da mensagem da resposta"""
return "Bem-vindo ao meu site"
def get(self, request) -> Response:
"""Lidar com uma requisição GET e retornar uma mensagem na resposta"""
return Response(
status=200,
content_type=self.content_type,
body=self.render_body()
)
class TemplateRenderMixin:
"""Mixin para visões que renderizam documentos HTML usando um arquivo de modelo.
Não deve ser usada sozinha!
"""
template_file: str = ""
def render_body(self) -> str:
"""Renderizar o corpo da mensagem como HTML"""
if not self.template_file:
raise ValueError("O caminho para um arquivo de modelo deve ser fornecido.")
with open(self.template_file) as fd:
return fd.read()
class ContentLengthMixin:
"""Mixin que adiciona um cabeçalho Content-Length na resposta.
Não deve ser usada sozinha!
"""
def get(self, request) -> Response:
"""Modificar a resposta para incluir o novo cabeçalho"""
response = super().get(request) # type: ignore
response.headers['Content-Length'] = len(response.body)
return response
class TemplateView(TemplateRenderMixin, ContentLengthMixin, View):
"""Uma visão que retorna respostas HTML baseadas em um arquivo de modelo."""
content_type = "text/html"
template_file = "index.html"
Mixins tornam a composição de objetos mais fácil, encapsulando funcionalidades reutilizáveis em classes com uma única responsabilidade, permitindo um desacoplamento limpo.
Aquí tienes la traducción al portugués:
Princípio da Substituição de Liskov (LSP)
“Funções que usam ponteiros ou referências para classes base
devem ser capazes de usar objetos de classes derivadas sem saber disso”,
Uncle Bob.
Este princípio leva o nome de Barbara Liskov, que colaborou com a cientista da computação Jeannette Wing no artigo seminal
"A behavioral notion of subtyping" (1994). Um dos conceitos centrais do artigo é que
"um subtipo (deve) preservar o comportamento dos métodos do supertipo, bem como todas as propriedades invariantes e históricas do supertipo".
Em essência, uma função que aceita um supertipo também deve aceitar todos os seus
subtipos sem necessidade de modificação.
Você consegue identificar o problema no seguinte código?
Ruim
from dataclasses import dataclass
@dataclass
class Response:
"""Uma resposta HTTP"""
status: int
content_type: str
body: str
class View:
"""Uma view simples que retorna respostas em texto puro"""
content_type = "text/plain"
def render_body(self) -> str:
"""Renderiza o corpo da mensagem da resposta"""
return "Bem-vindo ao meu site"
def get(self, request) -> Response:
"""Lida com uma requisição GET e retorna uma mensagem na resposta"""
return Response(
status=200,
content_type=self.content_type,
body=self.render_body()
)
class TemplateView(View):
"""Uma view que retorna respostas HTML baseadas em um arquivo de template."""
content_type = "text/html"
def get(self, request, template_file: str) -> Response: # type: ignore
"""Renderiza o corpo da mensagem como HTML"""
with open(template_file) as fd:
return Response(
status=200,
content_type=self.content_type,
body=fd.read()
)
def render(view: View, request) -> Response:
"""Renderiza uma View"""
return view.get(request)
A expectativa é que a função render() consiga trabalhar com View
e seu subtipo TemplateView, mas este último quebrou a compatibilidade
ao modificar a assinatura do método .get(). Isso resultará em uma exceção TypeError
quando usado com TemplateView.
Se quisermos que a função render() funcione com qualquer subtipo de View, devemos
garantir que seu protocolo público não seja quebrado. Mas como saber qual é esse protocolo?
Ferramentas de tipagem, como mypy, gerarão um erro ao detectar problemas como esse:
error: Signature of "get" incompatible with supertype "View"
<string>:36: note: Superclasse:
<string>:36: note: def get(self, request: Any) -> Response
<string>:36: note: Subclasse:
<string>:36: note: def get(self, request: Any, template_file: str) -> Response
Princípio da Segregação de Interface (ISP)
“Mantenha interfaces pequenas
para que os usuários não acabem dependendo de coisas que não precisam.”,
Uncle Bob.
Vários idiomas de programação orientada a objetos, como Java e Go,
possuem o conceito de interfaces. Uma interface define os métodos e
propriedades públicas de um objeto sem implementá-los. Elas são úteis quando não
queremos acoplar a assinatura de uma função a um objeto concreto,
mas sim dizer: "Não me importo com qual objeto você me dá, desde que ele tenha
certos métodos e atributos que eu espero usar".
O Python não tem interfaces. Em vez disso, temos Classes Base Abstratas (ABCs),
que são um pouco diferentes, mas podem servir ao mesmo propósito.
Bom
from abc import ABCMeta, abstractmethod
# Define a Classe Abstrata para um objeto genérico Greeter
class Greeter(metaclass=ABCMeta):
"""Um objeto que pode executar uma ação de saudação."""
@staticmethod
@abstractmethod
def greet(name: str) -> None:
"""Exibe uma saudação para o usuário com o nome fornecido"""
class FriendlyActor(Greeter):
"""Um ator que cumprimenta o usuário com uma saudação amigável"""
@staticmethod
def greet(name: str) -> None:
"""Cumprimenta uma pessoa pelo nome"""
print(f"Olá {name}!")
def welcome_user(user_name: str, actor: Greeter):
"""Dá boas-vindas a um usuário com um nome específico usando o ator fornecido"""
actor.greet(user_name)
welcome_user("Barbara", FriendlyActor())
Agora imagine o seguinte cenário: temos vários documentos PDF
que criamos e queremos disponibilizar para os visitantes do nosso site.
Estamos usando um framework web em Python e poderíamos projetar uma classe
para gerenciar esses documentos. Então, criamos uma classe base abstrata
abrangente para nosso documento.
Erro
import abc
class Persistable(metaclass=abc.ABCMeta):
"""Serializa um arquivo para dados e vice-versa"""
@property
@abc.abstractmethod
def data(self) -> bytes:
"""Os dados brutos do arquivo"""
@classmethod
@abc.abstractmethod
def load(cls, name: str):
"""Carrega o arquivo do disco"""
@abc.abstractmethod
def save(self) -> None:
"""Salva o arquivo no disco"""
class PDFDocument(Persistable):
"""Um documento PDF"""
@property
def data(self) -> bytes:
"""Os bytes brutos do documento PDF"""
... # Código omitido
@classmethod
def load(cls, name: str):
"""Carrega o arquivo do sistema de arquivos local"""
... # Código omitido
def view(request):
"""Uma view que lida com uma requisição GET para um documento"""
requested_name = request.qs['name']
return PDFDocument.load(requested_name).data
Porém, não podemos instanciar PDFDocument sem implementar .save(),
o que gera um erro:
O problema é que criamos uma interface que tem recursos que não precisamos agora.
A solução é decompor a interface em interfaces menores e compostáveis.
Bom
import abc
class DataCarrier(metaclass=abc.ABCMeta):
"""Carrega um conjunto de dados"""
@property
def data(self):
...
class Loadable(DataCarrier):
"""Pode carregar dados do armazenamento pelo nome"""
@classmethod
@abc.abstractmethod
def load(cls, name: str):
...
class Saveable(DataCarrier):
"""Pode salvar dados no armazenamento"""
@abc.abstractmethod
def save(self) -> None:
...
class PDFDocument(Loadable):
"""Um documento PDF"""
@property
def data(self) -> bytes:
... # Código omitido
@classmethod
def load(cls, name: str) -> None:
... # Código omitido
def view(request):
"""Uma view que lida com uma requisição GET para um documento"""
requested_name = request.qs['name']
return PDFDocument.load(requested_name).data
Princípio da Inversão de Dependência (DIP)
“Dependa de abstrações, não de detalhes concretos.”
Uncle Bob.
Imagine que queremos escrever uma view web que retorna uma resposta HTTP
transmitindo linhas de um arquivo CSV gerado dinamicamente.
Queremos usar o escritor CSV da biblioteca padrão.
Ruim
import csv
from io import StringIO
class StreamingHttpResponse:
"""Uma resposta HTTP em streaming"""
... # Código omitido
def some_view(request):
rows = (
['Primeira linha', 'Foo', 'Bar', 'Baz'],
['Segunda linha', 'A', 'B', 'C', '"Teste"', "Aqui está uma citação"]
)
def stream():
buffer_ = StringIO()
writer = csv.writer(buffer_, delimiter=';', quotechar='"')
for row in rows:
writer.writerow(row)
buffer_.seek(0)
data = buffer_.read()
buffer_.seek(0)
buffer_.truncate()
yield data
response = StreamingHttpResponse(stream(), content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="arquivo.csv"'
return response
Essa implementação é trabalhosa. Uma melhor abordagem é usar um objeto
que implemente .write() para retornar os dados imediatamente.
Este exemplo foi retirado de uma contribuição feita para a documentação do Django por este autor.