Как провести рефакторинг приложения с несколькими вариантами переключения?

10

У меня есть приложение, которое принимает целое число в качестве ввода и на основе ввода вызывает статические методы различных классов. Каждый раз, когда добавляется новый номер, нам нужно добавить еще один случай и вызвать другой статический метод другого класса. Теперь в коммутаторе 50 дел, и каждый раз, когда мне нужно добавить еще одно дело, я содрогаюсь. Есть лучший способ сделать это.

Я немного подумал и придумал эту идею. Я использую шаблон стратегии. Вместо случая переключения у меня есть карта объектов стратегии с ключом, являющимся входным целым числом. Как только метод вызывается, он ищет объект и вызывает универсальный метод для объекта. Таким образом, я могу избежать использования конструкции switch switch.

Что вы думаете?

Каушик Чакраборты
источник
2
В чем проблема с текущим кодом?
Филипп Кендалл,
Что происходит, когда вам нужно внести одно из этих изменений? Нужно ли добавлять switchслучай и вызывать уже существующий метод в вашей сложной системе, или вам нужно изобрести как метод, так и его вызов?
Килиан Фот
@KilianFoth Я унаследовал этот проект как разработчика технического обслуживания, и мне еще не пришлось вносить какие-либо изменения. Однако я скоро внесу изменения, поэтому я хочу провести рефакторинг сейчас. Но чтобы ответить на ваш вопрос, да, последний.
Каушик Чакраборти
2
Я думаю, вам нужно показать сжатый пример того, что происходит.
whatsisname
1
@KaushikChakraborty: тогда сделайте пример из памяти. Существуют ситуации, когда убер-коммутатор в 250+ случаях подходит, и бывают случаи, когда коммутатор плох, независимо от того, как мало он имеет. Дьявол кроется в деталях, а у нас нет деталей.
whatsisname

Ответы:

13

Теперь в коммутаторе 50 дел, и каждый раз, когда мне нужно добавить еще одно дело, я содрогаюсь.

Я люблю полиморфизм. Я люблю SOLID. Я люблю чисто объектно-ориентированное программирование. Я ненавижу видеть их с плохой репутацией, потому что они применяются догматически.

Вы не нашли веского основания для рефакторинга стратегии. Кстати, у рефакторинга есть имя. Это называется Заменить Условное Полиморфизмом .

Я нашел несколько полезных советов для вас от c2.com :

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

У вас есть переключатель на 50 ящиков, и вы можете создать 50 предметов. Ох и 50 строк строительного кода объекта. Это не прогресс. Почему бы нет? Потому что этот рефакторинг ничего не делает для уменьшения числа от 50. Этот рефакторинг используется, когда вы обнаружите, что вам нужно создать еще один оператор switch для того же ввода где-то еще. Вот когда этот рефакторинг помогает, потому что он превращает 100 обратно в 50.

Пока вы говорите о «переключателе», как будто он единственный, который у вас есть, я не рекомендую этого. Единственное преимущество рефакторинга сейчас заключается в том, что он снижает вероятность того, что какой-нибудь дурак скопирует и вставит ваш переключатель на 50 случаев.

Я рекомендую внимательно изучить эти 50 случаев, чтобы выявить общие черты. Я имею в виду 50? В самом деле? Вы уверены, что вам нужно так много дел? Возможно, вы пытаетесь сделать многое здесь.

candied_orange
источник
Я согласен с тем, что вы говорите. В коде много избыточностей, может случиться так, что многие случаи даже не нужны, но на первый взгляд это не так. В каждом случае вызывается метод, который вызывает несколько систем, объединяет результаты и возвращает код вызова. Каждый класс самодостаточен, выполняет одну работу, и я боюсь, что нарушу принцип высокой сплоченности, если я уменьшу количество случаев.
Каушик Чакраборти
2
Я могу получить 50, не нарушая высокую сплоченность и держать вещи самостоятельно. Я просто не могу сделать это с одним номером. Мне нужно 2, 5 и еще 5. Поэтому это называется факторингом. Серьезно, посмотрите на всю свою архитектуру и посмотрите, не можете ли вы найти выход из этого ада, в котором вы находитесь. Рефакторинг - это отмена плохих решений. Не увековечивая их в новых формах.
candied_orange
Теперь, если вы можете увидеть способ уменьшить 50 с помощью этого рефакторинга, сделайте это. Чтобы использовать идею Дока Браунса: карта карт может занять два ключа. Что-то думать о.
candied_orange
1
Я согласен с комментарием Candied. Проблема не в 50 случаях в операторе switch, а в проблеме архитектуры верхнего уровня, которая заставляет вас вызывать функцию, которая должна выбирать между 50 вариантами. Я разработал несколько очень больших и сложных систем и никогда не сталкивался с подобной ситуацией.
Данк
@Candied "Вы используете этот рефакторинг, когда обнаружите, что вам нужно создать еще один оператор switch для того же самого входа где-нибудь еще." Можете ли вы уточнить это? У меня есть похожий случай, когда у меня есть случаи переключений, но на разных слоях, как в нашем Сначала авторизация проекта, валидация, процедуры CRUD, затем dao. Таким образом, в каждом слое есть переключатели на одном входе, то есть на целое число, но выполняющие разные функции, такие как auth, valid. поэтому мы должны создать один класс для каждого типа, который имеет разные методы? Вписывается ли наш случай в то, что вы пытаетесь сказать, «повторяя один и тот же переключатель на том же входе»?
Сиддхарт Триха
9

