Может ли шаблон Стратегии быть реализован без существенного разветвления?

14

Шаблон Стратегии хорошо работает, чтобы избежать огромных конструкций if ... else и облегчить добавление или замену функциональности. Тем не менее, это все еще оставляет один недостаток, на мой взгляд. Кажется, что в каждой реализации все еще должна быть ветвящаяся конструкция. Это может быть фабрика или файл данных. В качестве примера возьмем систему заказов.

Фабрика:

// 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
странный урод
2
Вам все еще будут нужны эти условия где-то. Отправляя их на фабрику, вы просто соблюдаете DRY, так как в противном случае оператор if или switch мог бы появиться в нескольких местах.
Даниэль Б
1
Недостаток, который вы упоминаете, часто называют нарушением принципа «открыто-закрыто»
k3b
принятый ответ в связанном вопросе предлагает словарь / карту в качестве альтернативы if-else и switch
gnat

Ответы:

16

Конечно, нет. Даже если вы используете контейнер IoC, вам придется где-то иметь условия, решающие, какую конкретную реализацию внедрять. Это природа паттерна Стратегия.

Я действительно не понимаю, почему люди думают, что это проблема. В некоторых книгах есть утверждения, например, «Рефакторинг Фаулера» , что если вы видите переключатель / регистр или цепочку if / elses в середине другого кода, вам следует подумать об этом запахе и попытаться перенести его в свой собственный метод. Если код в каждом случае больше, чем строка, может быть, две, то вы должны рассмотреть возможность сделать этот метод фабричным методом, возвращающим стратегии.

Некоторые люди считают, что переключение / случай - это плохо. Это не вариант. Но он должен жить самостоятельно, если это возможно.

прецизионный самописец
источник
1
Я тоже не вижу в этом ничего плохого. Тем не менее, я всегда ищу способы уменьшить количество кода, поддерживающего его, что и вызвало вопрос.
Майкл К
Операторы switch (и длинные блоки if / else-if) являются плохими, и их следует избегать, если это вообще возможно, для того, чтобы ваш код можно было поддерживать. При этом «если это вообще возможно» признает, что в некоторых случаях коммутатор должен существовать, и в этих случаях стараться держать его в одном месте, и такой, который делает его менее трудоемким (так легко случайно пропустить 1 из 5 мест в коде, которые вам нужно было синхронизировать, если вы не изолируете их должным образом).
Shadow Man
10

Может ли шаблон Стратегии быть реализован без существенного разветвления?

Да , используя хэш-карту / словарь, где каждая реализация Стратегии регистрируется в. Заводской метод станет чем-то вроде

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

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

factory.register(NEW_ORDER, NewOrder.class);

Вы можете использовать статический конструктор для регистрации, если ваш язык поддерживает это.

Метод register ничего не делает, кроме добавления нового значения в hashmap:

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

[обновление 2012-05-04]
Это решение намного сложнее, чем оригинальное «решение по коммутации», которое я бы предпочел большую часть времени.

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

k3b
источник
3
Итак, теперь у вас есть целый Hashmap of Strategies, чтобы избежать переключения / случая? Насколько точно ряды factory.register(NEW_ORDER, NewOrder.class);чище или менее нарушают OCP, чем ряды case NEW_ORDER: return new NewOrder();?
фунтовые
5
Это совсем не чище. Это нелогично. Он жертвует принципом «держи его в тупике», чтобы улучшить принцип «открыто-закрыто». То же самое относится к инверсии управления и внедрению зависимости: их сложнее понять, чем простое решение. Вопрос заключался в том, чтобы «внедрить ... без значительного перехода», а не «как создать более интуитивное решение»
k3b
1
Я не вижу, чтобы вы также избегали ветвления. Вы только что изменили синтаксис.
фунтовые
1
Нет ли проблемы с этим решением в языках, которые не загружают классы, пока они не нужны? Статический код не будет работать до тех пор, пока класс не будет загружен, и класс никогда не будет загружен, потому что никакой другой класс не ссылается на него.
Кевин Клайн
2
Лично мне нравится этот метод, потому что он позволяет вам рассматривать ваши стратегии как изменяемые данные , а не как неизменяемый код.
Такрой
2

«Стратегия» - это необходимость выбирать между альтернативными алгоритмами хотя бы один раз , а не меньше. Где-то в вашей программе кто-то должен принять решение - может быть, пользователь или ваша программа. Если вы используете IoC, отражение, средство оценки файла данных или конструкцию switch / case для реализации этого не изменит ситуацию.

Док Браун
источник
Я бы не согласился с вашим утверждением один раз. Стратегии можно менять во время выполнения. Например, после сбоя при обработке запроса с использованием стандартной ProcessingStrategy можно было бы выбрать VerboseProcessingStrategy и перезапустить обработку.
dmux
@dmux: конечно, отредактировал мой ответ соответственно.
Док Браун
1

Шаблон стратегии используется, когда вы указываете возможные варианты поведения, и лучше всего используется при назначении обработчика при запуске. Указание того, какой экземпляр стратегии использовать, можно выполнить с помощью Mediator, вашего стандартного контейнера IoC, некоторого Фабрики, подобного описанному вами, или просто с помощью правильного, основанного на контексте (так как часто стратегия поставляется как часть более широкого использования). класса, который содержит его).

Для такого поведения, когда разные методы должны вызываться на основе данных, я бы предложил использовать конструкции полиморфизма, предоставляемые языком под рукой; не шаблон стратегии.

Telastyn
источник
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

Я думаю, что должно быть ограничение на количество приемлемых веток.

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

В любом случае, вы не можете избежать этого, и где-то вам придется проверить состояние или тип объекта, с которым вы работаете. Затем вы создадите стратегию для этого. Старое доброе разделение проблем.

CodeART
источник