Весняний РОЗПРОДАЖ

Стратегія

Також відомий як: Strategy

Суть патерна

Стратегія — це поведінковий патерн проектування, який визначає сімейство схожих алгоритмів і розміщує кожен з них у власному класі. Після цього алгоритми можна заміняти один на інший прямо під час виконання програми.

Патерн Стратегія

Проблема

Ви вирішили написати програму-навігатор для подорожуючих. Вона повинна показувати гарну й зручну карту, яка дозволяла б з легкістю орієнтуватися в незнайомому місті.

Однією з найбільш очікуваних функцій був пошук та прокладання маршрутів. Перебуваючи в невідомому йому місті, користувач повинен мати можливість вказати початкову точку та пункт призначення, а навігатор, в свою чергу, прокладе оптимальний шлях.

Перша версія вашого навігатора могла прокладати маршрут лише автомобільними шляхами, тому чудово підходила для подорожей автомобілем. Але, вочевидь, не всі їздять у відпустку автомобілями. Тому наступним кроком ви додали до навігатора можливість прокладання піших маршрутів.

Через деякий час з’ясувалося, що частина туристів під час пересування містом віддають перевагу громадському транспорту. Тому ви додали ще й таку опцію прокладання шляху.

Але й це ще не все. У найближчій перспективі ви хотіли б додати прокладку маршрутів велодоріжками, а у віддаленому майбутньому — маршрути, пов’язані з відвідуванням цікавих та визначних місць.

Код навігатора стає занадто роздутим

Код навігатора стає занадто роздутим.

Якщо з популярністю навігатора не було жодних проблем, то технічна частина викликала запитання й періодичний головний біль. З кожним новим алгоритмом код основного класу навігатора збільшувався вдвічі. В такому великому класі стало важкувато орієнтуватися.

Будь-яка зміна алгоритмів пошуку, чи то виправлення багів, чи додавання нового алгоритму, зачіпала основний клас. Це підвищувало ризик створення помилки шляхом випадкового внесення змін до робочого коду.

Крім того, ускладнювалася командна робота з іншими програмістами, яких ви найняли після успішного релізу навігатора. Ваші зміни нерідко торкалися одного і того самого коду, створюючи конфлікти, які вимагали додаткового часу на їхнє вирішення.

Рішення

Патерн Стратегія пропонує визначити сімейство схожих алгоритмів, які часто змінюються або розширюються, й винести їх до власних класів, які називають стратегіями.

Замість того, щоб початковий клас сам виконував той чи інший алгоритм, він відіграватиме роль контексту, посилаючись на одну зі стратегій та делегуючи їй виконання роботи. Щоб змінити алгоритм, вам буде достатньо підставити в контекст інший об’єкт-стратегію.

Важливо, щоб всі стратегії мали єдиний інтерфейс. Використовуючи цей інтерфейс, контекст буде незалежним від конкретних класів стратегій. З іншого боку, ви зможете змінювати та додавати нові види алгоритмів, не чіпаючи код контексту.

Стратегії побудови шляху

Стратегії побудови шляху.

У нашому прикладі кожен алгоритм пошуку шляху переїде до свого власного класу. В цих класах буде визначено лише один метод, що приймає в параметрах координати початку та кінця маршруту, а повертає масив всіх точок маршруту.

Хоча кожен клас прокладатиме маршрут на свій розсуд, для навігатора це не буде мати жодного значення, оскільки його робота полягає тільки у зображенні маршруту. Навігатору достатньо подати до стратегії дані про початок та кінець маршруту, щоб отримати масив точок маршруту в обумовленому форматі.

Клас навігатора буде мати метод для встановлення стратегії, що дозволить змінювати стратегію пошуку шляху «на льоту». Цей метод стане у нагоді клієнтському коду навігатора, наприклад, кнопкам-перемикачам типів маршрутів в інтерфейсі користувача.

Аналогія з життя

Способи пересування

Різні стратегії потрапляння до аеропорту.

Вам потрібно дістатися аеропорту. Можна доїхати автобусом, таксі або велосипедом. Тут вид транспорту є стратегією. Ви вибираєте конкретну стратегію в залежності від контексту — наявності грошей або часу до відльоту.

Структура

Структура класів патерна СтратегіяСтруктура класів патерна Стратегія
  1. Контекст зберігає посилання на об’єкт конкретної стратегії, працюючи з ним через загальний інтерфейс стратегій.

  2. Стратегія визначає інтерфейс, спільний для всіх варіацій алгоритму. Контекст використовує цей інтерфейс для виклику алгоритму.

    Для контексту неважливо, яка саме варіація алгоритму буде обрана, оскільки всі вони мають однаковий інтерфейс.

  3. Конкретні стратегії реалізують різні варіації алгоритму.

  4. Під час виконання програми контекст отримує виклики від клієнта й делегує їх об’єкту конкретної стратегії.

  5. Клієнт повинен створити об’єкт конкретної стратегії та передати його до конструктора контексту. Крім того, клієнт повинен мати можливість замінити стратегію на льоту, використовуючи сетер поля стратегії. Завдяки цьому, контекст не знатиме про те, яку саме стратегію зараз обрано.

Псевдокод

У цьому прикладі контекст використовує Стратегію для виконання тієї чи іншої арифметичної операції.

// Загальний інтерфейс стратегій.
interface Strategy is
    method execute(a, b)

// Кожна конкретна стратегія реалізує загальний інтерфейс у свій
// власний спосіб.
class ConcreteStrategyAdd implements Strategy is
    method execute(a, b) is
        return a + b

