0 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Java 8 и паттерн Стратегия

Java 8 и паттерн Стратегия

На дворе 2017 год. В компанию, где работает старший разработчик Джо, пришел на стажировку молодой студент Мартин. Он целый год скрупулезно изучал Java по современному учебнику с акцентом на функциональные интерфейсы, лямбда-выражения и прочие новшества.

Джо поинтересовался, знает ли Мартин паттерны проектирования. Мартин не знал, совсем не знал. И Джо понял, как он будет стажировать Мартина. Он даст ему те задачи, которые, по его мнению, без паттернов проектирования будут иметь трудно поддерживаемые решения. Когда Мартин напишет приложение, Джо сначала помучает его с расширением архитектуры, а затем покажет ему, как легко можно ее расширять, если при проектировании изначально заложить в нее чудесные паттерны. Однако, он решил сразу предупредить Мартина о направлениях расширения: а вдруг он сам сможет нечаянно реализовать паттерны проектирования, не зная их? Молодые люди весьма изобретательны.

Джо: Слушай, Мартин, для тебя есть ответственная работа. Нужно написать прототип симулятора утиного пруда. Создай модели уток, они могут крякать, летать, плавать и делать прочие действия. Вместо действий пока используй заглушки, выводящие в консоль текстовые сообщения. Учти, что утки могут быть разных видов, в том числе резиновая утка, деревянная, газетная, ручная утки, утки с утятами. Значит, не все разновидности уток могут в принципе крякать или летать или плавать или совершать другие действия. Некоторые утки могут со временем приобретать новые возможности, или утрачивать их. Учти, что тебе это приложение придется поддерживать длительное время, а начальство будет придумывать всё новые виды уток.

Мартин молча принялся за работу.

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

И затем выполнить его так:

Раз мы сохранили поведение в переменной, то его всегда можно будет заменить на любое другое поведение, даже динамически во время жизни объекта, не говоря уже о наследовании. А раз количество поведений также может меняться, то можно не заводить под каждое действие свою переменную, а сохранять их в динамически изменяемой структуре данных. Например, в Set. Или, лучше, в Map , указывая в качестве ключа текстовый идентификатор поведения, а то мы это поведение потом не сможем отличить от других. Наверное, стоит для хранения и манипуляции поведений создать свой собственный класс и его объект встроить в поле базового класса уток. С него и начнем:

Метод запуска способности мы в классе BehaviorRegistry реализовывать не будем, так как этот класс обобщен, поэтому мы не знаем, какой функциональный интерфейс лежит в основе его конкретного экземпляра, а значит не знаем название исполняющей функции: run(), call(), accept(), test(), apply() и т.д., и не знаем количество и типы аргументов для этих функций, и что эти функции возвращают.

Теперь воспользуемся им в классе Duck:

В принципе, это всё. Даже наследование и иерархия классов не понадобилась. Просто будем создавать объекты Duck и в поле behaviors сохранять столько разных вариантов поведения, сколько нужно, и затем исполнять их когда нужно. Вот такая получилась архитектура:

Читать еще:  Microsoft патентует клавиатуру с жестовым управлением

На диаграмме нет ни одного прямоугольника ни для какого поведения, так как в данной архитектуре поведение утки является таким же анонимным значением, как и число 6 является анонимным значением для переменной int number. Архитектуре не важно, что делает и как устроено поведение, лишь бы оно удовлетворяло функциональному интерфейсу Runnable, как и переменной int number не важно, какое число в него сохраняют, лишь бы оно было целочисленным.

Эксплуатировать архитектуру будет проще, если заранее заготовить справочник некоторых поведений в виде enum:

Теперь можно смело начинать конкретную эксплуатацию:

И вот результат:

Мартин сдает работу Джо…