Карта только объектов стратегии, которая инициализируется в некоторой функции вашего кода, где у вас есть несколько строк кода, похожих на

     myMap.Add(1,new Strategy1());
     myMap.Add(2,new Strategy2());
     myMap.Add(3,new Strategy3());

требует от вас и ваших коллег реализации функций / стратегий, вызываемых в отдельных классах, более единообразным образом (поскольку все объекты стратегии должны будут реализовывать один и тот же интерфейс). Такой код часто немного более полон, чем

     case 1:
          MyClass1.Doit1(someParameters);
          break;
     case 2:
          MyClass2.Doit2(someParameters);
          break;
     case 3:
          MyClass3.Doit3(someParameters);
          break;

Тем не менее, он по-прежнему не освобождает вас от бремени редактирования этого файла кода всякий раз, когда необходимо добавить новый номер. Реальные преимущества этого подхода в другом:

  • инициализация карты теперь отделена от кода отправки, который фактически вызывает функцию, связанную с конкретным номером, и последний больше не содержит эти 50 повторений, это будет просто выглядеть myMap[number].DoIt(someParameters). Таким образом, этот код отправки не нужно трогать всякий раз, когда приходит новый номер, и он может быть реализован в соответствии с принципом Open-Closed. Более того, когда вы получаете требования, в которых вам необходимо расширить сам код отправки, вам больше не придется менять 50 мест, а только одно.

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

Так что да, есть некоторые преимущества, и это, безусловно, шаг к более твердому коду. Однако, если это окупится рефакторингом, это то, что вам или вашей команде придется решать самостоятельно. Если вы не ожидаете, что код отправки будет изменен, логика инициализации будет изменена, и читаемость switchне будет реальной проблемой, то ваш рефакторинг может быть не так важен сейчас.

Док Браун
источник
Хотя я не хочу слепо заменять каждый переключатель полиморфизмом, я скажу, что использование карты, как советует Док Браун, хорошо сработало в прошлом. При реализации такой же интерфейс , пожалуйста , заменить Doit1, Doit2и т.д. с одним Doitметодом , который имеет много различных реализаций.
candied_orange
И если у вас есть контроль над типом входного символа, используемого в качестве ключа, вы можете пойти дальше, сделав doTheThing()метод входного символа. Тогда вам не нужно поддерживать карту.
Кевин Крумвиде,
1
@KevinKrumwiede: то, что вы предлагаете, означает простую передачу самих объектов стратегии в программе в качестве замены целых чисел. Однако, когда программа принимает целое число в качестве входных данных от некоторого внешнего источника данных, должно быть отображение целого числа на связанную стратегию, по крайней мере, в одном месте системы.
Док Браун
Продолжая предложение Дока Брауна: вы можете также создать фабрику, которая будет содержать логику для создания объектов стратегии, если вы решите пойти по этому пути. Тем не менее, ответ, предоставленный CandiedOrange, имеет для меня наибольшее значение.
Владимир Стокич
@DocBrown Это то, что я понял, «если у вас есть контроль над типом входного символа».
Кевин Крумвиде,
0

Я решительно поддерживаю стратегию, изложенную в ответе @DocBrown .

Я собираюсь предложить улучшение ответа.

Звонки

 myMap.Add(1,new Strategy1());
 myMap.Add(2,new Strategy2());
 myMap.Add(3,new Strategy3());

можно распространять. Вам не нужно возвращаться к тому же файлу, чтобы добавить другую стратегию, которая еще лучше придерживается принципа Open-Closed.

Скажем, вы реализуете Strategy1в файле Strategy1.cpp. Вы можете иметь следующий блок кода в нем.

namespace Strategy1_Impl
{
   struct Initializer
   {
      Initializer()
      {
         getMap().Add(1, new Strategy1());
      }
   };
}
using namespace Strategy1_Impl;

static Initializer initializer;

Вы можете повторить один и тот же код в каждом файле StategyN.cpp. Как видите, это будет много повторяющегося кода. Чтобы уменьшить дублирование кода, вы можете использовать шаблон, который можно поместить в файл, доступный для всех Strategyклассов.

namespace StrategyHelper
{
   template <int N, typename StrategyType> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new StrategyType());
      }
   };
}

После этого единственное, что вам нужно использовать в Strategy1.cpp:

static StrategyHelper::Initializer<1, Strategy1> initializer;

Соответствующая строка в StrategyN.cpp:

static StrategyHelper::Initializer<N, StrategyN> initializer;

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

class Strategy { ... };

template <int N> class ConcreteStrategy;

А потом вместо Strategy1использования ConcreteStrategy<1>.

template <> class ConcreteStrategy<1> : public Strategy { ... };

Измените вспомогательный класс для регистрации Strategys:

namespace StrategyHelper
{
   template <int N> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new ConcreteStrategy<N>());
      }
   };
}

Измените код в Strateg1.cpp на:

static StrategyHelper::Initializer<1> initializer;

Измените код в StrategN.cpp на:

static StrategyHelper::Initializer<N> initializer;
Р Саху
источник