class ConcreteStrategySubtract implements Strategy is
    method execute(a, b) is
        return a - b

class ConcreteStrategyMultiply implements Strategy is
    method execute(a, b) is
        return a * b

// Контекст завжди працює зі стратегіями через загальний
// інтерфейс. Він не знає, яку саме стратегію йому подано.
class Context is
    private strategy: Strategy

    method setStrategy(Strategy strategy) is
        this.strategy = strategy

    method executeStrategy(int a, int b) is
        return strategy.execute(a, b)


// Конкретна стратегія вибирається на більш високому рівні,
// наприклад, конфігуратором всієї програми. Готовий об'єкт-
// стратегія подається до клієнтського об'єкта, а потім може
// бути замінений іншою стратегією, в будь-який момент, «на
// льоту».
class ExampleApplication is
    method main() is
        // 1. Створити об'єкт контексту.
        // 2. Ввести перше число (n1).
        // 3. Ввести друге число (n2).
        // 4. Ввести бажану операцію.
        // 5. Потім, обрати стратегію:

        if (action == addition) then
            context.setStrategy(new ConcreteStrategyAdd())

        if (action == subtraction) then
            context.setStrategy(new ConcreteStrategySubtract())

        if (action == multiplication) then
            context.setStrategy(new ConcreteStrategyMultiply())

        // 6. Виконати операцію за допомогою стратегії:
        result = context.executeStrategy(n1, n2)

        // N. Вивести результат на екран.

Застосування

Якщо вам потрібно використовувати різні варіації якого-небудь алгоритму всередині одного об’єкта.

Стратегія дозволяє варіювати поведінку об’єкта під час виконання програми, підставляючи до нього різні об’єкти-поведінки (наприклад, що відрізняються балансом швидкості та споживання ресурсів).

Якщо у вас є безліч схожих класів, які відрізняються лише деякою поведінкою.

Стратегія дозволяє відокремити поведінку, що відрізняється, у власну ієрархію класів, а потім звести початкові класи до одного, налаштовуючи його поведінку стратегіями.

Якщо ви не хочете оголювати деталі реалізації алгоритмів для інших класів.

Стратегія дозволяє ізолювати код, дані й залежності алгоритмів від інших об’єктів, приховавши ці деталі всередині класів-стратегій.

Якщо різні варіації алгоритмів реалізовано у вигляді розлогого умовного оператора. Кожна гілка такого оператора є варіацією алгоритму.

Стратегія розміщує кожну лапу такого оператора до окремого класу-стратегії. Потім контекст отримує певний об’єкт-стратегію від клієнта й делегує йому роботу. Якщо раптом знадобиться змінити алгоритм, до контексту можна подати іншу стратегію.

Кроки реалізації

  1. Визначте алгоритм, що схильний до частих змін. Також підійде алгоритм, який має декілька варіацій, які обираються під час виконання програми.

  2. Створіть інтерфейс стратегій, що описує цей алгоритм. Він повинен бути спільним для всіх варіантів алгоритму.

  3. Помістіть варіації алгоритму до власних класів, які реалізують цей інтерфейс.

  4. У класі контексту створіть поле для зберігання посилання на поточний об’єкт-стратегію, а також метод для її зміни. Переконайтеся в тому, що контекст працює з цим об’єктом тільки через загальний інтерфейс стратегій.

  5. Клієнти контексту мають подавати до нього відповідний об’єкт-стратегію, коли хочуть, щоб контекст поводився певним чином.

Переваги та недоліки

  • Гаряча заміна алгоритмів на льоту.
  • Ізолює код і дані алгоритмів від інших класів.
  • Заміна спадкування делегуванням.
  • Реалізує принцип відкритості/закритості.
  • Ускладнює програму внаслідок додаткових класів.
  • Клієнт повинен знати, в чому полягає різниця між стратегіями, щоб вибрати потрібну.

Відносини з іншими патернами

  • Міст, Стратегія та Стан (а також трохи і Адаптер) мають схожі структури класів — усі вони побудовані за принципом «композиції», тобто делегування роботи іншим об’єктам. Проте вони відрізняються тим, що вирішують різні проблеми. Пам’ятайте, що патерни — це не тільки рецепт побудови коду певним чином, але й описування проблем, які призвели до такого рішення.

  • Команда та Стратегія схожі за принципом, але відрізняються масштабом та застосуванням:

    • Команду використовують для перетворення будь-яких різнорідних дій на об’єкти. Параметри операції перетворюються на поля об’єкта. Цей об’єкт тепер можна логувати, зберігати в історії для скасування, передавати у зовнішні сервіси тощо.
    • З іншого боку, Стратегія описує різні способи того, як зробити одну і ту саму дію, дозволяючи замінювати ці способи в якомусь об’єкті контексту прямо під час виконання програми.
  • Стратегія змінює поведінку об’єкта «зсередини», а Декоратор змінює його «ззовні».

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

  • Стан можна розглядати як надбудову над Стратегією. Обидва патерни використовують композицію, щоб змінювати поведінку головного об’єкта, делегуючи роботу вкладеним об’єктам-помічникам. Проте в Стратегії ці об’єкти не знають один про одного і жодним чином не пов’язані. У Стані конкретні стани самостійно можуть перемикати контекст.

Приклади реалізації патерна

Стратегія на C# Стратегія на C++ Стратегія на Go Стратегія на Java Стратегія на PHP Стратегія на Python Стратегія на Ruby Стратегія на Rust Стратегія на Swift Стратегія на TypeScript