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