Композиция и агрегация: альтернатива наследованию

Когда наследование — не панацея 🧐

Наследование в Python — мощный инструмент, но часто его используют там, где подошли бы композиция или агрегация. Представьте: вы проектируете игру и создаёте класс Dragon, наследуя его от FlyingCreature и FireBreathing. А что если дракон научится плавать? Придется перестраивать всю иерархию!

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

  • Жёсткая связь между классами
  • Раздувание иерархии (болезнь «горизонтального наследования»)
  • Сложности при изменении родительских классов

Композиция: «имеет» вместо «является» 🧩

Композиция — это когда объект содержит другие объекты как части. Пример из реальности: автомобиль имеет двигатель, а не является двигателем.

class Engine:
    def start(self):
        return "Двигатель заведён!"

class Car:
    def __init__(self):
        self.engine = Engine()  # Композиция!

    def start(self):
        print(f"Машина: {self.engine.start()}")

tesla = Car()
tesla.start()  # Машина: Двигатель заведён!

Плюсы композиции:

  • Гибкость: можно менять компоненты на лету
  • Проще тестировать (мокируем отдельные компоненты)
  • Избегаем «алмаза смерти» (diamond problem)

Агрегация: слабая форма композиции 🤝

Агрегация — когда объект содержит ссылку на другой объект, но они могут существовать отдельно. Например, университет и студенты:

class Student:
    def __init__(self, name):
        self.name = name

class University:
    def __init__(self):
        self.students = []  # Агрегация!

    def add_student(self, student):
        self.students.append(student)

harvard = University()
alice = Student("Alice")
harvard.add_student(alice)  # Студент существует вне университета

Разница с композицией: если уничтожить университет, студенты останутся (при композиции — двигатель автомобиля уничтожится с ним).

Практический пример: RPG-персонаж 🎮

Допустим, создаём персонажа для игры. С наследованием получилась бы сложная иерархия (Warrior > Magician > Hybrid), но с композицией всё элегантнее:

class Weapon:
    def attack(self):
        raise NotImplementedError

class Sword(Weapon):
    def attack(self):
        return "Удар мечом!"

class Spell:
    def cast(self):
        return "Кастует фаербол!"

class Character:
    def __init__(self):
        self.weapon = None
        self.spell = None

    def equip_weapon(self, weapon: Weapon):
        self.weapon = weapon

    def learn_spell(self, spell: Spell):
        self.spell = spell

    def fight(self):
        if self.weapon:
            print(self.weapon.attack())
        elif self.spell:
            print(self.spell.cast())

hero = Character()
hero.equip_weapon(Sword())
hero.fight()  # Удар мечом!
hero.learn_spell(Spell())
hero.fight()  # Кастует фаербол!

Когда что выбирать? 🤔

Наследование — когда отношение «является» и нужно полиморфное поведение.
Композиция — когда отношение «имеет» и важна инкапсуляция.
Агрегация — когда объекты логически связаны, но могут существовать отдельно.

Как говорит Данила Бежин в своём курсе по ООП: «Композиция часто делает код более предсказуемым, чем глубокие иерархии наследования».

Ваш ход! ✨

Попробуйте переделать этот пример с наследования на композицию:

class Animal:
    def make_sound(self):
        pass

class Dog(Animal):
    def make_sound(self):
        return "Гав!"

class Cat(Animal):
    def make_sound(self):
        return "Мяу!"

Подсказка: создайте класс SoundMaker и включайте его в животные как компонент.

Скрыть рекламу навсегда

🧠 Учёба без воды и зубрёжки

Закрытый Boosty с наработками опытного преподавателя.

Объясняю сложное так, чтобы щелкнуло.

🚀 Забрать доступ к Boosty