Недавно я получил электронное письмо от моего друга, который спрашивал мое мнение о Composition Vs. Наследование. Мой друг только что наткнулся на фразу «предпочитаю композицию наследованию». Теперь мой друг сказал что-то интересное, что я, возможно, раньше не рассматривал и не ценил, он сказал: «…Я изо всех сил пытаюсь понять, как можно сравнивать одно с другим…». На первый взгляд это выглядит просто, но на самом деле я остановился и задумался. Как их можно сравнивать друг с другом? В этом посте я утверждаю, что они несопоставимы, так как решают принципиально разные задачи. Я утверждаю, что хотя наследование можно использовать для решения проблем, которые решает композиция, это побочный эффект наследования.

Я собирался сделать здесь отступление о своих взглядах до и после ответа моему другу, но, честно говоря, это пост. Достаточно сказать, что моя точка зрения не изменилась, мои аргументы, лежащие в основе моей точки зрения, значительно возросли и развились.

Давайте сначала дадим несколько кратких определений, они упрощены, и я не буду вдаваться в подробности.

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

Наследование — это когда класс либо расширяет другой класс, либо абстрактный класс, таким образом получая свой набор поведения и состояния и классифицируясь как его версия. Или это когда класс реализует интерфейс (контракт), который определяет, какое поведение должно быть выражено, и оставляет реализацию за реализующим классом. Например, в игре NPC будет реализовывать персонажа, который имеет такие способности, как движение и состояние, такое как здоровье или выносливость.

Сочинение

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

Какие проблемы они решают?

Итак, в чем они не будут сопоставимы? Рассмотрим проблемы, которые решает каждый.

Наследование принципиально решает проблему наследственности и происхождения, а также решает проблему классификации и группировки, хороший пример этого - в биологии и зоологии.

С другой стороны, композиция имеет дело с проблемой необходимости изолировать другое поведение от вещей, предназначенных для этого поведения. Зачем нам вообще это делать? Давайте посмотрим, как возникла композиция. В начале был один мастер-файл, в этом файле была массивная последовательность кода, которая выполнялась. Повсюду были переходы и разрывы, чтобы попытаться направить выполнение на другой фрагмент кода. Это было сложно, трудно читать, и многие разработчики думали, что это чепуха. Затем были функции, способы разделения различного поведения на небольшие читаемые блоки, на которые было легко ссылаться, мы могли классифицировать и группировать наш код во что-то значимое, например, функцию для последовательного и стандартного расчета процентов. Затем мы начали группировать их в отдельные файлы, опять же, чтобы их было легко читать и понимать, и, в конце концов, где-то там, по крайней мере, для ООП, были определены и разработаны классы.

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

Наследование заключается в указании происхождения и наследственности и обеспечении того, чтобы производные одной вещи вели себя одинаково.

Так в чем проблема?

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

На это есть несколько причин:

  1. Учили неправильно о двух
  2. скучно и хочется развлечься
  3. поглаживание эго

Первая причина проста и понятна: неправильное обучение приводит к привычкам на всю жизнь, от которых трудно избавиться.

Вторая причина заключается в том, что вещи становятся интересными. Давайте будем честными, как только вы напишете свою 200-ю CRUD-систему или 500-й REST API, все станет скучно, как видите. Контроллер -> Сервис -> Репозиторий. Что существует базовый шаблон для решения почти 70% проблем, с которыми сталкивается бизнес, между каждым из них есть уровень сопоставления для перевода из одной формы в другую, а в сервисе будет некоторая бизнес-логика, которая устанавливает определенное состояние на основе определенные правила. Здесь нет ничего интересного, здесь нет ничего веселого, все скучно (и это хорошо!). Подходящий старший разработчик должен размять ноги, ему нужно играть и учиться, это жизнь разработчика программного обеспечения. Так что же делать разработчику, когда он решил проблемы и просто делает то же самое? Они играют, те делают наследство. Я буду первым, кто признает, что создание привлекательного фреймворка с использованием наследования и его расширяемость, почти как волшебство, — это действительно весело! Как и серьезное развлечение, он удовлетворяет определенную потребность в создании сложных и элегантных систем из ничего, требует определенного уровня интеллекта, чтобы следовать и понимать, и это заставляет нас чувствовать себя хорошо. Это заставляет нас чувствовать, что мы настоящие инженеры-программисты. Проблема в том, что это слишком сложно для ситуации. Создаваемому решению не нужен фреймворк, ему не нужно все наследование. Чувства разработчика не являются целью продуктов.

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

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

