← Назад к вопросам

Что из себя представляет проблема: наследование vs композиция?

2.4 Senior🔥 121 комментариев
#DevOps и инфраструктура

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Наследование vs Композиция: в чём проблема?

Это фундаментальный вопрос в ООП дизайне. Наследование долго было предпочтительным, но оно имеет критические проблемы. Композиция решает эти проблемы более элегантно. Расскажу почему.

Проблема наследования: Fragile Base Class

# ❌ Проблемное наследование
class Bird:
    def fly(self):
        return "Flying high!"

class Penguin(Bird):
    def fly(self):
        # Пингвины не летают, но наследуют fly()!
        raise Exception("Penguins cannot fly")

def make_bird_fly(bird: Bird):
    return bird.fly()

# Ошибка во время выполнения
bird = Penguin()
make_bird_fly(bird)  # Exception! ❌

# Это нарушает Liskov Substitution Principle (LSP)
# Подклассы не должны нарушать контракт базового класса

Проблема наследования: Diamond Problem

# ❌ Множественное наследование
class Animal:
    def sound(self):
        return "Some sound"

class CanFly:
    def fly(self):
        return "Flying"

class CanSwim:
    def swim(self):
        return "Swimming"

class Duck(CanFly, CanSwim, Animal):
    pass  # Какой sound() использовать?

# MRO (Method Resolution Order) решает, но это сложно:
print(Duck.mro())
# [Duck, CanFly, CanSwim, Animal, object]

Проблема наследования: Rigidity

# ❌ Наследование статично
class Vehicle:
    def move(self):
        pass

class Car(Vehicle):
    def move(self):
        return "Driving on road"

class Boat(Vehicle):
    def move(self):
        return "Sailing on water"

# Что если нужна amфибия? Наследовать от обоих? Проблемы!
class Amphibian(Car, Boat):  # Diamond!
    pass

# Каждое новое сочетание требует нового класса
# Комбинаций экспоненциально растёт!

Решение: Композиция

Композиция — это составление объекта из других объектов (has-a вместо is-a).

# ✅ Композиция: гибко, просто, SOLID
class MovementBehavior:
    def move(self):
        raise NotImplementedError

class DrivingBehavior(MovementBehavior):
    def move(self):
        return "Driving on road"

class SailingBehavior(MovementBehavior):
    def move(self):
        return "Sailing on water"

class Vehicle:
    def __init__(self, movement: MovementBehavior):
        self.movement = movement  # ← Composition: has-a
    
    def move(self):
        return self.movement.move()

# Использование
car = Vehicle(DrivingBehavior())
print(car.move())  # Driving on road

boat = Vehicle(SailingBehavior())
print(boat.move())  # Sailing on water

# Легко создать амфибию
class AmphibianBehavior(MovementBehavior):
    def move(self):
        return "Moving on land and water"

amphibian = Vehicle(AmphibianBehavior())
print(amphibian.move())  # Moving on land and water

Strategy Pattern: типичный пример композиции

# Наследование: плохо
class PaymentProcessor:
    def process(self, amount):
        raise NotImplementedError

class CreditCardProcessor(PaymentProcessor):
    def process(self, amount):
        return f"Processing ${amount} via credit card"

class PayPalProcessor(PaymentProcessor):
    def process(self, amount):
        return f"Processing ${amount} via PayPal"

class CryptoProcessor(PaymentProcessor):
    def process(self, amount):
        return f"Processing ${amount} via crypto"

# Что если нужна комбинация? Новый класс
class CreditCardAndPayPalProcessor(CreditCardProcessor, PayPalProcessor):
    # Конфликт методов!
    pass

# Композиция: хорошо
class PaymentStrategy:
    def execute(self, amount):
        raise NotImplementedError

class CreditCard(PaymentStrategy):
    def execute(self, amount):
        return f"${amount} via credit card"

class PayPal(PaymentStrategy):
    def execute(self, amount):
        return f"${amount} via PayPal"

class Checkout:
    def __init__(self, payment_strategy: PaymentStrategy):
        self.payment = payment_strategy
    
    def pay(self, amount):
        return self.payment.execute(amount)

# Легко переключаться между стратегиями
checkout = Checkout(CreditCard())
print(checkout.pay(99))  # $99 via credit card

checkout = Checkout(PayPal())
print(checkout.pay(99))  # $99 via PayPal

