Агрегация и её отличие от композиции
В прошлом уроке мы освоили композицию — объект создаёт свои части внутри себя. Сегодня разберём агрегацию: когда части могут жить отдельно от целого. Разница кажется мелкой — но именно она определяет, как грамотно проектировать базы данных, ORM-модели и целые системы.
🔑 Ключевая идея: жизненный цикл объектов
И композиция, и агрегация описывают отношение «целое — часть». У дома есть комнаты, у команды есть игроки, у отдела есть сотрудники. Технически оба подхода похожи. Но между ними есть одна фундаментальная разница:
Могут ли «части» существовать без «целого»?
Если нет — это композиция. Если да — это агрегация.
Композиция
Части принадлежат целому
Дом уничтожен → комнат тоже нет.
Комнаты были созданы внутри дома и без него не существуют.
Объект СОЗДАЁТСЯ внутри __init__
Агрегация
Части живут самостоятельно
Команда расформирована → игроки живы.
Они существовали до команды и продолжат жить после.
Объект ПЕРЕДАЁТСЯ снаружи
Именно жизненный цикл частей отличает эти два паттерна. Давайте посмотрим на оба — сначала в жизни, потом в коде.
🏠 Композиция: дом создаёт свои комнаты
Комнаты — это неотъемлемая часть дома. Они создаются вместе с домом, и без дома они существовать не могут. В коде это выражается просто: объекты-части создаются внутри метода __init__.
class Room:
"""Комната — часть дома."""
def __init__(self, name, area):
self.name = name # название: "Спальня", "Кухня" и т.д.
self.area = area # площадь в кв.м
def __repr__(self):
return f"Room('{self.name}', {self.area} м²)"
class House:
"""Дом — владелец своих комнат."""
def __init__(self, address):
self.address = address
# ✅ КОМПОЗИЦИЯ: комнаты СОЗДАЮТСЯ внутри __init__
# Они принадлежат этому конкретному дому и больше никому.
self.rooms = [
Room("Гостиная", 25),
Room("Спальня", 18),
Room("Кухня", 12),
]
def total_area(self):
return sum(r.area for r in self.rooms)
def describe(self):
print(f"Дом по адресу: {self.address}")
print(f"Комнаты ({len(self.rooms)}):")
for r in self.rooms:
print(f" - {r.name}: {r.area} м²")
print(f"Общая площадь: {self.total_area()} м²")
house = House("ул. Пушкина, 10")
house.describe()
# Дом по адресу: ул. Пушкина, 10
# Комнаты (3):
# - Гостиная: 25 м²
# - Спальня: 18 м²
# - Кухня: 12 м²
# Общая площадь: 55 м²
# Когда дом «уничтожается» (выходит из области видимости),
# комнаты тоже исчезают — у них нет жизни вне этого дома.
del house
# Теперь ни house, ни его комнаты недоступны.
🔍 Обратите внимание: никто снаружи не передаёт комнаты в дом. Дом сам создаёт Room(...) внутри __init__. Это и есть признак композиции — объект сам порождает свои части.
⚽ Агрегация: команда принимает готовых игроков
Игрок существует сам по себе: у него есть имя, возраст, история. Он может войти в команду, потом перейти в другую, потом оказаться в третьей. Команда не создаёт игроков — она их принимает извне. Это агрегация.
class Player:
"""Игрок — самостоятельный объект. Живёт независимо от команды."""
def __init__(self, name, position):
self.name = name
self.position = position # позиция: "нападающий", "вратарь" и т.д.
def __repr__(self):
return f"Player('{self.name}', '{self.position}')"
class Team:
"""Команда — агрегирует игроков, но не владеет ими."""
def __init__(self, name):
self.name = name
# ✅ АГРЕГАЦИЯ: список пустой — игроки добавляются снаружи
self.players = []
def add_player(self, player):
"""Принимаем готового игрока извне."""
self.players.append(player)
print(f"{player.name} вступил в команду '{self.name}'")
def remove_player(self, player):
"""Игрок уходит из команды, но сам объект player НЕ удаляется."""
self.players.remove(player)
print(f"{player.name} покинул команду '{self.name}'")
def describe(self):
print(f"\nКоманда: {self.name} ({len(self.players)} игроков)")
for p in self.players:
print(f" - {p.name} ({p.position})")
# Создаём игроков ОТДЕЛЬНО — они существуют сами по себе
player1 = Player("Иванов", "нападающий")
player2 = Player("Петров", "защитник")
player3 = Player("Сидоров", "вратарь")
# Создаём команду
team = Team("Динамо")
# Передаём игроков в команду — АГРЕГАЦИЯ
team.add_player(player1) # Иванов вступил в команду 'Динамо'
team.add_player(player2) # Петров вступил в команду 'Динамо'
team.add_player(player3) # Сидоров вступил в команду 'Динамо'
team.describe()
# Команда расформирована — объект удалён
del team
# НО игроки живы! Они по-прежнему доступны:
print(player1) # Player('Иванов', 'нападающий')
print(player2) # Player('Петров', 'защитник')
# Один игрок может быть в нескольких командах одновременно
team_a = Team("Спартак")
team_b = Team("ЦСКА")
team_a.add_player(player1) # Иванов вступил в команду 'Спартак'
team_b.add_player(player1) # Иванов вступил в команду 'ЦСКА'
# player1 теперь числится в обеих командах —
# при композиции такое невозможно!
✅ Два признака агрегации в этом коде:
1. Игроки созданы до команды и за пределами класса Team.
2. После del team объекты player1, player2, player3 живы — они не были «собственностью» команды.
🔬 Сравнение бок о бок: Department и Employee
Посмотрим на ещё один пример — отдел компании и сотрудников. Сотрудники — самостоятельные люди: они существовали до прихода в отдел и продолжат жить после увольнения. Отдел их агрегирует, а не порождает.
class Employee:
"""Сотрудник — полностью самостоятельный объект."""
def __init__(self, name, role):
self.name = name
self.role = role
def work(self):
print(f"{self.name} ({self.role}) работает")
def __repr__(self):
return f"Employee('{self.name}', '{self.role}')"
class Department:
"""Отдел агрегирует сотрудников.
Принципиально: сотрудники ПЕРЕДАЮТСЯ снаружи,
не создаются внутри __init__.
"""
def __init__(self, name):
self.name = name
self.employees = [] # изначально пусто
def hire(self, employee):
"""Принять сотрудника на работу в отдел."""
self.employees.append(employee)
def fire(self, employee):
"""Уволить сотрудника — он уходит, но как объект продолжает жить."""
self.employees.remove(employee)
def daily_standup(self):
print(f"\n📋 Стендап отдела '{self.name}':")
for emp in self.employees:
emp.work()
# --- Создаём сотрудников независимо от отделов ---
alice = Employee("Алиса", "бэкенд-разработчик")
bob = Employee("Боб", "дизайнер")
carol = Employee("Кэрол", "тестировщик")
dave = Employee("Дэйв", "аналитик")
# --- Создаём отделы ---
dev_dept = Department("Разработка")
qa_dept = Department("QA")
# --- Наполняем отделы через агрегацию ---
dev_dept.hire(alice)
dev_dept.hire(bob)
dev_dept.hire(carol)
qa_dept.hire(carol) # ← Кэрол работает сразу в двух отделах!
qa_dept.hire(dave)
dev_dept.daily_standup()
# 📋 Стендап отдела 'Разработка':
# Алиса (бэкенд-разработчик) работает
# Боб (дизайнер) работает
# Кэрол (тестировщик) работает
qa_dept.daily_standup()
# 📋 Стендап отдела 'QA':
# Кэрол (тестировщик) работает
# Дэйв (аналитик) работает
# Отдел ликвидирован — сотрудники никуда не делись
del dev_dept
print(alice) # Employee('Алиса', 'бэкенд-разработчик') — жива!
print(carol) # Employee('Кэрол', 'тестировщик') — жива!
💡 Ключевые наблюдения:
1. carol числится одновременно в двух отделах — это возможно только при агрегации.
2. После del dev_dept объекты alice, bob, carol живы.
3. В __init__ класса Department нет ни одного вызова Employee(...) — это и есть маркер агрегации.
А теперь сравним оба подхода в единой таблице, чтобы разница отложилась в памяти навсегда.
📊 Сравнительная таблица
📐 Как это выглядит на UML-диаграммах
UML (Unified Modeling Language) — стандартный язык для рисования схем объектных систем. Когда вы рисуете схему классов на доске или в документации, для связей «целое — часть» используют ромбики.
◆ Закрашенный ромб
Рисуется у целого. Означает композицию: части не могут жить без владельца.
House ◆── Room
◇ Незакрашенный ромб
Рисуется у целого. Означает агрегацию: части живут независимо.
Team ◇── Player
⚠️ Честное предупреждение для новичков: на реальных проектах UML-диаграммы рисуют не всегда — многие команды обходятся без них. Но понимать эти обозначения важно, потому что в книгах, документации и обсуждениях архитектуры вы будете их встречать.
🐍 Важное уточнение: в Python разница концептуальная
Вот правда о Python, которую важно знать: язык сам по себе не enforces разницу между композицией и агрегацией. Python не выдаст ошибку, если вы напутали. Оба подхода технически реализуются одинаково — через атрибуты и ссылки.
🤔 Тогда зачем вообще различать?
Потому что разница важна при проектировании, а не при написании кода. Правильный выбор влияет на:
— структуру базы данных (внешние ключи, CASCADE DELETE)
— ORM-модели (Django: ForeignKey vs вложенный объект)
— управление памятью в больших системах
— понимание кода другими разработчиками
# Разница в коде — одна строка, но она говорит о многом
# --- КОМПОЗИЦИЯ: объект создаётся ВНУТРИ ---
class House:
def __init__(self, address):
self.address = address
self.rooms = [Room("Гостиная", 25), Room("Спальня", 18)]
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# Room создаётся здесь — внутри House
# --- АГРЕГАЦИЯ: объект ПЕРЕДАЁТСЯ снаружи ---
class Team:
def __init__(self, name):
self.name = name
self.players = [] # пусто — заполняется снаружи
def add_player(self, player):
self.players.append(player)
# ^^^^^^
# player уже существовал до этого вызова
# Наглядное сравнение при создании объектов:
# Композиция — создаём только «целое»:
house = House("ул. Лесная, 5")
# Комнаты создаются автоматически внутри
# Агрегация — создаём части отдельно, потом собираем:
p1 = Player("Марков", "нападающий")
p2 = Player("Нилов", "вратарь")
team = Team("Локомотив")
team.add_player(p1)
team.add_player(p2)
💡 Простой способ запомнить:
Видите self.parts = [SomeClass(...)] внутри __init__? → Это композиция.
Видите self.parts = [] и метод add(item)? → Это агрегация.
🏗 Зачем это нужно в реальной разработке
Разберём три конкретные области, где понимание этой разницы помогает принимать правильные решения.
🗄 Проектирование БД
Композиция → CASCADE DELETE: удалил заказ — удалились все строки заказа автоматически.
Агрегация → SET NULL или RESTRICT: удалил команду — игроки остаются в базе.
🔧 ORM (Django)
Композиция → ForeignKey(on_delete=CASCADE).
Агрегация → ManyToManyField или ForeignKey(on_delete=SET_NULL): объект живёт после удаления родителя.
🧠 Архитектурные решения
Неправильный выбор → баги при удалении данных, утечки памяти, нарушения бизнес-логики. Правильный выбор → предсказуемая, надёжная система.
📖 Пример из жизни Django-разработчика:
Вы проектируете интернет-магазин. Order (заказ) содержит OrderItem (строки заказа) — это композиция: строки заказа существуют только в рамках заказа. Логично использовать CASCADE.
Но Product (товар) входит во многие заказы — это агрегация. Нельзя удалить товар и потерять все исторические заказы! Здесь нужен PROTECT или SET_NULL.
✅ Итоги урока
- Главный вопрос: могут ли «части» существовать без «целого»? Нет → композиция. Да → агрегация.
- Композиция в коде: части создаются внутри
__init__—self.rooms = [Room(...)]. - Агрегация в коде: части передаются снаружи — список пуст в
__init__, заполняется через методadd(). - Агрегация позволяет: части жить после удаления целого; одна часть — в нескольких «целых» одновременно.
- В Python: язык не enforces разницу технически — она остаётся концептуальной, но от этого не менее важной.
- UML: ◆ закрашенный ромб — композиция; ◇ незакрашенный — агрегация.
- В реальных проектах: правильный выбор влияет на структуру БД, ORM-политики удаления и архитектуру систем.
👉 В следующем уроке мы завершим модуль о связях между классами и сведём воедино все изученные паттерны: наследование, ассоциацию, композицию и агрегацию. Вы научитесь смотреть на задачу и сразу выбирать правильный тип связи.