Джо: Это что? Разве это архитектура приложения? Всего-лишь два прямоугольника с одной связью на диаграмме. Когда я сделал свою версию программы, на моей диаграмме было двенадцать прямоугольников в самом начале, когда утки могли только крякать и летать, и их стало более трехсот через полгода, когда начальство придумало пятьдесят различных аспектов поведения, и для каждого аспекта по несколько различных вариантов реализаций! Я отделил изменяющееся от постоянного, и для каждой группы в архитектуре создал по своей иерархии, чтобы добиться гибкости и устранить дублирование кода. А у тебя, я смотрю, изменяющееся вообще вынесено за пределы архитектуры, в область анонимного и неопределенного, ты оставил в ней только постоянное. Однако, твое приложение по крайней мере не менее гибкое и расширяемое, чем мое. И дублирования кода я шибко не вижу, разве что при создании кряквы и красноголовой утки одинаковое добавление способностей крякать и летать можно было вынести в отдельный метод, но это мелочи. Я думаю, ты будешь очень быстро расширять это приложение, поэтому у меня есть для тебя еще работенка для нагрузки. Как насчет создания мониторов погоды для метеостанции? Я только что получил техтребования.

Java 8 и паттерн Стратегия

Также известен под именем Policy. codelab.ru источник codelab.ru оригинал

Условия, Задача, Назначение

Паттерн стратегия это осуществляет. По-сути, он: источник оригинал codelab.ru codelab.ru

определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. И далее – позволяет изменять алгоритмы независимо от клиентов, которые ими пользуются.

Мотивация

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

codelab.ru codelab.ru источник оригинал

  • SimpleCompositor реализует простую стратегию, выделяющую по одной строке за раз. codelab.ru оригинал codelab.ru источник
  • TeXCompositor реализует алгоритм поиска точек разбиения на строки. Эта стратегия пытается выполнить глобальную оптимизацию разбиения на строки, рассматривая сразу целый параграф. codelab.ru оригинал codelab.ru источник
  • ArrayCompositor реализует стратегию расстановки переходов на новую строку таким образом, что в каждой строке оказывается одно и то же число элементов.
    Это полезно, например, при построчном отображении набора пиктограмм. codelab.ru источник codelab.ru оригинал

Объект Composition хранит ссылку на объект Compositor. Всякий раз, когда объекту Composition требуется переформатировать текст, он делегирует данную обязанность своему объекту Compositor. Клиент задает, какой объект Compositor следует использовать, параметризуя им объект Composition. codelab.ru codelab.ru источник оригинал

Читать еще:  Как убрать режим ограниченной функциональности Word в Word 2016, Word 2013, Word 2010, Word 2007

Признаки применения, использования паттерна Стратегия (Strategy)

  1. Имеется много родственных классов, отличающихся только поведением.
    Т.е. имеющих похожие интерфейсы, но реализующих свою логику по разному.
    Стратегия позволяет сконфигурировать класс, задав одно из возможных поведений. оригинал codelab.ru источник codelab.ru
  2. Вам нужно иметь несколько разных вариантов алгоритма.
    Например, можно определить два варианта алгоритма, один из которых требует больше времени, а другой — больше памяти. источник codelab.ru codelab.ru оригинал
  3. В алгоритме содержатся данные, о которых клиент не должен «знать».
    Используйте паттерн стратегия, чтобы не раскрывать сложные, специфичные для алгоритма структуры данных (подход «черного ящика»). оригинал codelab.ru codelab.ru источник
  4. В классе определено много поведений.
    И все это добро представлено разветвленными условными операторами. В этом случае проще перенести код из ветвей в отдельные классы стратегий. источник codelab.ru оригинал codelab.ru

Решение

оригинал codelab.ru codelab.ru источник

Участники паттерна Стратегия (Strategy)

  1. Strategy (Compositor) – стратегия.
    Объявляет общий для всех поддерживаемых алгоритмов (стратегий) интерфейс. Класс Context пользуется этим интерфейсом для вызова конкретного алгоритма, определенного в классе ConcreteStrategy. codelab.ru оригинал codelab.ru источник
  2. ConcreteStrategy (SimpleCompositor, TeXCompositor, ArrayCompositor) — конкретная стратегия.
    Реализует алгоритм, использующий интерфейс, объявленный в классе Strategy. codelab.ru оригинал источник codelab.ru
  3. Context (Composition) – контекст.
    Конфигурируется объектом класса ConcreteStrategy.
    Хранит ссылку на объект класса Strategy.
    Может определять интерфейс, который позволяет объекту Strategy получить доступ к данным контекста. codelab.ru codelab.ru источник оригинал

