На своем примере я объясню разницу между двумя важными концепциями объектно-ориентированного программирования: композицией и наследованием. Оба механизма используются для построения новых объектов и устанавливают отношения между ними. Однако преимущества использования той или иной техники не всегда очевидны.
Наследование как инструмент создания новых объектов
С помощью механизма наследования мы можем создавать новые объекты на основе старых.
Предположим, у нас есть класс Animal
, на основе которого мы хотим создать объект Cat
.
import random
class Animal:
hungry = random.randint(0, 10)
def eat(self):
self.hungry -= 1
Классу Animal
присуще поведение всех животных. Например: «все животные едят» — это утверждение не противоречит и в отношении кошек. Но кошки ещё и мяукают, чего нельзя сказать обо всех остальных животных. Поэтому в классе Cat
мне нужно добавить способность мяукать, при этом, сохранив способность питаться. Для этого существует механизм наследования:
class Cat(Animal):
def meow(self):
print("meow!")
Несмотря на то, что в коде класса Cat
не присутствует метод eat
, при создании объекта он появится в качестве ссылки на класс Animal
:
barsik = Cat()
barsik.hungry
barsik.eat()
barsik.hungry
barsik.meow()
7
6
meow!
Благодаря механизму наследования я могу создать класс Dog
, который будет, к примеру, гавкать и не потеряет способности кушать.
Выводы
Наследование расширяет родительский класс дочерним.
В дочернем классе появляются методы и атрибуты родительского.
Дочерний класс всегда зависит от родительского.
Методы и атрибуты родительского класса становятся собственными методами дочернего.
Композиция как средство расширения существующих объектов
В отличие от наследования, композиция не связана с иерархией классов. Она не предназначена для создания новых объектов, а скорей для расширения существующих.
Предположим, что у нашей кошки есть хозяин — довольно сложный объект. Сложный потому, что это не просто атрибут, который можно добавить в класс Cat
. Это полноценный объект со своим поведением.
Ключевые слова в принятии решения в пользу композиции — это: «довольно сложный объект». Иначе говоря, это класс, который не наследуется явно, а встраивается в существующий в качестве атрибута или сам содержит таковой.
class CatOwner:
age = random.randint(0,99)
name = ""
surname = ""
profession = ""
cat = Cat() # композиция
def feed_cat(self):
self.cat.eat()
Ещё один способ понять, что вам нужна композиция — это неуместность наследования. Наследование предполагает однородность. Владелец кошки и кошка не одного рода или ещё: владелец кошки — это не животное (не наследует класс Animal
). Вместо этого: у владельца есть кошка; или конкретней: владелец состоит из кошки (а также из своих атрибутов).
Выводы
Композиция связывает два разнородных объекта.
Композиция подходит для «сборки» сложного объекта из более простых.
Композиция позволяет вызывать методы и читать атрибуты одного объекта из другого.
При композиции объекты могут сохранять независимую инициализацию (не зависят друг от друга).
Реальный пример
Выше была теория, а теперь я покажу свою оплошность, найденную в коде старого репозитория.
Итак, у меня есть класс MyBot
, в котором я ошибочно использовал композицию:
import telegram
class MyBot(NetworkExceptionHandler):
"""
RSS/Atom feed bot for Telegram. Processes images for the RSS entries,
Attaches the text message and buttons for the Telegram message.
Sends it to the channel.
"""
def __init__(self) -> None:
super().__init__()
self._bot = telegram.Bot(token='1367659674:Wb0q07PCbuZDu9LR5BF01i66Fow0UXjv')
В последней строке я делаю объект класса Bot
модуля telegram
скрытым атрибутом собственного класса! Не знаю что на меня нашло, но когда я это увидел, я в спешке бросился клонировать репозиторий на локальную машину с целью исправить это недоразумение.
Собственно, недоразумение состоит в том, что класс MyBot
в качестве композиции использует почти одноименный класс Bot
, то есть класс, равный ему по своему качеству. Гипотетически «бот может состоять из (другого) бота» в случае композиции, но, согласитесь, куда логичней утверждение, что «мой бот — это бот Telegram». А значит, речь идёт об однородных объектах как это бывает в случае наследования. Более того, при наследовании, MyBot
расширял бы класс Bot
модуля telegram
и сам бы он являлся просто ботом, а не «ботом в боте» как в случае с композицией.
Здесь я могу только процитировать:
Простое лучше, чем сложное.
Плоское лучше, чем вложенное.
Несмотря на то, что текущий код полностью работоспособен, он создаёт много проблем при чтении, а читаемость, как известно, имеет значение.
Рефакторинг
У этой проблемы на удивление простое решение. Для этого достаточно наследовать класс Bot
и инициализировать его в своем классе, при этом, передав в него значение параметра с помощью ключевого аргумента:
class MyBot(NetworkExceptionHandler, telegram.Bot):
def __init__(self) -> None:
super().__init__(token='1367659674:Wb0q07PCbuZDu9LR5BF01i66Fow0UXjv')
В итоге я выиграл лишнюю строчку кода!
Кстати, на момент разработки этого проекта я был знаком с рассмотренными нюансами, но это не спасло меня от стилистической ошибки, что в очередной раз подтверждает необходимость осознанного подхода к использованию рассмотренных механизмов объектно-ориентированного программирования.