Python

Interfaces, Protocolos e Composição Já leu

8 min de leitura

Interfaces, Protocolos e Composição
No artigo anterior vimos que herança é uma ferramenta poderosa — mas não é sempre a melhor escolha. Um princípio amplam

No artigo anterior vimos que herança é uma ferramenta poderosa — mas não é sempre a melhor escolha. Um princípio amplamente aceito no design de software diz: prefira composição à herança. Neste artigo vamos entender a diferença entre essas abordagens, explorar o sistema de protocolos do Python e aprender a construir sistemas mais flexíveis e fáceis de manter.


O Problema da Herança Excessiva

Herança cria um acoplamento forte entre classes. Considere:

class Ave:
    def respirar(self):
        return "Respirando..."

    def voar(self):
        return "Voando..."

    def cantar(self):
        return "Cantando..."


class Pinguim(Ave):
    def voar(self):
        raise NotImplementedError("Pinguins não voam!")

O Pinguim herda voar() de Ave mas não pode implementá-la — viola o Princípio de Substituição de Liskov: uma subclasse deve poder substituir sua classe pai sem quebrar o programa. Esse é o sinal de que herança não é a ferramenta certa aqui.


Composição

Composição significa construir classes a partir de outras classes, incluindo-as como atributos — relação "tem um" em vez de "é um":

class Motor:
    def __init__(self, potencia_cv, combustivel):
        self.potencia_cv = potencia_cv
        self.combustivel = combustivel
        self._ligado = False

    def ligar(self):
        self._ligado = True
        return f"Motor {self.potencia_cv}cv ligado."

    def desligar(self):
        self._ligado = False
        return "Motor desligado."

    @property
    def ligado(self):
        return self._ligado


class SistemaSom:
    def __init__(self, marca, potencia_watts):
        self.marca   = marca
        self.potencia = potencia_watts

    def tocar(self, musica):
        return f"[{self.marca}] Tocando: {musica}"


class Carro:
    """Carro TEM UM motor e TEM UM sistema de som — composição."""

    def __init__(self, modelo, motor, som):
        self.modelo = modelo
        self._motor = motor
        self._som   = som

    def ligar(self):
        return self._motor.ligar()

    def tocar_musica(self, musica):
        if not self._motor.ligado:
            return "Ligue o carro primeiro."
        return self._som.tocar(musica)

    def status(self):
        estado = "ligado" if self._motor.ligado else "desligado"
        return f"{self.modelo} — {estado}"


motor  = Motor(150, "Flex")
som    = SistemaSom("Pioneer", 200)
carro  = Carro("Honda Civic", motor, som)

print(carro.ligar())
print(carro.tocar_musica("Bohemian Rhapsody"))
print(carro.status())

A vantagem: podemos trocar o motor ou o sistema de som sem modificar a classe Carro. Cada componente é independente e reutilizável.


Interfaces Informais: Duck Typing

Python não tem a palavra-chave interface como Java ou C#. Em vez disso, usa duck typing — qualquer objeto que implemente os métodos esperados pode ser usado:

class RelatorioCSV:
    def gerar(self, dados):
        linhas = [",".join(str(v) for v in linha) for linha in dados]
        return "\n".join(linhas)

    def salvar(self, conteudo, caminho):
        print(f"Salvando CSV em {caminho}...")


class RelatorioJSON:
    def gerar(self, dados):
        import json
        return json.dumps(dados, ensure_ascii=False, indent=2)

    def salvar(self, conteudo, caminho):
        print(f"Salvando JSON em {caminho}...")


class RelatorioHTML:
    def gerar(self, dados):
        linhas = "".join(
            f"<tr>{''.join(f'<td>{v}</td>' for v in linha)}</tr>"
            for linha in dados
        )
        return f"<table>{linhas}</table>"

    def salvar(self, conteudo, caminho):
        print(f"Salvando HTML em {caminho}...")


def exportar(relatorio, dados, caminho):
    """Funciona com qualquer objeto que tenha gerar() e salvar()."""
    conteudo = relatorio.gerar(dados)
    relatorio.salvar(conteudo, caminho)
    return conteudo


dados = [["Ana", 9.5], ["Bruno", 8.0], ["Carla", 7.5]]

exportar(RelatorioCSV(),  dados, "notas.csv")
exportar(RelatorioJSON(), dados, "notas.json")
exportar(RelatorioHTML(), dados, "notas.html")

Nenhuma das classes herda de uma interface comum — mas todas funcionam com exportar() porque implementam gerar() e salvar().


Protocolos Formais: typing.Protocol

A partir do Python 3.8, o módulo typing oferece Protocol — uma forma de definir interfaces sem herança, usando structural subtyping (verificação estática pela estrutura):

from typing import Protocol

class Serializavel(Protocol):
    def serializar(self) -> str:
        ...

    def deserializar(self, dados: str) -> None:
        ...


class UsuarioJSON:
    def __init__(self, nome, email):
        self.nome  = nome
        self.email = email

    def serializar(self) -> str:
        import json
        return json.dumps({"nome": self.nome, "email": self.email})

    def deserializar(self, dados: str) -> None:
        import json
        obj = json.loads(dados)
        self.nome  = obj["nome"]
        self.email = obj["email"]


class ConfiguracaoINI:
    def __init__(self):
        self.dados = {}

    def serializar(self) -> str:
        return "\n".join(f"{k}={v}" for k, v in self.dados.items())

    def deserializar(self, dados: str) -> None:
        for linha in dados.strip().split("\n"):
            k, v = linha.split("=")
            self.dados[k.strip()] = v.strip()


def salvar(obj: Serializavel, caminho: str) -> None:
    """Aceita qualquer objeto que implemente o protocolo Serializavel."""
    conteudo = obj.serializar()
    print(f"Salvando em {caminho}: {conteudo}")