Схема использования паттерна Стратегия (Strategy)

клиент создает объект ConcreteStrategy и передает его контексту, после чего клиент «общается» исключительно с контекстом. Часто в распоряжении клиента находится несколько классов ConcreteStrategy, которые он может выбирать. codelab.ru источник оригинал codelab.ru

Вопросы, касающиеся реализации паттерна Стратегия (Strategy)

  1. Определение интерфейсов классов Strategy и Context.
    Интерфейсы классов Strategy и Context могут обеспечить объекту класса ConcreteStrategy эффективный доступ к любым данным контекста, и наоборот.
    Например, Context передает данные в виде параметров операциям класса Strategy. Это разрывает тесную связь между контекстом и стратегией. При этом не исключено, что иногда контекст будет передавать данные, которые стратегии не нужны.
    Другой метод — передать контекст в качестве аргумента, в таком случае стратегия сама будет запрашивать у него данные, или, например, сохранить ссылку на свой контекст, так что передавать вообще ничего не придется.
    И в том, и в другом случаях стратегия может запрашивать только ту информацию, которая реально необходима. Но тогда в контексте должен быть определен более развитый интерфейс доступа к своим данным, что несколько усиливает связанность классов Strategy и Context.
    Какой подход лучше, зависит от конкретного алгоритма и требований, которые он предъявляет к данным. codelab.ru codelab.ru оригинал источник
  2. Стратегии как параметры класса.
    В C++ для конфигурирования класса стратегией можно использовать шаблоны. Этот способ хорош, только если стратегия определяется на этапе компиляции и ее не нужно менять во время выполнения. Тогда конфигурируемый класс (например, Context) определяется в виде шаблона, для которого класс Strategy является параметром:

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

В языке Java имеется похожий механизм Generics. Вот как бы это все выглядело там:

Java 8 и паттерн Стратегия

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

Читать еще:  NewSQL: SQL никуда не уходит

Мы конечно можем вынести эти ифы в один метод в классе Human но это очень плохой вариант потому, что класс предок знает что-то о своих потомках. Об этом не будем т. к. это выходит за рамки нашей темы.

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

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

Теперь давайте применим паттерн “Стратегия”.

Вот теперь код стал намного красивее. Никаких вам ифов. И если появится желание добавить новый стиль то не нужно касаться иерархии human вообще. Достаточно просто добавить еще одну реализацию интерфейса Style и у нужного объекта изменить стиль с помощью сеттера.

Как мы этого добились?
Все просто мы в классе human использовали интерфейс Style. Это позволило нам абстрагироваться от реализации. Теперь под этот интерфейс можно подставить любую реализацию и это ни как не повлияет на остальной код.
Затем мы делегировали обязанности метода getWoman() классу реализующему интерфейс Style.

Хорошо теперь поговорим о самом шаблоне Стратегия.

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

На схеме Context соответствует нашему классу Human. А интерфейс Strategy это наш интерфейс style. ConcreteStrategy это наши реализации стилей. Эта часть является изменяемой. В отличии от контекста.

Теперь более приземленные примеры.
Слышали про такой интерфейс Comparator в Java? Если нет то немного поясню для чего он нужен. В java можно сортировать массивы (см. рис.)

Но разработчики добавили возможность еще сортировать и объекты классов если вместе с ними в метод sort будет передана реализация класса comparator. Вот как это выглядит:

В данном случае context это тело метода сорт. Которое работает с интерфейсом Comparator и ничего не знает о том какая реализация за ним скрыта. Семейством алгоритмов будет все реализации интерфейса Comparator.

Еще пример!
Вовремя проектирования приложение могут меняться требования того как и куда сохраняются данные. Это может быть база данных или файловая система. В таких случаях часто применяется шаблон DAO.

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

Вообще этот алгоритм очень распространен. Если перечислять все места где он используется это ролик затянется на часы.

Помните мой сюжет про инверсию управления? Там я приводил пример в котором контекстом является класс Speaker. А семейством алгоритмов реализации интерфейса SpeachWriter.

Ссылка на основную публикацию
Статьи c упоминанием слов:
Adblock
detector