Curso de Python / Escolher entre herança e composição

Herança estende uma classe por meio de uma relação de subtipo; composição organiza um objeto usando outros objetos ou funções como componentes. Escolha pela relação que o programa precisa representar e pelas mudanças esperadas, sem concluir que trechos de código parecidos exigem uma hierarquia.

Examinar a relação e o contrato antes de escolher

Uma classe derivada deve poder cumprir o contrato esperado de sua classe base nos usos previstos. Se ela exigir entradas mais restritas ou mudar o significado do resultado, substituir o objeto base por ela pode quebrar o chamador, mesmo que o nome do método seja igual.

Um livro de registros usa uma política de taxa; ele não é uma espécie de política. Guardar essa política num atributo expressa a colaboração por composição. Já uma política de taxa fixa pode ser uma especialização de uma política base, se mantiver a interface e as regras esperadas. Um desenho pode combinar as duas técnicas sem que elas sejam alternativas mutuamente exclusivas.

Hierarquias profundas aumentam os lugares que precisam ser entendidos quando uma implementação muda. Comece pelo comportamento necessário e mantenha as responsabilidades claras, como na prática de classes, instâncias e métodos. A documentação de herança em Python apresenta a definição e a busca de métodos.

Executar uma política substituível

Salve o programa completo em composition.py, com UTF-8, numa pasta de prática. Execute python composition.py no terminal com o ambiente virtual selecionado se houver. Em macOS ou Ubuntu sem ambiente virtual, use python3 composition.py quando necessário. Nenhum pacote externo é exigido.

class FeePolicy:
    def apply(self, amount):
        return amount

class FixedFee(FeePolicy):
    def __init__(self, fee):
        self.fee = fee

    def apply(self, amount):
        return super().apply(amount) + self.fee

class Ledger:
    def __init__(self, policy):
        self.policy = policy

    def charge(self, amount):
        return self.policy.apply(amount)

print(Ledger(FeePolicy()).charge(8000))
print(Ledger(FixedFee(500)).charge(8000))

A saída esperada é:

8000
8500

FeePolicy.apply() devolve o valor recebido sem acréscimo. FixedFee herda de FeePolicy e redefine apply() para acrescentar a taxa armazenada. Por isso a política base produz 8000 e a taxa fixa de 500 produz 8500.

Ledger recebe uma política na inicialização, guarda-a em self.policy e delega o cálculo por self.policy.apply(amount). Sua implementação não precisa mudar para usar a outra política. A herança está entre as políticas; a composição está entre o registro e a política que ele usa.

super().apply(amount) chama a próxima implementação conforme a ordem de resolução de métodos, ou MRO (Method Resolution Order), da classe do objeto. Nesta hierarquia simples, ela é a de FeePolicy. Não interprete super() como um nome fixo de pai para toda situação: em estruturas de herança múltipla, a próxima classe pode depender da MRO completa. A referência de super() detalha esse mecanismo.

Comparar herança com composição por função

O programa seguinte separa duas alternativas para o mesmo cálculo: o primeiro registro redefine um método de taxa; o segundo recebe uma função que devolve a taxa. Salve o conteúdo completo em policies.py e execute python policies.py, ou python3 policies.py quando necessário.

class InheritedLedger:
    def fee(self):
        return 0
    def charge(self, amount):
        return amount + self.fee()
class FixedLedger(InheritedLedger):
    def fee(self):
        return 500
class ComposedLedger:
    def __init__(self, fee_policy):
        self.fee_policy = fee_policy
    def charge(self, amount):
        return amount + self.fee_policy()
def fixed_fee():
    return 500
print(FixedLedger().charge(8000))
ledger = ComposedLedger(fixed_fee)
print(ledger.charge(8000))
ledger.fee_policy = lambda: 0
print(ledger.charge(8000))

O resultado esperado é:

8500
8500
8000

FixedLedger herda a operação charge(), que chama self.fee(). Para uma instância de FixedLedger, essa chamada encontra o método redefinido e obtém 500. ComposedLedger calcula a mesma soma chamando o colaborador recebido em fee_policy.

A atribuição ledger.fee_policy = lambda: 0 troca somente o colaborador desse objeto. A pequena função sem argumentos passa a devolver zero, e a cobrança resulta em 8000. Em uma aplicação maior, uma função nomeada pode tornar a política mais clara que uma expressão lambda; o ponto da comparação é poder substituir o componente sem reescrever o registro.

As duas interfaces dos exemplos são diferentes: no primeiro arquivo, a política oferece apply(amount); no segundo, a função de taxa é chamada sem argumentos. Não passe diretamente fixed_fee ao primeiro Ledger esperando que ele adivinhe essa adaptação. O colaborador precisa fornecer o comportamento e a forma de chamada que o objeto consumidor espera.

Testar substituição e revisar os limites do desenho

  1. No primeiro programa, troque somente FixedFee(500) por FixedFee(0). As duas linhas devem ser 8000. Restaure 500 e confirme 8000 e 8500.
  2. No segundo programa, mantenha o código das classes e altere somente a atribuição da política de lambda: 0 para lambda: 200. As duas primeiras linhas continuam 8500 e a última passa a 8200. Restaure zero.
  3. Para testar uma colaboração pequena no primeiro arquivo, crie uma classe separada com um método apply(self, amount) que devolva amount. Ela pode ser usada por Ledger sem herdar de FeePolicy, desde que cumpra a operação esperada. A compatibilidade depende do comportamento, não só do nome do método.
  4. Rode novamente os dois programas originais em processos novos e confira todas as linhas de saída. A troca de um colaborador não deve alterar sem querer os resultados de outros objetos.

Os exemplos não validam tipos, limites de taxa, moeda ou valores negativos. Seu objetivo é tornar as relações e chamadas visíveis. Se essas regras forem necessárias, defina quem as verifica e teste o contrato de cada política antes de considerá-las substituíveis.

Antes de acrescentar uma classe derivada, pergunte se ela mantém o significado do resultado da base, se está herdando operações sem relação com seu papel e se receber um componente tornaria as dependências mais claras. Para um problema pequeno, uma função pode bastar. Mantenha entrada interativa, apresentação e armazenamento fora da política de cálculo enquanto compara essas alternativas, para que uma diferença de desenho não fique misturada a efeitos externos.