И наследование, и композиция переиспользуют код, но связывают классы по-разному. Наследование (is-a) привязывает подкласс к внутренностям базового. Композиция (has-a) держит один объект внутри другого и использует только его публичный интерфейс. Основы — в уроке Композиция и наследование; этот урок — про проектное решение.

Слева `Penguin` наследует `Bird.fly` и вынужден бросать ошибку; справа у `Bird` есть `Mover`, его реализуют `Fly` и `Walk`
Слева Penguin наследует Bird.fly и вынужден бросать ошибку; справа у Bird есть Mover, его реализуют Fly и Walk

Когда наследование оправдано

  • Настоящее is-a. Каждый подкласс выполняет все обещания базового класса, поэтому его можно подставить везде, где ждут базовый: принцип подстановки Лисков.
  • Стабильная база, рассчитанная на расширение: Exception, абстрактные базовые классы, точки расширения фреймворков вроде unittest.TestCase.
  • Неглубокое дерево: один-два уровня.
python
class Bird:
    def fly(self):
        return "flap"

class Sparrow(Bird):
    pass

class Penguin(Bird):
    def fly(self):
        raise NotImplementedError("penguins cannot fly")

for b in [Sparrow(), Penguin()]:
    try:
        print(type(b).__name__, b.fly())
    except NotImplementedError as e:
        print(type(b).__name__, "fails:", e)
text
Sparrow flap
Penguin fails: penguins cannot fly

Penguin наследует fly(), который не может выполнить. Теперь каждому коду, обходящему объекты Bird, нужен особый случай. Если подкласс бросает NotImplementedError или молча ничего не делает в унаследованном методе, «is-a» было лишь иллюзией.

Хрупкий базовый класс