Помогите мне выбрать

У меня есть несколько простых правил, которые я применяю, чтобы определить, действительно ли ситуация требует наследования, а не композиции. Если одно из этих правил соблюдается, то наследование рассматривается как вариант, если нет, то нет.

  1. Если я использую интерфейс, то я никогда не должен заботиться о порядке, в котором выполняются реализующие классы. Мне никогда не нужно выбирать только один тип этого интерфейса, и мне никогда не нужно приводить к разным объектам в зависимости от того, с какой реализацией этого интерфейса я имею дело.
  2. Если я использую абстрактный класс или интерфейс, то я всегда без исключения должен иметь возможность заменить любую другую его реализацию для достижения того же желаемого результата. Меня никогда не должно волновать, какой из них заменен.

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

  1. У меня, вероятно, есть какой-то список/набор, который я повторяю неупорядоченным образом, я никогда не забочусь о реализации, и я никогда не пытаюсь навязать случайный порядок, например, помещая сначала что-то в список (в тот момент, когда я это делаю, затем Я неправильно применил интерфейс, и его там вообще не должно быть)
  2. Я создаю некую обобщенную структуру, и в этом случае структура должна быть автономной библиотекой, полностью внешней, и она не должна меняться в соответствии с бизнес-правилами. Библиотека должна быть применима к нескольким различным экземплярам программного обеспечения и проектам.

В случае 2, если это что-то вроде интерфейса поверх доступа к БД, то это меня просто не устраивает. Это просто совершенно нереально! Часто используемое здесь оправдание состоит в том, что вы можете захотеть изменить базовую базу данных в любой момент. Честно! Это никогда, никогда, никогда не произойдет, и если это произойдет, то, скорее всего, вам все равно придется изменить загрузку кода, все, что это делает, это добавляет дополнительную сложность для рассмотрения!

В наши дни мы живем на территории микросервисов, а это означает, что потребность во фреймворках внутри отдельного микросервиса должна быть нулевой. Хорошее эмпирическое правило, которое я применяю, заключается в том, что если микросервис хорошо сделан и хорошо понят (реализует дискретный вертикальный срез хорошо известных бизнес-функций), то его дешевле переписать, чем реорганизовать. К сожалению, это означает, что большинство правил объектно-ориентированного программирования, которые у нас есть, выходят за рамки, поскольку они просто излишни для того, что мы пытаемся сделать.

Я довольно увлечен всем этим, в основном потому, что все эти вещи я открыл для себя трудным путем в своем собственном программировании. Раньше я был основным сторонником повсеместного использования наследования, но по мере того, как я становился лучше в том, что я делаю, и по мере того, как я испытывал больше, я теперь кодирую для удобочитаемости. Если код выглядит поразительно легко поддающимся рефакторингу, то, вероятно, это действительно хороший код, и его не следует подвергать рефакторингу, потому что нет никакого рефакторинга, который можно было бы сделать, чтобы упростить чтение этого кода. В наши дни всю оптимизацию выполняет компилятор, и большая часть программного обеспечения дважды компилируется с помощью различных форм байт-кода и JIT-компиляции, оптимизация для компьютера просто не требуется, а если и требуется, то обычно это решается не такими вещами, как OO, а такими вещами. как объединение и кэширование.

Вывод

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

Если вам нужен совет или начальная бесплатная консультация по некоторым из этих вещей и некоторым проблемам, которые возникают у вас с качеством кода в вашем бизнесе, пожалуйста, свяжитесь с нами: [email protected] — адрес электронной почты, который вам нужен, пишите мне письмо, и я свяжусь с вами.