Blog

Composição em vez de herança em Python

Herança é a primeira ferramenta que a maioria das pessoas usa e a que envelhece pior. O motivo é simples: uma subclasse herda tudo, inclusive as partes que não se aplicam a ela.

O problema do pinguim

# inheritance for reuse: the shape that goes wrong
class Animal:
    def __init__(self, name): self.name = name
    def speak(self): return '...'
    def fly(self): return f"{self.name} flies away"

class Bird(Animal):
    def speak(self): return f"{self.name} tweets"

class Penguin(Bird):
    pass

p = Penguin('Pingu')
print(p.speak())
print(p.fly())          # inherited, and wrong

Ele imprime:

Pingu tweets
Pingu flies away

Um pinguim é inquestionavelmente uma ave, e a hierarquia está inquestionavelmente errada. Não houve erro de modelagem — fly foi colocado na classe base porque a maioria das aves voa, o que era verdade quando aquilo foi escrito.

Os remendos habituais pioram a situação. Sobrescreva fly para levantar erro e você tem uma subclasse que falha na interface do pai. Mova fly para uma classe FlyingBird e você vai ter que fazer tudo de novo na primeira vez que encontrar outra coisa que não voa.

Composição: a capacidade é um objeto

# composition: the capability is an object, not an ancestor
class Wings:
    def fly(self, name): return f"{name} flies away"

class Bird2:
    def __init__(self, name, wings=None):
        self.name, self.wings = name, wings
    def speak(self): return f"{self.name} tweets"
    def fly(self):
        if self.wings is None:
            raise TypeError(f"{self.name} cannot fly")
        return self.wings.fly(self.name)

print(Bird2('Robin', Wings()).fly())
try:
    Bird2('Pingu').fly()
except TypeError as err:
    print(type(err).__name__ + ':', err)

Ele imprime:

Robin flies away
TypeError: Pingu cannot fly

Voar agora é algo que uma ave tem, não algo que ela é. Acrescentar uma ave que não voa não exige classe nova nem mudança de hierarquia.

O ganho é poder trocar comportamento

O argumento de verdade não é taxonomia. É que uma capacidade composta pode ser trocada em tempo de execução e substituída num teste.

# swapping behaviour at runtime is the payoff
class JsonFormat:
    def render(self, row): return f'{{"name": "{row}"}}'
class CsvFormat:
    def render(self, row): return f'{row}'

class Report:
    def __init__(self, fmt): self.fmt = fmt
    def emit(self, rows): return [self.fmt.render(r) for r in rows]

print(Report(JsonFormat()).emit(['ada']))
print(Report(CsvFormat()).emit(['ada']))

Ele imprime:

['{"name": "ada"}']
['ada']

Um Report só. Como herança isso seria JsonReport e CsvReport, e um terceiro formato significaria uma terceira classe — além de um problema de verdade no dia em que você quiser um relatório que é os dois.

Testar mostra a diferença com mais clareza. Passe um formatador falso e você testou o Report sem tocar em JSON. Com herança você teria que criar uma subclasse para interceptar.

Quando herança está certa

Ela tem uma função, e é mais estreita do que as pessoas usam: quando a relação é genuinamente “é um” e a classe base é abstrata — ela define uma interface em vez de entregar comportamento que as subclasses talvez não queiram.

# inheritance is right when it is genuinely 'is a' and the base is abstract
class Shape:
    def area(self): raise NotImplementedError
    def describe(self): return f"{type(self).__name__} with area {self.area():.2f}"

class Circle(Shape):
    def __init__(self, r): self.r = r
    def area(self): return 3.14159 * self.r ** 2

class Square(Shape):
    def __init__(self, s): self.s = s
    def area(self): return self.s ** 2

for s in (Circle(1), Square(2)):
    print(s.describe())

Ele imprime:

Circle with area 3.14
Square with area 4.00

Shape promete area e oferece describe construído em cima dela. Nenhuma subclasse herda algo que não deveria ter, porque não há nada concreto para herdar. A parte 9 cobre ABCs e Protocols, que tornam essa promessa verificável.

O que lembrar

  • Herança dá à subclasse tudo, inclusive o que ela não deveria ter.

  • Use composição quando você está herdando para reaproveitar código, e não para declarar um tipo.

  • O benefício prático é trocar e falsificar comportamento, não pureza de modelagem.

  • Herança serve quando a base é abstrata e a relação é genuinamente “é um”.

A regra prática que sobrevive ao código real: se você está escrevendo uma subclasse para ter acesso a um método, você quer composição. Se está escrevendo uma para prometer uma interface, herança está bem.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.