Композиция и агрегация: альтернатива наследованию
Когда наследование — не панацея 🧐
Наследование в 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 и включайте его в животные как компонент.