Композиция вместо Наследования в PHP

Источник: «Composition Over Inheritance in PHP»
В последнее время в сообществе PHP разработчиков ведутся дебаты о преимуществах и недостатках Композиции и Наследования. В мире объектно-ориентированного программирования (ООП) существует множество мощных инструментов и концепций, каждая из которых имеет свои сильные и слабые стороны и призвана улучшить читаемость, модульность и возможность повторного использования кода.

Прежде чем мы рассмотрим преимущества Композиции над Наследованием, давайте разберёмся в этих двух принципах независимо друг от друга.

Наследование

Это фундаментальная концепция ООП в PHP, представляющая собой технику, при которой объект или класс основывается на другом объекте (прототипическое наследование) или классе (наследование на основе класса), используя одну и ту же реализацию и дополняя её возможностью использовать, расширять и изменять поведение, определённое в других классах. Типичным примером является класс Vehicle, имеющий такие подклассы, как Car и Motorcycle, где последние наследуют свойства и методы класса Vehicle. Сила наследования заключается в его простоте и в том, что оно облегчает повторное использование кода, однако при неправильном использовании оно может привести к созданию жёстко связанных и негибких структур кода, известных как иерархии наследования (Inheritance Hierarchies).

Композиция

С другой стороны, Композиция — это ещё одна практика ООП, при которой класс формируется из нескольких более простых классов или объектов путём их объединения и сборки таким образом, чтобы смоделировать более сложное поведение. Вместо того чтобы создавать монолитные классы с наследуемыми атрибутами и методами, композиция работает за счёт содержания экземпляров других классов, реализующих требуемую функциональность.

Используя предыдущий пример, вместо класса Car, наследующего от Vehicle, мы можем иметь класс Vehicle, содержащий экземпляры Engine, Wheels и других необходимых компонентов. Такая организация обеспечивает высокую степень гибкости, позволяя более точно контролировать поведение объектов.

Композиция вместо Наследования

Принцип Композиция вместо Наследования — это рекомендация как можно чаще использовать Композицию вместо Наследования. Причина в том, что хотя наследование позволяет определить поведение класса в терминах другого класса, оно связывает жизненный цикл и уровень доступа родительского класса с дочерним классом, что может сделать код более жёстким. Композиция позволяет объектам получать поведение и возможности через отношения, а не через жёсткую структуру классов, что приводит к лучшей модульности и меньшему сцеплению между классами.

Использование Композиции вместо Наследования даёт более надёжное решение при разработке сложных приложений с большим количеством подвижных частей и поведений. Такой подход обеспечивает гибкость и масштабируемость, а также позволяет получить дизайн, в большей степени соответствующий реальному миру.

Ниже мы рассмотрим пример рефакторинга кода, использующего Наследование, для использования вместо него Композиции. Цель состоит не в том, чтобы пренебречь наследованием, а в том, чтобы показать, как композиция может обеспечить более надёжное и гибкое решение в определённых сценариях.

Рефакторинг от Наследования к Композиции

Представьте себе, что у нас есть система каталогизации и классификации птиц. Мы могли бы иметь что-то вроде этого:

// Пример Наследования

class Bird
{
public function fly(): string
{
return "I can fly!";
}
}

final class Pigeon extends Bird
{
}

$pigeon = new Pigeon();
echo $pigeon->fly(); // I can fly!

В приведённом примере мы имеем базовый класс Bird, содержащий метод fly(). Класс Pigeon наследуется от Bird и, следовательно, имеет доступ к методу fly(). Однако такой подход может стать проблематичным, если мы захотим добавить класс Penguin. Пингвины технически являются птицами, но они не летают.

final class Penguin extends Bird
{
}

$penguin = new Penguin();
echo $penguin->fly(); // I can fly!

Это можно исправить, переопределив метод fly(). Но в зависимости от размера и сложности проекта это может выйти из-под контроля. Мы можем доработать эту реализацию, чтобы вместо неё использовать Композицию:

// Пример Композиции

final class Bird
{
public function __construct(
protected BirdType $birdType,
) {}

public function fly(): string
{
return $this->birdType->fly();
}
}

interface BirdType
{
public function fly(): string;
}

final class FlyingBird implements BirdType
{
public function fly(): string
{
return "I can fly!";
}
}

final class NonFlyingBird implements BirdType
{
public function fly(): string
{
return "I can't fly!";
}
}

$pigeon = new Bird(new FlyingBird());
echo $pigeon->fly(); // I can fly!

$penguin = new Bird(new NonFlyingBird());
echo $penguin->fly(); // I can't fly!

В приведённом выше примере мы переработали наш код для использования Композиции. Теперь у нас есть интерфейс BirdType, обозначающий, может ли птица летать или нет, а класс Bird принимает объект BirdType в качестве аргумента своего конструктора. Теперь класс Bird имеет метод fly(), указывающий на метод fly() объекта BirdType. Такой подход позволяет получить больший контроль над отдельными функциями поведения, поскольку каждая функциональность инкапсулируется в своём классе. Это изменение позволяет нам добиться большей гибкости (теперь наш пингвин не может летать), а класс Bird обладает лучшей масштабируемостью, что облегчает управление уникальным поведением различных типов птиц.

Благодаря Композиции каждый класс несёт свою собственную ответственность (Принцип единой ответственности), а сложные модели поведения лучше управляются путём разделения их на отдельные сущности. Взаимодействие классов друг с другом становится более явным, и в итоге мы получаем структуру кода, которую легче поддерживать и расширять, избегая жёсткой структуры наследования.

Заключение

В области ООП обе концепции являются жизненно важными инструментами в инструментарии разработчика. В этой статье я попытался показать преимущество Композиции над Наследованием. Хотя Наследование — это действительно интуитивный и элегантный способ организации и повторного использования кода, в зависимости от контекста и способа реализации оно может стать ограничивающим и проблематичным, когда классы становятся тесно связанными и слишком зависимыми от жёсткой иерархической структуры.

При разработке реальных PHP-приложений обычно используются сложные классы с многочисленными поведенческими функциями. Как показано в примере, использование Композиции вместо Наследования делает код более надёжным, масштабируемым и управляемым. Это приводит к тому, что код становится проще расширять и поддерживать, усиливая модульность и инкапсуляцию.

Помните, что выбор правильной техники или концепции в значительной степени зависит от конкретного контекста и общих требований к проекту. Цель данной статьи — не игнорировать наследование, а понять, как Композиция обеспечивает гибкую альтернативу в конкретных ситуациях.

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

Надеюсь, что Вам понравилась эта статья, и если да, то не забудьте поделиться этой статьёй со своими друзьями!!! До встречи!

Дополнительные материалы

Предыдущая Статья

Избегайте AOP: Array-Oriented Programming

Следующая Статья

Размещение NULL значений для ORDER BY с nullable столбцами