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

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

Давайте погрузимся в это!

Lv0 — Копировать/вставить лучше, чем ничего

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

Lv1 — Функциональное наследование лучше, чем копирование/вставка

Это все еще отвратительно, на мой взгляд, но, по крайней мере, код не дублируется.

Первая проблема заключается в том, что не у всех транспортных средств есть двигатели (например, у велосипеда их нет). Следовательно, по мере роста кодовой базы класс Vehicle будет иметь все больше и больше обязанностей (и больше if утверждений), в конечном итоге став объектом Бога.

Также мы видим проблему с GarageDoor; то, что у него есть двигатель, не делает его транспортным средством.

Lv2 — Черты и примеси лучше, чем функциональное наследование

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

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

Lv3 — Статические сервисы лучше, чем трейты и примеси

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

Использование статических сервисов решает эту проблему. Теперь классы Car и GarageDoor могут выбирать, какой метод они хотят предоставить, и просто делегировать поведение EngineService.

Недостатком этого подхода является введение статической связи. Когда в вашем приложении их слишком много, код становится жестким, и сложно вносить изменения, не вводя регрессии.

Lv4 — сервис-контейнер лучше, чем статические сервисы

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

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

Например, у вас может быть почтовая служба, которая отправляет настоящие электронные письма, определенные в классе ProductionContainer, и поддельная почтовая программа, которая вместо этого регистрирует их, определенную в LocalContainer.

Lv5 — внедрение зависимостей лучше, чем сервис-контейнер

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

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

Lv6 — инверсия зависимостей лучше, чем внедрение зависимостей

Этот подход аналогичен внедрению конкретного сервиса, но с небольшим нюансом: требуемый тип конструкторов Car и GarageDoor — это интерфейс.

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

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

Суть принципа инверсии зависимостей гласит:

  • Модули высокого уровня (например, Автомобиль) не должны зависеть от модулей низкого уровня (например, Двигатель). Оба должны зависеть от абстракций (таких как EngineInterface).
  • Абстракции не должны зависеть от деталей. Детали (конкретные реализации) должны зависеть от абстракций.

Какой уровень мне использовать?

Как и во всем в программировании, здесь нет прямого решения делать или не делать. Видите ли, система, использующая только 6-й уровень, инверсию зависимостей, несомненно, очень элегантна и надежна, но она также очень сложна и многословна. Иногда статический сервис может работать лучше. Здесь я хочу напомнить правило дизайна №1: не переусердствуйте.

Я призываю вас рассматривать все эти решения как дополняющие друг друга. За исключением нулевого уровня (копирование/вставка), по возможности избегайте этого. Довольно, пожалуйста 🙏

Не могли бы вы предложить улучшение этого списка?

Пожалуйста, оставьте комментарий ниже, чтобы сказать мне, что вы думаете, и не забудьте поставить лайк статье. Это мотивирует меня писать больше. 😅

Первоначально опубликовано на https://dev.to 14 июля 2021 г.