Практическое использование шаблона Стратегия

от автора

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

Как же правильно изучать шаблоны проектирования? Есть два подхода: скучный и доходчивый (Нравится моя классификация?). Скучный подход подразумевает академическое изучение списка паттернов с использованием абстрактных примеров. Лично я предпочитаю, противоположный – доходчивый подход, когда постановка задачи на относительно высоком уровне формулировки позволяет выбрать шаблоны проектирования. Хотя вы можете комбинировать оба подхода.

Итак, поехали?

Шаблон «Стратегия» относится к группе поведенческих шаблонов.

Краткое определение шаблона «Стратегия»
Шаблон служит для переключения между семейством алгоритмов, когда объект меняет свое поведение, на основании изменения своего внутреннего состояния.

Практические примеры применения шаблона «Стратегия»

• Сортировка (sorting): мы хотим отсортировать эти числа, но мы не знаем, будем ли мы использовать BrickSort, BubbleSort или какую-либо другую сортировку. Например, у вас есть веб-сайт, на котором страница отображает элементы в зависимости от популярности. Однако «Популярным» может быть много вещей (большинство просмотров, большинство подписчиков, дата создания, большая активность, наименьшее количество комментариев). В случае, если руководство еще не знает точно, как сделать заказ, и может захотеть поэкспериментировать с различными заказами на более поздний срок, вы создаете интерфейс (IOrderAlgorithm или что-то еще) с методом заказа и позволяете объекту Orderer делегировать порядок конкретной реализации интерфейса IOrderAlgorithm. Вы можете создать «CommentOrderer», «ActivityOrderer» и т. д. и просто отключить их при появлении новых требований.

• Обработки очереди из разнородных объектов (queue processing and saving data): Примером может быть прокси-система накапливающая объекты от разных источников данных, тогда после извлечения из очереди объекта и последующее его сохранение определяется стратегией выбора, на основе свойств этого объекта.

• Валидация (validation). Нам нужно проверять элементы в соответствии с «Неким правилом», но пока не ясно, каким будет это правило, и мы можем подумать о новых.

• Аутентификации (authentication): выбор стратегии аутентификации между схемами Basic, Digest, OpenID, OAuth.

Вот пример:

interface AuthStrategy {     auth(): void; } class Auth0 implements AuthStrategy {     auth() {         log('Authenticating using Auth0 Strategy')     } } class Basic implements AuthStrategy {     auth() {         log('Authenticating using Basic Strategy')     } } class OpenID implements AuthStrategy {     auth() {         log('Authenticating using OpenID Strategy')     } }  class AuthProgram {     private _strategy: AuthStrategy     use(strategy: AuthStrategy) {         this._strategy = strategy         return this     }     authenticate() {         if(this._strategy == null) {             log("No Authentication Strategy set.")         }         this._strategy.auth()     }     route(path: string, strategy: AuthStrategy) {         this._strategy = strategy         this.authenticate()         return this     } } 

• Игры (games): стратегии перемещения в пространстве игры — игрок ходит, либо бегает, но, возможно, в будущем он также сможет плавать, летать, телепортироваться, рыть под землей и др. Другой пример, когда в игре, например с различными персонажами, где каждый персонаж может иметь разные виды оружия, но в конкретный момент времени может использовать только одно из них. Так что контекстом здесь является персонаж «Король», «Командир», «Солдат» и оружие как стратегия где метод атаковать Attack() зависит от вида оружия. Так что, если конкретные классы оружия могут быть «Меч», «Топор», «Арбалет», «Лук и Стрелы» они все должны иметь метод Attack ().

• Хранение информации (storing information): стратегия сохранения информации, чтобы приложение сохраняло информацию в базе данных, но позже может понадобиться сохранить файл или сделать веб-звонок. Рассмотрим, систему обработки файлов PDF, которая получила архив, содержащий множество документов и некоторые метаданные. На основании метаданных принимается решение, куда поместить документ; скажем, в зависимости от данных, можно было бы хранить документ в системах хранения A, B или C, или их комбинации. Систему обработки PDF используют разные клиенты, и у них имеются разные требования к откату / обработке ошибок при обработке PDF. Один клиент хочет, чтобы система доставки остановилась при первой ошибке, оставила все документы, уже доставленные в свои хранилища, но остановила процесс и больше ничего не доставляла, а другой, чтобы откатывание от B в случае ошибок при сохранении в C, но оставляла все, что уже было доставлено в A. Легко представить, что у третьего или четвертого клиента также будут другие требования. Чтобы решить проблему разнородных требований клиентов к обработке сохранения множества файлов PDF, может быть создан базовый класс доставки, который содержит логику доставки, а также методы для отката файлов из всех хранилищ. Эти методы фактически не вызываются системой доставки напрямую в случае ошибок. Вместо этого класс использует Dependency Injection для получения класса «Стратегия отката / обработки ошибок» (на основе клиента, использующего систему), который вызывается в случае ошибок, который, в свою очередь, вызывает методы отката, если это подходит для этой стратегии. Сам класс доставки сообщает, что происходит с классом стратегии (какие документы были доставлены, какие хранилища и какие сбои произошли), и всякий раз, когда возникает ошибка, он спрашивает стратегию, продолжать или нет. Если в стратегии говорится «остановите это», класс вызывает метод очистки «cleanUp» стратегии, который использует ранее сообщенную информацию, чтобы решить, какие методы отката следует вызвать из класса доставки, или просто ничего не делать.

• Вывод (outputting): нам нужно вывести X в виде простой строки, но позже это может быть CSV, XML, JSON и др.

• Выставление счета (invoicing): стратегия выставления счета за использование чего-либо на основании календаря, индивидуальных показателей, прайс-листа и бонусов.

