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

У каждой машины есть функция привода: водитель нажимает педаль акселератора, и машина движется.

Итак, вы создаете базовый класс под названием Car, в котором есть функция drive ().

Вы реализуете метод Drive () в этом базовом классе, чтобы все подклассы автомобилей, унаследованные от этого базового класса Car, получали его, поскольку это общая функциональность. Вы очень довольны тем, что пишете повторно используемый код.

Клиент хочет автомобиль, который может ездить на бензине, а другой - на электричестве.

Вы создаете для них два класса:

Автомобиль с бензиновым двигателем необходимо заправлять топливом, а электромобиль - заряжать, поэтому в каждом классе используются два метода.

Ваше приложение работает должным образом, и клиент доволен, что, в свою очередь, делает счастливыми вас и вашего начальника.

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

Гибрид должен работать как на бензине, так и на электричестве. Вдруг у вас возникла проблема. Вы найдете три способа вписать это в вашу существующую модель.

Либо наследуйте от класса Petrol Car, либо продублируйте код для Charge ().

Или вы можете унаследовать от класса Electric Car и продублировать код для Refuel ().

Третий вариант, который вы видите, и, безусловно, самый популярный, - это наследование от класса Car и дублирование кода для Refuel () и Charge ().

На мой взгляд, это решение является наиболее разумным из всех трех. Гибридный автомобиль - это не только электромобиль, он должен быть производным от класса электромобилей; то же самое относится к классу автомобилей с бензиновым двигателем. Однако по-прежнему происходит много дублирования кода. Изменения в логике заправки теперь необходимо вносить в нескольких местах, это может вызвать проблемы во время технического обслуживания. Вы принимаете риск, полагая, что в будущем не будет больших изменений. Как раз тогда, когда вы были довольны своей работой, к вам подходит клиент и говорит, что ему нужна игрушечная машинка прямо сейчас. Игрушечная машинка особенная. У него не будет функции Диска. И вот так модель, которую вы создали, рушится. Теперь метод Drive () нужно будет продублировать для всех классов автомобилей, кроме класса Toy Car. Вы видите, как мы сейчас идем по скользкой дорожке, верно?

Мы видим образец отношений IS-A; и электромобиль - это автомобиль. Это корень проблемы. Мы должны избавиться от такого количества отношений IS-A и составить объект автомобиля с отношениями HAS-A. Отношения HAS-A можно интерпретировать как поведение автомобиля HAS A drive. Мы можем создать отдельные интерфейсы для каждого поведения:

Теперь мы можем составить автомобильный класс с таким поведением:

Это означает, что объект car состоит из вышеуказанных функций. Это не конкретная реализация, а скорее абстракции, позволяющие подключать любое поведение в зависимости от необходимости. Может быть любое количество конкретных реализаций поведения:

Каждый класс Refuel Behavior должен иметь метод Refuel. Класс NoRefuelBehaviour также является поведением Refuel в том смысле, что он заявляет об отсутствии такого поведения в объекте.

Мы берем конкретную реализацию интерфейсов, которые у нас есть в классе Car, и внедряем их, чтобы составить автомобили со смешанными функциями.

Состав может выглядеть так:

Бензиновый автомобиль = SimpleDriveBehaviour + RefuelPetrolBehaviour + NoChargeBehaviour

Электромобиль = SimpleDriveBehaviour + NoRefuelBehaviour + FastChargeBehaviour

Гибридный автомобиль = SimpleDriveBehaviour + RefuelDieselBehaviour + SimpleChargeBehaviour

Игрушечная машина = NoDriveBehaviour + NoRefuelBehaviour + NoChargeBehaviour

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

Заключение

В этой статье мы рассмотрели очень простой пример. Даже в этой простой модели дерево наследования сломалось, и мы рассмотрели, как исправить это с помощью композиции.

Вы можете подумать, но Танвир, разве наследование не является одним из столпов объектно-ориентированного программирования, вы говорите нам не использовать его?

Все, что я говорю, как и многие другие разработчики, - предпочитаю композицию наследованию. В настоящее время я большую часть времени работаю с Golang, и он не поддерживает наследование. Это заставляет разработчиков вместо этого использовать композицию. Это приводит к гораздо лучшему дизайну программного обеспечения. Я считаю, что не будет преувеличением сказать, что нам даже не нужно наследование.

Это были мои мысли о композиции и наследовании, а что вы думаете? Оставьте комментарий ниже или напишите мне в Твиттере на @ war1oc.

Если вы обнаружите какую-либо аномалию, мы будем благодарны за обратную связь!