Проблема: Tight Coupling при наследовании

# ❌ Наследование создаёт сильную связь
class DataProcessor:
    def process(self):
        data = self.fetch_data()  # ← зависит от подкласса
        return self.transform(data)  # ← зависит от подкласса
    
    def fetch_data(self):
        raise NotImplementedError
    
    def transform(self, data):
        raise NotImplementedError

class CSVProcessor(DataProcessor):
    def fetch_data(self):
        return self.read_csv()
    
    def transform(self, data):
        return self.validate_csv(data)

# Изменение базового класса ломает подклассы
# Сложно тестировать (нужно создавать подклассы)

# ✅ Композиция: слабая связь
class DataFetcher:
    def fetch(self):
        raise NotImplementedError

class DataTransformer:
    def transform(self, data):
        raise NotImplementedError

class CSVFetcher(DataFetcher):
    def fetch(self):
        return self.read_csv()

class CSVTransformer(DataTransformer):
    def transform(self, data):
        return self.validate_csv(data)

class Pipeline:
    def __init__(
        self,
        fetcher: DataFetcher,
        transformer: DataTransformer
    ):
        self.fetcher = fetcher
        self.transformer = transformer
    
    def execute(self):
        data = self.fetcher.fetch()
        return self.transformer.transform(data)

# Легко комбинировать, легко тестировать
pipeline = Pipeline(CSVFetcher(), CSVTransformer())
result = pipeline.execute()

# В тестах просто создаём mock
class MockFetcher(DataFetcher):
    def fetch(self):
        return {"test": "data"}

pipeline_test = Pipeline(MockFetcher(), CSVTransformer())

Сравнение: Наследование vs Композиция

┌─────────────────┬──────────────────────┬──────────────────────┐
│ Аспект          │ Наследование ❌       │ Композиция ✅        │
├─────────────────┼──────────────────────┼──────────────────────┤
│ Связанность     │ Сильная (tight)      │ Слабая (loose)       │
│ Гибкость        │ Жёсткая              │ Гибкая               │
│ Тестируемость   │ Сложнее (mock класс) │ Проще (inject mock)  │
│ Переиспользование│ Ограничено          │ Высокое              │
│ Изменяемость    │ Рискованна           │ Безопасна             │
│ Complexity      │ Растёт с уровнями   │ Контролируется        │
└─────────────────┴──────────────────────┴──────────────────────┘

Когда всё-таки использовать наследование?

# 1. Реальное отношение IS-A
class Shape:
    def area(self):
        raise NotImplementedError

class Circle(Shape):  # ✅ Circle IS-A Shape
    def area(self):
        return 3.14 * self.radius ** 2

# 2. Иерархия исключений
class CustomError(Exception):  # ✅ Используй наследование
    pass

# 3. Абстрактные базовые классы (для контракта)
from abc import ABC, abstractmethod

class Repository(ABC):
    @abstractmethod
    def get(self, id):
        pass

class UserRepository(Repository):  # ✅ OK для контракта
    def get(self, id):
        return db.query(User).filter_by(id=id).first()

Практический совет

# Используй это правило:
# ✅ Композиция по умолчанию
# ❌ Наследование только если:
#    1. Есть настоящее отношение IS-A
#    2. Иерархия максимум 2-3 уровня
#    3. Не нарушается LSP
#    4. Базовый класс не меняется часто

# Принцип SOLID (Single Responsibility)
# Композиция помогает соблюдать SRP лучше
class UserService:
    def __init__(
        self,
        repository: Repository,  # ← Inject
        email_sender: EmailSender,  # ← Inject
        logger: Logger  # ← Inject
    ):
        self.repo = repository
        self.email = email_sender
        self.log = logger
    
    def create_user(self, user_data):
        user = self.repo.save(user_data)
        self.email.send_welcome(user.email)
        self.log.info(f"User created: {user.id}")
        return user

# Легко тестировать, легко менять реализацию

Заключение

Наследование создаёт жёсткую, тяжело тестируемую иерархию. Композиция даёт гибкость, слабую связанность и легче следует SOLID принципам. Современный подход: используй композицию как основу, наследование только для реального IS-A отношения. Это кулинарный рецепт: сначала композиция, потом наследование если очень надо.

Что из себя представляет проблема: наследование vs композиция? | PrepBro