Design Patterns

Я, конечно же, не Майкл Фелпс, да и плавать больше предпочитаю в море, но мне очень нравится наблюдать за тренировками пловцов. Этот процесс словно филигранный часовой механизм, работу которого можно созерцать бесконечно. И каждый пловец подобен важной детали в нем. Кто-то плывет быстрее, а кто-то — медленнее. Кто-то совершает “лягушачьи удары” ногами. А есть те, кто, подобно китам, выпускает маленькие струйки воды. Но самое сильное впечатление производит пловец, выпрыгивающий из воды словно скат манта и снова погружающийся в голубую гладь. Но все из них постоянно плавают от одного конца бассейна к другому и обратно (все носят плавательные шапочки и очки, и вроде все на одно лицо).

Согласно Google, существуют 4 известных стиля плавания: кроль на груди (или вольный стиль — Freestyle, так как он доминирует на соревнованиях, например в триатлоне, где выбор стиля остается за спортсменами), кроль на спине — Backstroke, брасс — Breaststroke и баттерфляй — Butterfly (хотя, на мой взгляд, для описания этого стиля больше подходит сравнение с крыльями-плавниками манты).

Обзор данных стилей плавания был подготовлен на основе краткого руководства для начинающих. Описание каждого стиля включает breathing (технику дыхания) и body movement (движение тела), arm movement (движение рук) и leg movement (движение ног). Напоминает классический случай наследования для повторного использования кода, не правда ли?

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

К сути проблемы

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

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

Рассмотрим реализацию вышеуказанного руководства по плаванию. Мы можем переопределить каждое поведение при помощи определенных классов Swimming Stroke. Предположим, что flutter kick (порхающий удар ног) по умолчанию является leg movement, тогда whip kick (хлыстообразный удар ног) и dolphin kick (дельфиноподобный удар ног) будут переопределяемыми типами поведения. Баттерфляй, объединяющий в себе работу рук наподобие движения плавников манты и удары ногами по образу дельфиньего хвоста, — это ли не пример конфуза с выбором названия.

Пока все идет хорошо. Теперь вам нужно добавить дополнительный класс Swimming Stroke, к примеру назовем его Pupa (куколка), который предполагает исключительно dolphin kick. Полагаю, что вы включите еще один базовый уровень (или просто скопируете реализацию leg movement класса Breaststoke).

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

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

Шаблон проектирования Стратегия (Strategy) — это попытка обеспечить возможность расширения кода в изменчивом мире для достижения поставленной цели. Методы выполнения действий могут быть разными, но конечная цель остается неизменной. 

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

Что нам известно об объектной композиции? 

Наследование — это удобный способ реализации повторного использования кода в больших объемах, но при этом он сопровождается сильным зацеплением суперкласса с его подклассами. Шаблоны проектирования способствуют уменьшению этого эффекта и контролю за ним. Чем слабее зацепление в системе, тем она более гибкая и открытая изменениям. Честно говоря, нам не так сильно требуется наследование, как нас пытаются заверить руководства по объектно-ориентированному программированию (ООП) и сопутствующие примеры. 

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

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

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

Такие шаблоны проектирования, как Декоратор и Стратегия (который нам далее предстоит рассмотреть) используют принцип объектной композиции. Оба этих шаблона формируют конкретные классы для одновременного создания более сложных объектов.

Шаблон Стратегия 

Обратимся к определению из Википедии

Стратегия — поведенческий шаблон проектирования, предназначенный для определения семейства алгоритмов, инкапсуляции каждого из них и обеспечения их взаимозаменяемости. Это позволяет выбирать алгоритм путём определения соответствующего класса. Шаблон Strategy позволяет менять выбранный алгоритм независимо от объектов-клиентов, которые его используют.

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

Существует несколько подходов для реализации шаблона Стратегия. Один из них состоит в том, чтобы извлекать алгоритмы и внедрять их в базовый класс. Иначе говоря, мы создадим стратегии для разных типов поведения класса Swimming Strokes.

Посмотрим на диаграмму данных. Теперь Swimming Stroke не определяет типы поведения body, arm, leg movements и breathing как методы, а делегирует их. Они внедряются в экземпляр Swimming Stroke и не кодируются жестков класс. Это позволяет создавать новые Swimming Strokes, используя разные типы поведения или, согласно установившейся терминологии, разные стратегии.

