Как же правильно изучать шаблоны проектирования? Есть два подхода: скучный и доходчивый (Нравится моя классификация?). Скучный подход подразумевает академическое изучение списка паттернов с использованием абстрактных примеров. Лично я предпочитаю, противоположный – доходчивый подход, когда постановка задачи на относительно высоком уровне формулировки позволяет выбрать шаблоны проектирования. Хотя вы можете комбинировать оба подхода.
Итак, поехали?
Шаблон «Стратегия» относится к группе поведенческих шаблонов.
Краткое определение шаблона «Стратегия»
Шаблон служит для переключения между семейством алгоритмов, когда объект меняет свое поведение, на основании изменения своего внутреннего состояния.
Практические примеры применения шаблона «Стратегия»
- Сортировка (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(), также могут быть увязаны с адаптером.
Рефакторинг и шаблон «Стратегия»
Шаблон «Стратегия» следует использовать, когда вы начинаете замечать повторяющиеся алгоритмы, но в разных вариациях. Таким образом, необходимо разделить алгоритмы на классы и заполнять их по необходимости в своей программе.
Далее, если Вы заметили повторяющиеся условные операторы вокруг родственного алгоритма.
Когда в большинстве классов присутствует связанное поведение. И тогда его нужно выделить и переместить их в отдельные классы.
Надеюсь эта подборка примеров поможет вам уместно использовать шаблон «Стратегия».
Буду рад, если в комментариях вы сможете привести еще примеры этого шаблона.
Счастливого кодирования, друзья и коллеги!
Deosis
В некоторых примерах просто добавили слово стратегия.
Стратегия — это делегирование части логики алгоритма вовне. В большинстве случаев реализация паттерна представляет из себя передачу коллбэка в функцию.
Стратегия в сортировке, это не выбор алгоритма целиком, а передача в алгоритм функции сравнения, что позволяет использовать повторно использовать алгоритм сортировки, когда понадобится отсортировать элементы по другому критерию.