Как можно исключить использование ключа в коде?
Как можно исключить использование ключа в коде?
Ответы:
Операторы 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();
}
typeof
, и в этом ответе не предлагаются способы или причины обхода операторов switch в других ситуациях.
См. Запах Заявлений Переключателя :
Как правило, подобные операторы 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();
}
}
Надеюсь это поможет.
Map<Integer, Command>
не нуждается в переключателе?
Переключатель - это шаблон, независимо от того, реализован ли он с помощью оператора 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
операторы было бы хорошо заменить, если вы обнаружите, что добавляете новые состояния или новое поведение в операторы:
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 (а также более легким для чтения), и, скорее всего, ускорятся только самые большие переключатели, если они будут заменены каким-либо видом косвенной структуры данных поиска.
«switch» - это просто языковая конструкция, и все языковые конструкции можно рассматривать как инструменты для выполнения работы. Как и в случае с реальными инструментами, некоторые инструменты лучше подходят для одной задачи, чем для другой (вы не использовали бы кувалду для подвешивания крючка для фото). Важной частью является то, как определяется «выполнение работы». Должен ли он быть обслуживаемым, быстрым, масштабируемым, расширяемым и т. Д.
В каждой точке процесса программирования обычно существует ряд конструкций и шаблонов, которые можно использовать: переключатель, последовательность if-else-if, виртуальные функции, таблицы переходов, карты с указателями функций и так далее. С опытом программист инстинктивно узнает, какой инструмент использовать в данной ситуации.
Следует предположить, что любой, кто поддерживает или просматривает код, по крайней мере так же квалифицирован, как и автор оригинала, так что любая конструкция может быть безопасно использована.
Если есть переключатель, позволяющий различать различные типы объектов, вы, вероятно, упускаете некоторые классы для точного описания этих объектов или некоторые виртуальные методы ...
Для C ++
Если вы имеете в виду, например, AbstractFactory, я думаю, что метод registerCreatorFunc (..) обычно лучше, чем требование добавлять регистр для каждого необходимого «нового» оператора. Затем позволяем всем классам создавать и регистрировать функцию creatorFunction (..), которая может быть легко реализована с помощью макроса (если я осмелюсь упомянуть). Я считаю, что это общий подход, который используют многие структуры. Я впервые увидел это в ET ++, и я думаю, что многие фреймворки, требующие макроса DECL и IMPL, используют его.
В процедурном языке, таком как C, переключение будет лучше, чем любая из альтернатив.
В объектно-ориентированном языке почти всегда есть другие доступные альтернативы, которые лучше используют структуру объекта, особенно полиморфизм.
Проблема с операторами switch возникает, когда несколько очень похожих блоков переключателей появляются в нескольких местах приложения, и необходимо добавить поддержку нового значения. Разработчик довольно часто забывает добавить поддержку нового значения в один из блоков переключателей, разбросанных по всему приложению.
При полиморфизме новый класс заменяет новое значение, и новое поведение добавляется как часть добавления нового класса. Поведение в этих точках переключения затем либо наследуется от суперкласса, переопределяется для обеспечения нового поведения, либо реализуется во избежание ошибки компилятора, когда метод super является абстрактным.
Там, где не происходит явного полиморфизма, может быть целесообразно реализовать шаблон Стратегии .
Но если ваша альтернатива - это большой блок IF ... THEN ... ELSE, то забудьте об этом.
Указатели на функции - это один из способов заменить громоздкий оператор 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) или функции, которые являются значениями 1-го класса (Lua), вы можете добиться результатов, аналогичных «переключению», используя массив (или список) из (указателей) функций.
Вы должны быть более конкретными на языке, если вы хотите получить лучшие ответы.
Операторы Switch часто могут быть заменены хорошим дизайном OO.
Например, у вас есть класс «Учетная запись», и вы используете оператор переключения для выполнения другого расчета в зависимости от типа учетной записи.
Я бы предложил заменить его на ряд классов учетных записей, представляющих различные типы учетных записей, и все они реализуют интерфейс учетной записи.
В этом случае переключение становится ненужным, поскольку вы можете обрабатывать все типы учетных записей одинаково, и благодаря полиморфизму будет выполнен соответствующий расчет для типа учетной записи.
Зависит от того, почему вы хотите заменить его!
Многие интерпретаторы используют «вычисленные переходы» вместо операторов switch для выполнения кода операции.
Что мне не хватает в переключателе C / C ++, так это Pascal 'in' и диапазоны. Я также хотел бы включить струны. Но они, хотя и являются тривиальными для компилятора, являются тяжелой работой, когда они выполняются с использованием структур, итераторов и прочего. Так что, напротив, есть много вещей, которые я хотел бы заменить на переключатель, если бы только ключ C () был более гибким!
Переключение не очень хороший способ, так как он нарушает принцип 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 используется ассоциативный массив:
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);
}
Еще один голос за если / еще. Я не большой поклонник дел или заявлений о смене, потому что есть люди, которые их не используют. Код менее читабелен, если вы используете регистр или переключатель. Может быть, не менее читабельным для вас, но для тех, кому никогда не нужно было использовать команду.
То же самое касается объектных фабрик.
Блоки 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 концептуально отличается настолько, что его нельзя использовать.
Надеюсь, что отвечает на ваши вопросы.