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

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

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

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

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

    Краткое определение шаблона «Стратегия»


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

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


    • Сортировка (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(), также могут быть увязаны с адаптером.

    Рефакторинг и шаблон «Стратегия»


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

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

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

    Счастливого кодирования, друзья и коллеги!
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 8

      +4

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

        +3

        Тот же текст, переписанный из ОО в ФП стиле (и переведенный на английский еще, чтобы можно было твитнуть):


        Use dependency injection whenever possible to decouple things.

        Не благодарите.

          +1
          Почти как в анекдоте: «Эдак ты мне все программирование к стратегиям сведешь!». Местами в примерах у вас просто полиморфизм.
          Мне нравится такое неформальное определение стратегии. Если сейчас хочется в метод передать параметром функцию/лямбду, то раньше бы это превратилось в стратегию.
            +3
            Причем тут .Net? Тем более яп непонятно какой…
              0

              На каком языке написаны примеры?

                0

                На TypeScript похоже.

                0
                исключающий процедурный стиль IF…. ELSE…… или SWITCH …. CASE….

                Он переехал в CipherFactory. Но ваш вариант конечно солиднее выглядит.
                  0
                  Я вот тоже не очень понял это. В том же примере с принтером почему-то опущен момент, когда и как в принтер передается конкретная стратегия печати. А перед передачей конкретной еще кто-то должен ведь решить, какая именно сейчас нужна, а значит, вот вам if/else, от которого стремились уйти.

                Only users with full accounts can post comments. Log in, please.