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

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

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

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

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


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

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


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

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


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

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

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

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