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


175

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


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

Я голосую за этот вопрос, чтобы сделать сообщество доступным для редактирования, потому что все говорят одно и то же, и было бы неплохо объединить все мнения;)
Джош

2
Переключатель не так стандартен, как другие инструкции. Например, в C ++ вы, вероятно, забудете «перерыв», и тогда вы получите неожиданные результаты. Кроме того, этот «перерыв» слишком похож на GOTO. Я никогда не пытался исключить переключатели, но они точно не мои любимые инструкции. ;)

2
Я думаю, что он имеет в виду концепцию переключения, а не оператор переключения C ++ или Java. Переключатель также может быть цепочкой блоков if-else if.
Программист вне закона

2
Вы можете быть удивлены: большое количество известных программистов из гибкого сообщества присоединяются к кампании против IF: antiifcampaign.com/supporters.html
Mike A

Ответы:


267

Операторы 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();
}

2
Я был избит из-за подобного предложения в stackoverflow.com/questions/374239/… Столько людей не верят в полиморфизм :) Очень хороший пример.
Назгоб

30
-1: я никогда не использовал оператор switch typeof, и в этом ответе не предлагаются способы или причины обхода операторов switch в других ситуациях.
Кевин

3
Я согласен с @Kevin - данный пример не показывает, как устранить переключение при помощи полиморфизма. Простые примеры: получить имя перечисления путем переключения его значений или выполнить некоторый код с помощью соответствующих значений в каком-то алгоритме.
HotJard

Мне было интересно, как устранить это, прежде чем можно полагаться на конкретные реализации, то есть на заводе. И я нашел отличный пример здесь stackoverflow.com/a/3434505/711855
juanmf

1
очень тривиальный пример.
GuardianX

239

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

Как правило, подобные операторы 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();
    }
}

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


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

8
Аргументы против переключения действительны до тех пор, пока вы понимаете, что полиморфное решение жертвует простотой кода. Кроме того, если вы всегда сохраняете регистры коммутатора в перечислениях, некоторые компиляторы предупреждают вас, что в коммутаторе отсутствуют состояния.
Харви

1
Это отличный пример о полных / неполных операциях и, конечно, реструктуризации кода в ООП. Благодаря тонну. Я думаю, что будет полезно, если сторонники ООП / Шаблонов проектирования предложат рассматривать концепции ООП как операторы, а не как концепции . Я имею в виду, что "extends", "factory", "Implements" и т. Д. Так часто используются в файлах, классах, ветвях. Они должны быть простыми как операторы типа «+», «-», «+ =», «?:», «==», «->» и т. Д. Когда программист использует их в своем уме так же просто, как операторы, только затем он может обдумать всю библиотеку классов о состоянии программы и (не) завершенных операциях.
namespaceform

13
Я начинаю думать, что SWITCH гораздо более понятен и логичен по сравнению с этим. И мне обычно очень нравится ООП, но эта резолюция кажется мне слишком абстрактной
JK

1
С объектом Command, код для генерации Map<Integer, Command>не нуждается в переключателе?
ataulm

41

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

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

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

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

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


1
Может ли кто-нибудь дать сравнение обработки сообщений с использованием коммутатора и других альтернатив?
Майк

37

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


Предполагая, конечно, что он не пишет на C. :)
Бернард

1
в C он может (ab?) использовать указатели на функции и структуры для создания чего-то подобного объекту;)
Tetha

Вы можете написать FORT ^ H ^ H ^ H ^ H Java на любом языке. ; р
Бернард

Полностью согласен - switch - это отличный способ сократить строки кода, но не играйте с ним слишком много.
HotJard

21

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

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

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

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

4
Зависит от языка. Это может стать намного менее читабельным, чем переключатель
Vinko Vrsalovic

Это хорошее решение в правильной ситуации. Я недавно сделал это сопоставление кодов клавиш, и, похоже, с этой целью все нормально читалось.
Бернард

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

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

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

12

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

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


13
Я предполагаю, что это был сарказм :)
День Крейга

6

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

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

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

5

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интерфейс и каждую из реализаций. Это то же бремя, что и раньше, хотя теперь компилятор будет проверять, есть ли у вас реализации нового поведения в каждом ранее существующем состоянии.

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



3

Ну, во-первых, я не знал, что использование switch - это анти-паттерн.

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


Точно - switch - это просто синтаксический метадон для группы if / elsifs.
Майк

3

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


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

3

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

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

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


Хорошо, но зачем нам 5 разных избыточных способов сделать одно и то же - условное выполнение?
Майк

@ mike.amy: потому что каждый метод имеет свои преимущества и затраты, и все дело в получении максимальной выгоды при наименьших затратах.
Skizz

1

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


1

Для C ++

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


1

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

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

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

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

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

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


1

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

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


1

Указатели на функции - это один из способов заменить громоздкий оператор 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. Возможно, я должен задать вопрос об этом. :)


0

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

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

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


0

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

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

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

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


0

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

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

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


0

Переключение не очень хороший способ, так как он нарушает принцип 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();
}

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

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


0

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

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);
}

учтивость


-12

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

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

Блоки 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 концептуально отличается настолько, что его нельзя использовать.

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


Ух ты - явно спорный ответ. Мне было бы интересно узнать, что я так неправильно понял.
seanyboy

Э-э, переключиться как слишком сложный? Я не знаю ... мне кажется, что не осталось бы много языковых функций, которые вы могли бы использовать. :) Кроме того, в вашем центральном примере было бы разумнее сделать, если (a == 1 || a == 2 || a == 3) что-то сделать?
Ларс Вестергрен

Кроме того, в вашем последнем примере «break» ничего не будет делать на большинстве языков - это разрывается из ближайшего блока (обычно цикла), что в любом случае происходит на следующей строке (endif). Если вы используете язык, в котором break - это «возврат», многие возражения также недовольны (за исключением «защитных заявлений»)
Ларс Вестергрен,

4
«Оптимизация, черт возьми. Это не имеет большого значения для скорости вашего кода». Мой код работает на мобильной платформе. Оптимизация имеет значение. Кроме того, переключатели могут сделать код ОЧЕНЬ чистым (по сравнению с if..elseif..elseif..elseif ...) при правильном использовании. Никогда их не видел? Изучите их.
Свати

Переключение совсем не сложно, но я всегда старался минимизировать количество конструкций, используемых в коде, чтобы уменьшить трения в понимании.
seanyboy
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.