Короткий ответ
Глубокое наследование создаёт жёсткую связанность между уровнями иерархии и делает локальные изменения базового класса потенциально опасными для всех потомков (fragile base class problem); переход к композиции — сборке поведения из небольших, независимо тестируемых объектов-стратегий/сервисов, внедряемых через конструктор — обычно снижает эту связанность, но требует более явного проектирования интерфейсов и не бесплатен в смысле читаемости и количества объектов.
Как это работает подробнее
При наследовании подкласс неявно зависит от внутренней реализации родителя (какие методы вызывают друг друга, в каком порядке, какие инварианты поддерживаются), а не только от его публичного контракта — изменение этой внутренней механики в базовом классе может незаметно для автора изменения сломать логику в далёких потомках. Композиция заменяет "является" (is-a) на "использует" (has-a): объект работы с заказом получает нужное поведение (расчёт скидки, уведомление, валидацию) как отдельные объекты-коллаборанты через явные интерфейсы, и изменение одного коллаборанта не затрагивает остальных, пока контракт интерфейса не меняется.
Пример кода
# Было: пятиуровневая иерархия, где каждый подкласс переопределяет шаги обработки
class Order: ...
class DiscountedOrder(Order): ...
class DiscountedInternationalOrder(DiscountedOrder): ...
# ломается непредсказуемо при изменении Order.process()
# Альтернатива: композиция стратегий, внедряемых явно
class Order:
def __init__(self, discount_strategy, shipping_strategy):
self.discount_strategy = discount_strategy
self.shipping_strategy = shipping_strategy
def total(self, base_amount):
discounted = self.discount_strategy.apply(base_amount)
return self.shipping_strategy.add_cost(discounted)
# Изменение одной стратегии не затрагивает Order и другие стратегии напрямуюЧто использовать на практике
- Практика. Перенос с наследования на композицию — это рефакторинг, который стоит делать постепенно: выделить нестабильные, часто переопределяемые участки поведения в отдельные протоколы/классы-стратегии, оставив неглубокое наследование там, где оно действительно отражает устойчивую иерархию типов (например, разные варианты одного интерфейса с минимальной переопределённой логикой). Критерий выбора — частота и характер изменений: если поведение на разных уровнях меняется независимо и часто, композиция снижает риск регрессий; если иерархия стабильна и уровни меняются синхронно, глубина наследования может быть приемлема.
Подводные камни
- Риски. Механически заменяют всё наследование композицией без оценки реальной стоимости рефакторинга (миграция существующих подклассов, тесты, время команды) и рискуют получить избыточно многословный код с большим количеством мелких интерфейсов там, где простая иерархия из двух уровней работала бы прекрасно. Также путают "глубина наследования — это плохо" с "наследование — это плохо": проблема не в наследовании как инструменте, а в его неограниченной глубине и смешении уровней абстракции внутри одной иерархии.
- Компромисс. Композиция обычно снижает хрупкость и улучшает тестируемость по отдельности, но увеличивает количество объектов, интерфейсов и мест, где нужно явно связать зависимости (вручную или через DI-контейнер), что может замедлить онбординг новых разработчиков и усложнить навигацию по коду. Решение зависит от реальной частоты изменений в каждом уровне иерархии, размера команды и того, насколько критично для бизнеса минимизировать регрессии при доработке заказа.