Способы устранения переключения в коде

Как можно исключить использование ключа в коде?

23 ответа

Решение

Операторы switch сами по себе не являются антипаттернами, но если вы кодируете объектно-ориентированные, вы должны подумать, лучше ли использовать переключение с помощью полиморфизма, а не с помощью оператора switch.

С полиморфизмом это:

foreach (var animal in zoo) {
    switch (typeof(animal)) {
        case "dog":
            echo animal.bark();
            break;

        case "cat":
            echo animal.meow();
            break;
    }
}

становится так:

foreach (var animal in zoo) {
    echo animal.speak();
}

См. Запах Заявлений Переключателя:

Как правило, подобные операторы switch разбросаны по всей программе. Если вы добавляете или удаляете предложение в одном коммутаторе, вам часто приходится искать и ремонтировать другие.

И Рефакторинг, и Рефакторинг на Шаблоны имеют подходы для решения этой проблемы.

Если ваш (псевдо) код выглядит так:

class RequestHandler {

    public void handleRequest(int action) {
        switch(action) {
            case LOGIN:
                doLogin();
                break;
            case LOGOUT:
                doLogout();
                break;
            case QUERY:
               doQuery();
               break;
        }
    }
}

Этот код нарушает принцип Open Closed и является хрупким по отношению к каждому новому типу кода действия. Чтобы исправить это, вы можете ввести объект "Command":

interface Command {
    public void execute();
}

class LoginCommand implements Command {
    public void execute() {
        // do what doLogin() used to do
    }
}

class RequestHandler {
    private Map<Integer, Command> commandMap; // injected in, or obtained from a factory
    public void handleRequest(int action) {
        Command command = commandMap.get(action);
        command.execute();
    }
}

Если ваш (псевдо) код выглядит так:

class House {
    private int state;

    public void enter() {
        switch (state) {
            case INSIDE:
                throw new Exception("Cannot enter. Already inside");
            case OUTSIDE:
                 state = INSIDE;
                 ...
                 break;
         }
    }
    public void exit() {
        switch (state) {
            case INSIDE:
                state = OUTSIDE;
                ...
                break;
            case OUTSIDE:
                throw new Exception("Cannot leave. Already outside");
        }
    }

Тогда вы можете ввести объект "State".

// Throw exceptions unless the behavior is overriden by subclasses
abstract class HouseState {
    public HouseState enter() {
        throw new Exception("Cannot enter");
    }
    public HouseState leave() {
        throw new Exception("Cannot leave");
    }
}

class Inside extends HouseState {
    public HouseState leave() {
        return new Outside();
    }
}

class Outside extends HouseState {
    public HouseState enter() {
        return new Inside();
    }
}

class House {
    private HouseState state;
    public void enter() {
        this.state = this.state.enter();
    }
    public void leave() {
        this.state = this.state.leave();
    }
}

Надеюсь это поможет.

Переключатель - это шаблон, независимо от того, реализован ли он с помощью оператора switch, цепочки, таблицы поиска, полиморфизма oop, сопоставления с шаблоном или чего-то еще.

Вы хотите исключить использование "оператора switch" или "шаблона switch"? Первый может быть исключен, второй, только если можно использовать другой шаблон / алгоритм, и большую часть времени это невозможно или это не лучший подход для этого.

Если вы хотите исключить оператор switch из кода, первый вопрос, который нужно задать, - это где имеет смысл исключить оператор switch и использовать какой-то другой метод. К сожалению, ответ на этот вопрос зависит от конкретной области.

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

Вот несколько альтернатив для переключения оператора:

Само по себе переключение не так уж и плохо, но если у вас в ваших методах много "switch" или "if/else" для объектов, это может быть признаком того, что ваш дизайн немного "процедурный" и что ваши объекты просто ценны ковши. Переместите логику в ваши объекты, вызовите метод для ваших объектов и дайте им решить, как реагировать.

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

Ваш код будет выглядеть примерно так (psuedo):

void InitMap(){
    Map[key1] = Object/Action;
    Map[key2] = Object/Action;
}

Object/Action DoStuff(Object key){
    return Map[key];
}

Все любят ОГРОМНОЕ if else блоки. Так легко читать! Мне любопытно, почему вы хотели бы удалить операторы switch, хотя. Если вам нужен оператор switch, вам, вероятно, нужен оператор switch. Если серьезно, я бы сказал, что это зависит от того, что делает код. Если все, что делает переключатель, это вызывает функции (скажем), вы можете передавать указатели на функции. Является ли это лучшим решением является дискуссионным.

Язык также является важным фактором и здесь, я думаю.

Я думаю, что вы ищете шаблон стратегии.

Это может быть реализовано несколькими способами, которые были упомянуты в других ответах на этот вопрос, таких как:

  • Карта значений -> функции
  • Полиморфизм. (подтип объекта будет определять, как он обрабатывает конкретный процесс).
  • Функции первого класса.

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

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

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

switch операторы было бы хорошо заменить, если вы обнаружите, что добавляете новые состояния или новое поведение в операторы:

Int State;

String getString () {
   переключатель (состояние) {
     case 0: // поведение для состояния 0
           вернуть "ноль";
     case 1: // поведение для состояния 1
           вернуть "один";
   }
   бросить новый IllegalStateException();
}

double getDouble () {

   switch (this.state) {
     case 0: // поведение для состояния 0
           возврат 0d;
     case 1: // поведение для состояния 1
           возврат 1d;
   }
   бросить новый IllegalStateException();
}

Добавление нового поведения требует копирования switch и добавление новых состояний означает добавление другого case каждому switch заявление.

В Java вы можете переключать только очень ограниченное количество примитивных типов, значения которых вы знаете во время выполнения. Это само по себе представляет проблему: состояния представляются в виде магических чисел или символов.

Сопоставление с образцом и несколько if - else блоки могут использоваться, хотя на самом деле возникают те же проблемы при добавлении нового поведения и новых состояний.

Решение, которое другие предложили как "полиморфизм", является примером модели состояния:

Замените каждое из состояний своим собственным классом. Каждое поведение имеет свой собственный метод в классе:

Состояние государства;

String getString () {
   return state.getString ();
}

double getDouble () {
   return state.getDouble ();
}

Каждый раз, когда вы добавляете новое состояние, вы должны добавить новую реализацию IState интерфейс. В switch мир, вы бы добавили case для каждого switch,

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

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

Если еще

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

Ну, во-первых, я не знал, что использование переключателя было анти-паттерном.

Во-вторых, switch всегда можно заменить на операторы if / else if.

Почему ты хочешь? В руках хорошего компилятора оператор switch может быть намного более эффективным, чем блоки if/else (а также более легким для чтения), и только самые большие переключатели, вероятно, будут ускорены, если они будут заменены каким-либо видом косвенной структуры данных поиска.

В процедурном языке, таком как C, переключение будет лучше, чем любая из альтернатив.

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

Проблема с операторами switch возникает, когда несколько очень похожих блоков переключателей встречаются в нескольких местах приложения, и необходимо добавить поддержку нового значения. Разработчик довольно часто забывает добавить поддержку нового значения в один из блоков переключателей, разбросанных по всему приложению.

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

Там, где не происходит явного полиморфизма, вполне возможно, стоит реализовать шаблон Стратегии.

Но если ваша альтернатива - большой блок IF ... THEN ... ELSE, то забудьте об этом.

Переключение не очень хороший способ, так как он нарушает принцип Open Close. Вот как я это делаю.

public class Animal
{
       public abstract void Speak();
}


public class Dog : Animal
{
   public virtual void Speak()
   {
       Console.WriteLine("Hao Hao");
   }
}

public class Cat : Animal
{
   public virtual void Speak()
   {
       Console.WriteLine("Meauuuu");
   }
}

А вот как это использовать (принимая ваш код):

foreach (var animal in zoo) 
{
    echo animal.speak();
}

В основном то, что мы делаем, - делегирование ответственности дочернему классу вместо того, чтобы родитель решал, что делать с детьми.

Вы также можете прочитать "Принцип замещения Лискова".

В JavaScript используется ассоциативный массив:
этот:

function getItemPricing(customer, item) {
    switch (customer.type) {
        // VIPs are awesome. Give them 50% off.
        case 'VIP':
            return item.price * item.quantity * 0.50;

            // Preferred customers are no VIPs, but they still get 25% off.
        case 'Preferred':
            return item.price * item.quantity * 0.75;

            // No discount for other customers.
        case 'Regular':
        case
        default:
            return item.price * item.quantity;
    }
}

становится так:

function getItemPricing(customer, item) {
var pricing = {
    'VIP': function(item) {
        return item.price * item.quantity * 0.50;
    },
    'Preferred': function(item) {
        if (item.price <= 100.0)
            return item.price * item.quantity * 0.75;

        // Else
        return item.price * item.quantity;
    },
    'Regular': function(item) {
        return item.price * item.quantity;
    }
};

    if (pricing[customer.type])
        return pricing[customer.type](item);
    else
        return pricing.Regular(item);
}

учтивость

Используйте язык, который не поставляется со встроенным оператором switch. Perl 5 приходит на ум.

Если серьезно, почему вы хотите избежать этого? И если у вас есть веские основания избегать этого, то почему бы просто не избежать этого?

Указатели на функции являются одним из способов заменить громоздкие операторы switch, они особенно хороши в языках, где вы можете захватывать функции по их именам и создавать что-то с ними.

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

Это отличный пример "разделяй и властвуй":

Скажем, у вас есть какой-то переводчик.

switch(*IP) {
    case OPCODE_ADD:
        ...
        break;
    case OPCODE_NOT_ZERO:
        ...
        break;
    case OPCODE_JUMP:
        ...
        break;
    default:
        fixme(*IP);
}

Вместо этого вы можете использовать это:

opcode_table[*IP](*IP, vm);

... // in somewhere else:
void opcode_add(byte_opcode op, Vm* vm) { ... };
void opcode_not_zero(byte_opcode op, Vm* vm) { ... };
void opcode_jump(byte_opcode op, Vm* vm) { ... };
void opcode_default(byte_opcode op, Vm* vm) { /* fixme */ };

OpcodeFuncPtr opcode_table[256] = {
    ...
    opcode_add,
    opcode_not_zero,
    opcode_jump,
    opcode_default,
    opcode_default,
    ... // etc.
};

Обратите внимание, что я не знаю, как удалить избыточность opcode_table в C. Возможно, я должен задать вопрос об этом.:)