• Навигация (navigation): построение маршрута на основании стратегии перемещения.

• Протоколирование (logging): в таких известных фреймворках протоколирования как Log4Net и Log4j реализованы присоеденители, раскладки и фильтры Appenders, Layouts, and Filters.

• Шифрование (encrypting): для небольших файлов вы можете использовать стратегию «в памяти», когда весь файл читается и хранится в памяти (скажем, для файлов <1 ГБ). Для больших файлов вы можете использовать другую стратегию, где части файла читаются в памяти, а частично зашифрованные результаты хранятся в файлах tmp. Это могут быть две разные стратегии для одной и той же задачи.

Вот и пример:

// Имплементация шаблона "Стратегия" interface  Cipher  {      public void performAction(); }  class InMemoryCipherStrategy implements Cipher {           public void performAction() {              // загрузка в byte[] ....          } }  class SwaptToDiskCipher implements Cipher {           public void performAction() {              // скинуть часть в файл ....           }  }  // Использование в клиенте File file = getFile(); Cipher c = CipherFactory.getCipher( file.size()); c.performAction(); 

• Графический редактор (graphic editor): например, в приложении Windows Paint имеется реализация шаблона стратегии, в котором можно независимо выбирать форму и цвет в разных разделах. Здесь форма и цвет являются алгоритмами, которые могут быть изменены во время выполнения.

Shape redCircle = new RedCircle(); //  Без шаблона «Стратегия» Shaped redCircle = new Shape("red","circle"); // С шаблоном «Стратегия» 

SOLID и имплементация шаблона «Стратегия»

Какую же основную проблему решает шаблон «Стратегия»? Фактически – это замена плоского кода ЕСЛИ…. ТО…… на его объектную реализацию.

Пример грязного плоского кода (неправильно):

class Document {...} class Printer {     print(doc: Document, printStyle: Number) {         if(printStyle == 0 /* цветная печать */) {             // ...         }         if(printStyle == 1 /* монохромная печать*/) {             // ...                     }         if(printStyle == 2 /* еще один вид печати */) {             // ...         }         if(printStyle == 3 /* еще один вид печати */) {             // ...                     }         if(printStyle == 4 /* еще один вид печати */) {             // ...         }         // ...     } } 

Пример этого же кода с шаблоном «Стратегия» (правильно):

class Document {...} interface PrintingStrategy {     printStrategy(d: Document): void; } class ColorPrintingStrategy implements PrintingStrategy {     printStrategy(doc: Document) {         log("Цветная печать")         // ...     } } class InvertedColorPrintingStrategy implements PrintingStrategy {     printStrategy(doc: Document) {         log("Инвертировання цветная печать")         // ...     } } class Printer {     private printingStrategy: PrintingStrategy     print(doc: Document) {         this.printingStrategy.printStrategy(doc)     } } 

Вот еще пример правильной реализации шаблона «Стратегия» основанной на SOLID.

//интерфейс  открытия/закрытия interface LockOpenStrategy {     open();     lock(); } // определение класса для сканера сетчатки глаза class RetinaScannerLockOpenStrategy implements LockOpenStrategy {     open() {         //...     }     lock() {         //...     } }  // Определение класса для ввода пароля с клавиатуры class KeypadLockOpenStrategy implements LockOpenStrategy {     open() {         if(password != "мойсуперпароль"){             log("Entry Denied")             return         }         //...     }     lock() {         //...     } } // Определение корневого абстрактного класса Дверь с методом Открытие. abstract class Door {     public lockOpenStrategy: LockOpenStrategy } //Определение класса стеклянная дверь. class GlassDoor extends Door {} // Определение класса металлическая дверь. class MetalDoor extends Door {} // Определение адаптера класса для корневого класса Дверь. class DoorAdapter {     openDoor(d: Door) {         d.lockOpenStrategy.open()     } }

Ниже собственно кодирование логики.

var glassDoor = new GlassDoor(); // Создать объект дверь glassDoor.lockOpenStrategy = new RetinaScannerLockOpenStrategy(); // Применить свойственную ему стратегию открытия по сканеру сетчатки глаза var metalDoor = new MetalDoor(); // Создать объект класса Металлическая дверь metalDoor.lockOpenStrategy = new KeypadLockOpenStrategy(); // Определить свойственную ей стратегию открытия Клавиатура. var door1 = new DoorAdapter().openDoor(glassDoor); // Используя адаптер открыть стекл. дверь var door2  = new DoorAdapter().openDoor(metalDoor); // Используя адаптер открыть металл. дверь 

Как видно выше это полностью объектно-ориентированный код исключающий процедурный стиль IF…. ELSE…… или SWITCH …. CASE….

Кстати, зачем мы использовали класс-адаптер? Сама по себе дверь не откроется, всегда открывается при помощи чего-то с одной стороны, а с другой могут быть какие-то события предшествующие открыванию двери или по завершению ее открывания, например BeforeOpen() and AfterOpen(), также могут быть увязаны с адаптером.

Рефакторинг и шаблон «Стратегия»
Шаблон «Стратегия» следует использовать, когда вы начинаете замечать повторяющиеся алгоритмы, но в разных вариациях. Таким образом, необходимо разделить алгоритмы на классы и заполнять их по необходимости в своей программе.
Далее, если Вы заметили повторяющиеся условные операторы вокруг родственного алгоритма.
Когда в большинстве классов присутствует связанное поведение. И тогда его нужно выделить и переместить их в отдельные классы.

Надеюсь эта подборка примеров поможет вам уместно использовать шаблон «Стратегия».
Буду рад, если в комментариях вы сможете привести еще примеры этого шаблона.

Счастливого кодирования, друзья и коллеги!

ссылка на оригинал статьи https://habr.com/ru/post/487858/