Чи можна втілити стратегію без значних розгалужень?


14

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

Завод:

// All of these classes implement OrderStrategy
switch (orderType) {
case NEW_ORDER: return new NewOrder();
case CANCELLATION: return new Cancellation();
case RETURN: return new Return();
}

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

<strategies>
   <order type="NEW_ORDER">com.company.NewOrder</order>
   <order type="CANCELLATION">com.company.Cancellation</order>
   <order type="RETURN">com.company.Return</order>
</strategies>

Але це все ще додає код коробки для оброблення файлу даних - більш легкий для перевірки модуль та відносно стабільний код, але додаткова складність.

Крім того, такий тип конструкції не добре перевіряє інтеграцію. Кожна окрема стратегія може бути простішою для перевірки, але кожна нова стратегія, яку ви додаєте, є складністю для тестування. Це менше, ніж у вас, якби ви не використовували шаблон, але він все ще є.

Чи є спосіб реалізувати стратегію, що зменшує цю складність? Або це просто так просто, як це стає, і намагання йти далі, лише додасть ще один шар абстракції за мало користі?


Гммммм .... можливо, можна спростити речі з такими речами, як eval... може, не працює на Java, але, можливо, іншими мовами?
FrustratedWithFormsDesigner

1
Роздуми @FrustratedWithFormsDesigner - це магічне слово в java
ratchet freak

2
Ці умови вам все одно будуть потрібні десь. Натиснувши їх на завод, ви просто дотримуєтесь DRY, оскільки в іншому випадку заява if або switch може з’явитися в декількох місцях.
Даніель Б

1
Недолік, про який ви згадуєте, часто називають порушенням принципу відкритого закритого принципу
k3b

прийнята відповідь у пов'язаному питанні пропонує словник / карту як альтернативу if-else та переключенню
gnat

Відповіді:


16

Звичайно, ні. Навіть якщо ви використовуєте контейнер IoC, вам доведеться десь мати умови, вирішуючи, яку конкретну реалізацію вводити. Така природа стратегії.

Я не розумію, чому люди вважають, що це проблема. У деяких книгах є твердження, як, наприклад, Фоулер Refactoring , що якщо ви бачите перемикач / корпус або ланцюжок if / elses посеред іншого коду, вам слід врахувати цей запах і поглянути, щоб перенести його на свій власний метод. Якщо код у кожному випадку більше, ніж рядок, може бути два, то слід розглянути можливість зробити цей метод заводським методом, повертаючи Стратегії.

Деякі люди вважають, що перемикач / випадок поганий. Це не так. Але він повинен проживати самостійно, якщо це можливо.


1
Я також не вважаю це поганим. Однак я завжди шукаю способів зменшити кількість збереження коду, який би його торкнувся, що підштовхнуло питання.
Майкл К

Оператори перемикання (і довгі блоки блоків if / else-if) є поганими, і їх слід уникати, якщо це можливо, щоб зберегти свій код збереження. Це сказано, що "якщо це можливо" визнає, що є деякі випадки, коли вимикач повинен існувати, і в цих випадках намагайтеся тримати його в одному місці, і той, що робить його збереженням менше, ніж справа (це так просто випадково пропустіть 1 з 5 місць у коді, які вам потрібно було синхронізувати, коли ви не ізолюєте його належним чином).
Shadow Man

10

Чи можна втілити стратегію без значних розгалужень?

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

Class strategyType = allStrategies[orderType];
return runtime.create(strategyType);

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

factory.register(NEW_ORDER, NewOrder.class);

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

Метод реєстрації не має нічого іншого, як додавання нового значення до хешмапу:

void register(OrderType orderType, Class class)
{
   allStrategies[orderType] = class;
}

[оновлення 2012-05-04]
Це рішення набагато складніше, ніж оригінальне "рішення комутатора", яке я б віддав перевагу більшість часу.

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


3
Отже, тепер у вас є ціла стратегія хешмапу, щоб уникнути перемикання / випадку? Наскільки саме рядки factory.register(NEW_ORDER, NewOrder.class);чистіші, або менш порушення OCP, ніж рядки case NEW_ORDER: return new NewOrder();?
pdr

5
Це зовсім не чистіше. Це контрінтуїтивно. Він жертвує принципом "тримайте-стимулюйте-дуріть", щоб поліпшити принцип відкритості-закритості. Те саме стосується інверсії контролю та введення залежності: їх важче зрозуміти, ніж просте рішення. Питання було "реалізувати ... без істотного розгалуження", а не "як створити інтуїтивніший soltuion"
k3b

1
Я не бачу, щоб ви також уникли розгалуження. Ви щойно змінили синтаксис.
pdr

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

2
Особисто мені цей метод подобається, тому що він дозволяє ставитися до своїх стратегій як до змінних даних , замість того, щоб трактувати їх як незмінний код.
Tacroy

2

"Стратегія" полягає в тому, щоб хоча б один раз вибирати між альтернативними алгоритмами , не менше. Десь у вашій програмі хтось повинен прийняти рішення - можливо, користувач або ваша програма. Якщо ви використовуєте IoC, відображення, оцінювач файлів даних або конструкцію комутатора / випадку, щоб це здійснити, ситуація не зміниться.


Я не погоджуюся з вашим твердженням одного разу. Стратегії можна міняти під час виконання. Наприклад, після того, як не вдалося обробити запит за допомогою стандартної ProcessingStrategy, може бути обраний VerboseProcessingStrategy і повторний процес обробки.
dmux

@dmux: звичайно, відповідно відредагував мою відповідь.
Док Браун

1

Шаблон стратегії використовується, коли ви вказуєте можливі форми поведінки, і найкраще використовується при призначенні обробника при запуску. Вказання, який екземпляр стратегії використовувати можна через Медіатор, ваш стандартний контейнер IoC, якийсь завод, як ви описуєте, або просто за допомогою правильного на основі контексту (оскільки часто стратегія подається як частина більш широкого використання класу, який його містить).

Для такої поведінки, коли різні методи потрібно викликати на основі даних, я б запропонував використовувати конструкції поліморфізму, що надаються мовою; не стратегія.


1

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

enum FactoryType {
   Type1(Type1.class),
   Type2(Type2.class);

   private Class<? extends Type> clazz;

   private FactoryType(Class<? extends Type> clazz) {
      this.clazz = clazz;
   }

   public Class<? extends Type> getTypeClass() {
      return clazz;
   }
}

Це різко знижує заводський код:

public Type create(FactoryType type) throws Exception {
   return type.getTypeClass().newInstance();
}

(Будь ласка, ігноруйте погану обробку помилок - приклад коду :))

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


Не впевнений, що спричинило це, щоб повернутися на головну сторінку, але це хороша відповідь, і в Java 8 тепер можна створювати посилання на конструктор без роздумів.
JimmyJames

1

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

Наприклад:

public interface IOrderStrategy
{
    OrderType OrderType { get; }
}

public class NewOrder : IOrderStrategy
{
    public OrderType OrderType { get; } = OrderType.NewOrder;
}

public class OrderFactory
{
    private IEnumerable<IOrderStrategy> _strategies;

    public OrderFactory(IEnumerable<IOrderStrategy> strategies) // Injected by IoC container
    {
        _strategies = strategies;
    }

    public IOrderStrategy Create(OrderType orderType)
    {
        IOrderStrategy strategy = _strategies.FirstOrDefault(s => s.OrderType == orderType);

        if (strategy == null)
            throw new ArgumentException("Invalid order type.", nameof(orderType));

        return strategy;
    }
}

1

Я думаю, що має бути обмежено кількість філій, які є прийнятними.

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.