Для C++

Если вы имеете в виду, например, AbstractFactory, я думаю, что метод registerCreatorFunc(..) обычно лучше, чем требование добавлять регистр для каждого необходимого "нового" оператора. Затем позволяем всем классам создавать и регистрировать функцию creatorFunction(..), которая может быть легко реализована с помощью макроса (если я осмелюсь упомянуть). Я считаю, что это общий подход, который используют многие структуры. Я впервые увидел это в ET++, и я думаю, что многие фреймворки, требующие макроса DECL и IMPL, используют его.

Если есть переключатель, позволяющий различать различные виды объектов, вы, вероятно, упускаете некоторые классы для точного описания этих объектов или некоторые виртуальные методы...

Операторы Switch часто могут быть заменены хорошим дизайном OO.

Например, у вас есть класс Account, и вы используете оператор переключения для выполнения другого расчета в зависимости от типа аккаунта.

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

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

Наиболее очевидный, независимый от языка ответ - использовать серию "если".

Если используемый вами язык имеет указатели на функции (C) или имеет функции со значениями 1-го класса (Lua), вы можете достичь результатов, аналогичных "переключению", используя массив (или список) из (указателей) функций.

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

Зависит от того, почему вы хотите заменить его!

Многие интерпретаторы используют "вычисленные переходы" вместо операторов switch для выполнения кода операции.

Чего мне не хватает в переключателе C/C++, так это в Pascal 'in' и диапазонах. Я также хотел бы включить струны. Но они, хотя и являются тривиальными для компилятора, являются тяжелой работой, когда они выполняются с использованием структур, итераторов и прочего. Так что, наоборот, есть много вещей, которые я бы хотел заменить на переключатель, если бы только ключ C () был более гибким!

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

То же самое касается объектных фабрик.

Блоки if / else - это простая конструкция, которую получают все. Есть несколько вещей, которые вы можете сделать, чтобы убедиться, что у вас нет проблем.

Во-первых, не пытайтесь делать отступы, если заявления более чем пару раз. Если вы обнаружите, что делаете отступы, значит, вы делаете это неправильно.

 if a = 1 then 
     do something else 
     if a = 2 then 
         do something else
     else 
         if a = 3 then 
             do the last thing
         endif
     endif 
  endif

Это действительно плохо - делай это вместо этого.

if a = 1 then 
   do something
endif 
if a = 2 then 
   do something else
endif 
if a = 3 then 
   do something more
endif 

Оптимизация будь проклята. Это не сильно влияет на скорость вашего кода.

Во-вторых, я не против выхода из блока If, если в определенном блоке кода разбросано достаточно операторов break, чтобы сделать его очевидным

procedure processA(a:int)
    if a = 1 then 
       do something
       procedure_return
    endif 
    if a = 2 then 
       do something else
       procedure_return
    endif 
    if a = 3 then 
       do something more
       procedure_return
    endif 
end_procedure

РЕДАКТИРОВАТЬ: On Switch и почему я думаю, что трудно уловить:

Вот пример оператора switch...

private void doLog(LogLevel logLevel, String msg) {
   String prefix;
   switch (logLevel) {
     case INFO:
       prefix = "INFO";
       break;
     case WARN:
       prefix = "WARN";
       break;
     case ERROR:
       prefix = "ERROR";
       break;
     default:
       throw new RuntimeException("Oops, forgot to add stuff on new enum constant");
   }
   System.out.println(String.format("%s: %s", prefix, msg));
 }

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

например

for i from 1 to 1000 {statement1; statement2}
if something=false then {statement1; statement2}
while isOKtoLoop {statement1; statement2}

Для меня (и вы можете исправить меня, если я ошибаюсь), оператор Case выбрасывает это правило в окно. Условно исполняемый блок кода не помещается в структуру начала / конца. Из-за этого, я считаю, что Case концептуально достаточно отличается, чтобы его нельзя было использовать.

Надеюсь, что отвечает на ваши вопросы.

Другие вопросы по тегам