usuario = UsuarioJSON("Ana", "ana@email.com")
config  = ConfiguracaoINI()
config.dados = {"tema": "escuro", "idioma": "pt-BR"}

salvar(usuario, "usuario.json")
salvar(config,  "config.ini")

UsuarioJSON e ConfiguracaoINI não herdam de Serializavel — mas ferramentas como mypy verificam estaticamente se implementam os métodos exigidos pelo protocolo.


Mixins

Mixins são classes pequenas e focadas que adicionam funcionalidades específicas via herança múltipla — sem representar uma entidade completa:

class LogMixin:
    """Adiciona capacidade de logging a qualquer classe."""

    def log(self, mensagem, nivel="INFO"):
        from datetime import datetime
        agora = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        print(f"[{agora}] [{nivel}] [{type(self).__name__}] {mensagem}")


class ValidacaoMixin:
    """Adiciona validação de campos obrigatórios."""

    campos_obrigatorios = []

    def validar(self):
        erros = []
        for campo in self.campos_obrigatorios:
            valor = getattr(self, campo, None)
            if not valor:
                erros.append(f"Campo '{campo}' é obrigatório.")
        return erros


class SerializacaoMixin:
    """Adiciona serialização automática para dicionário."""

    def para_dict(self):
        return {
            k: v for k, v in self.__dict__.items()
            if not k.startswith("_")
        }


class Pedido(LogMixin, ValidacaoMixin, SerializacaoMixin):
    campos_obrigatorios = ["cliente", "produto", "quantidade"]

    def __init__(self, cliente, produto, quantidade):
        self.cliente    = cliente
        self.produto    = produto
        self.quantidade = quantidade

    def processar(self):
        erros = self.validar()
        if erros:
            for erro in erros:
                self.log(erro, "ERRO")
            return False

        self.log(f"Processando pedido: {self.produto} x{self.quantidade}")
        return True


pedido = Pedido("Ana", "Teclado Mecânico", 2)
pedido.processar()
print(pedido.para_dict())

pedido_invalido = Pedido("", "Mouse", 0)
pedido_invalido.validar()
pedido_invalido.processar()

Composição vs. Herança: quando usar cada uma

Situação Use
A relação é claramente "é um" (Cachorro é um Animal) Herança
A relação é "tem um" (Carro tem um Motor) Composição
Quer adicionar comportamento sem criar hierarquia Mixin
Quer definir um contrato sem forçar herança Protocol
A hierarquia tem mais de 2 níveis Reveja o design
Subclasse precisa desabilitar métodos do pai Composição

Exemplo Completo: Sistema de Notificações

from typing import Protocol
from datetime import datetime


class Canal(Protocol):
    def enviar(self, destinatario: str, mensagem: str) -> bool:
        ...


class CanalEmail:
    def enviar(self, destinatario: str, mensagem: str) -> bool:
        print(f"[EMAIL] Para: {destinatario} | {mensagem}")
        return True


class CanalSMS:
    def enviar(self, destinatario: str, mensagem: str) -> bool:
        if len(mensagem) > 160:
            print(f"[SMS] Mensagem muito longa para {destinatario}")
            return False
        print(f"[SMS] Para: {destinatario} | {mensagem}")
        return True


class CanalPush:
    def enviar(self, destinatario: str, mensagem: str) -> bool:
        print(f"[PUSH] Para: {destinatario} | {mensagem[:50]}...")
        return True


class LogMixin:
    def _registrar(self, canal, destinatario, sucesso):
        status = "OK" if sucesso else "FALHOU"
        agora  = datetime.now().strftime("%H:%M:%S")
        print(f"  [{agora}] {canal} → {destinatario}: {status}")


class ServicoNotificacao(LogMixin):
    """Usa composição — recebe canais como dependências."""

    def __init__(self, canais: list):
        self._canais = canais

    def notificar(self, destinatario: str, mensagem: str):
        resultados = {}
        for canal in self._canais:
            nome   = type(canal).__name__
            sucesso = canal.enviar(destinatario, mensagem)
            self._registrar(nome, destinatario, sucesso)
            resultados[nome] = sucesso
        return resultados

    def adicionar_canal(self, canal):
        self._canais.append(canal)


servico = ServicoNotificacao([
    CanalEmail(),
    CanalSMS(),
    CanalPush(),
])

print("=== Notificação de boas-vindas ===")
servico.notificar("ana@email.com", "Bem-vinda à plataforma!")

print("\n=== Alerta de segurança ===")
servico.notificar(
    "bruno@email.com",
    "Detectamos um acesso suspeito na sua conta. "
    "Se não foi você, altere sua senha imediatamente."
)

Resumo

  • Prefira composição à herança quando a relação entre classes é "tem um" e não "é um"
  • Duck typing permite polimorfismo sem herança formal — basta implementar os métodos esperados
  • typing.Protocol formaliza interfaces sem exigir herança, compatível com verificação estática
  • Mixins adicionam comportamentos específicos via herança múltipla de forma controlada
  • Herança excessiva cria acoplamento forte e hierarquias frágeis
  • Composição torna os componentes independentes, substituíveis e testáveis isoladamente

Referências e Leituras Complementares

Comentários

Mais em Python

A História do Python e os Primeiros Passos
A História do Python e os Primeiros Passos

Antes de escrever a primeira linha de c&oacute;digo, vale a pena entender de...

Estruturas de Controle: if, elif e else
Estruturas de Controle: if, elif e else

Um programa que executa sempre as mesmas instru&ccedil;&otilde;es na mesma or...

Funções: definição, parâmetros e escopo
Funções: definição, parâmetros e escopo

Fun&ccedil;&otilde;es s&atilde;o o principal mecanismo de organiza&ccedil;&at...