Итак, посмотрим, как все это будет выглядеть в коде. 

Реализация шаблона Стратегия 

Сначала инкапсулируем типы поведения body movement и breathing. В качестве примера рассмотрим leg movement. Обратите внимание на используемый здесь общий интерфейс.

public interface LegMovementStrategy {
   void legMovement();
}

public class DolphinKick implements LegMovementStrategy {
   @Override
   public void legMovement() {
       log.info("The legs held together and move up and down symmetrically with the feet extended." ("Ноги держатся вместе и симметрично двигаются с вытянутыми ступнями вверх и вниз"));
   }
}

public class FlutterKick implements LegMovementStrategy {
   @Override
   public void legMovement() {
       log.info("The legs perform fast, compact movements, alternating up and down with outstretched feet" ("Поочередно ноги совершают быстрые, компактные движения с вытянутыми ступнями вверх и вниз"));
   }
}
...

Аналогичным образом создаем стратегии body movement, arm movement и breathing (здесь можно ознакомиться с полным вариантом кода). 

Теперь внедряем эти стратегии в абстрактный класс Swimming Stroke. 

@RequiredArgsConstructor
public abstract class SwimmingStroke {

   private final BodyMovementStrategy bodyMovementStrategy;
   private final ArmMovementStrategy armMovementStrategy;
   private final LegMovementStrategy legMovementStrategy;
   private final BreathingStrategy breathingStrategy;

   public void executeBodyMovement() { bodyMovementStrategy.bodyMovement(); }
   public void executeArmMovement() { armMovementStrategy.armMovement(); }
   public void executeLegMovementStrategy() { legMovementStrategy.legMovement(); }
   public void executeBreathingStrategy() { breathingStrategy.breathing(); }
}

После этого создаем определение Swimming Stroke внедрением конкретных стратегий в родительский класс. Вот таким образом выглядит класс Butterfly:

public class Butterfly extends SwimmingStroke {

   public Butterfly() {
       super(new WaveLikeUndulation(), 
             new SymmetricalArmMovement(), 
             new DolphinKick(), 
             new BreathDuringTheArmRecovery());
   }
}

И наконец, создаем Swimming Guide Service и добавляем метод Overview Butterfly Stroke. 

@Service
public class SwimmingGuideService {

   public void overviewButterflyStroke() {
       SwimmingStroke butterfly = new Butterfly();
       butterfly.executeBodyMovement();
       butterfly.executeArmMovement();
       butterfly.executeLegMovementStrategy();
       butterfly.executeBreathingStrategy();
   }
}

Вызываем метод overviewButterflyStroke() и проверяем логи: 

WaveLikeUndulation - The body executes a wave-like undulation, where the chest and the hips move up and down in the water in a specific order.

SymmetricalArmMovement - The hands trace an hourglass pattern underwater, moving from an extended forward position to below the chest and then to the hips.

DolphinKick - The legs held together and move up and down symmetrically with the feet extended.

BreathDuringTheArmRecovery - To breathe, the swimmer turns his head to the side during the arm recovery until the mouth is above the water surface.
  • WaveLikeUndulation — тело выполняет волнообразное движение, в котором грудь и бедра двигаются вверх и вниз в воде в определенной последовательности.
  • SymmetricalArmMovement — кисти рук под водой совершают движение по контуру песочных часов, вытягиваясь вперед, затем вниз к груди и по направлению к бедрам.
  • DolphinKick — ноги держатся вместе и симметрично двигаются с вытянутыми ступнями вверх и вниз.
  • BreathDuringTheArmRecovery — делая вдох, пловец поворачивает голову в сторону во время возврата рук в исходное положение, пока рот находится над водой.

Вот и все! Пройдите по ссылке на GitHub для ознакомления с проектом данной статьи.

Читайте также:


Перевод статьи Gene Zeiniss: Design Patterns Saga: Manta Ray vs Butterfly

Предыдущая статьяОх, TypeScript, ты боль моя
Следующая статьяP.S. Дорогой рефакторинг, нам нужно на время расстаться