design patterns - pattern - Maneras de eliminar el interruptor en el código
adapter pattern c# (23)
¡Depende de por qué quieres reemplazarlo!
Muchos intérpretes usan ''gotos computados'' en lugar de declaraciones switch para la ejecución del código de operación.
Lo que extraño del cambio C / C ++ es el Pascal ''in'' y los rangos. También me gustaría poder encender cadenas. Pero estos, aunque son triviales para un compilador, son un trabajo duro cuando se hace usando estructuras e iteradores y cosas. Entonces, por el contrario, hay muchas cosas que desearía poder reemplazar con un interruptor, ¡si solo el interruptor de C () fuera más flexible!
¿Cuáles son las formas de eliminar el uso del interruptor en el código?
¿Por qué quieres? En manos de un buen compilador, una instrucción switch puede ser mucho más eficiente que if / else blocks (además de ser más fácil de leer), y solo los switches más grandes se acelerarán si son reemplazados por cualquier tipo de la estructura de datos de búsqueda indirecta.
''switch'' es solo una construcción de lenguaje y todos los constructos de lenguaje pueden considerarse como herramientas para hacer un trabajo. Al igual que con las herramientas reales, algunas herramientas se adaptan mejor a una tarea que a otra (no usaría un martillo para colocar un gancho de imagen). La parte importante es cómo se define ''hacer el trabajo''. ¿Necesita ser mantenible, necesita ser rápido, necesita escalar, necesita ser extensible y demás?
En cada punto del proceso de programación generalmente hay una gama de construcciones y patrones que se pueden usar: un interruptor, una secuencia if-else-if, funciones virtuales, tablas de salto, mapas con punteros de función, etc. Con experiencia, un programador conocerá instintivamente la herramienta adecuada para usar en una situación determinada.
Se debe suponer que cualquier persona que mantenga o revise el código es al menos tan experto como el autor original, de modo que cualquier construcción se puede usar con seguridad.
Bueno, para empezar, no sabía que usar el interruptor era un anti patrón.
En segundo lugar, el cambio siempre se puede reemplazar con declaraciones if / else if.
Cambiar no es una buena forma de ir ya que rompe el Open Close Principal. Así es como lo hago.
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");
}
}
Y aquí está cómo usarlo (tomando su código):
foreach (var animal in zoo)
{
echo animal.speak();
}
Básicamente, lo que estamos haciendo es delegar la responsabilidad a la clase infantil en lugar de que el padre decida qué hacer con los niños.
También puede leer sobre el "Principio de sustitución de Liskov".
Creo que la mejor manera es usar un buen Mapa. Usando un diccionario puede mapear casi cualquier entrada a algún otro valor / objeto / función.
su código se vería algo (psuedo) como este:
void InitMap(){
Map[key1] = Object/Action;
Map[key2] = Object/Action;
}
Object/Action DoStuff(Object key){
return Map[key];
}
Creo que lo que estás buscando es el Patrón de estrategia.
Esto se puede implementar de varias maneras, que se han mencionado en otras respuestas a esta pregunta, como por ejemplo:
- Un mapa de valores -> funciones
- Polimorfismo. (el subtipo de un objeto decidirá cómo maneja un proceso específico).
- Funciones de primera clase.
El cambio en sí mismo no es tan malo, pero si tiene muchos "cambiar" o "si / más" en los objetos en sus métodos, puede ser una señal de que su diseño es un poco "de procedimiento" y que sus objetos son solo de valor cubos. Mueva la lógica a sus objetos, invoque un método en sus objetos y déjelos decidir cómo responder en su lugar.
En JavaScript usando una matriz asociativa:
esta:
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;
}
}
se convierte en esto:
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);
}
En un lenguaje de procedimiento, como C, entonces el cambio será mejor que cualquiera de las alternativas.
En un lenguaje orientado a objetos, existen casi siempre otras alternativas disponibles que mejor utilizan la estructura del objeto, particularmente el polimorfismo.
El problema con las instrucciones de conmutación se produce cuando se producen varios bloques de conmutación muy similares en varios lugares de la aplicación, y es necesario agregar soporte para un nuevo valor. Es bastante común que un desarrollador olvide agregar soporte para el nuevo valor a uno de los bloques de interruptores diseminados por la aplicación.
Con el polimorfismo, una nueva clase reemplaza el nuevo valor y el nuevo comportamiento se agrega como parte de agregar la nueva clase. El comportamiento en estos puntos de conmutación luego se hereda de la superclase, se reemplaza para proporcionar un nuevo comportamiento o se implementa para evitar un error del compilador cuando el supermétodo es abstracto.
Donde no existe un polimorfismo evidente, puede valer la pena implementar el patrón de Estrategia .
Pero si tu alternativa es un gran IF ... THEN ... ELSE block, entonces olvídalo.
La respuesta más obvia e independiente del lenguaje es usar una serie de ''si''.
Si el idioma que está utilizando tiene punteros de función (C) o tiene funciones que son valores de primera clase (Lua), puede obtener resultados similares a un "cambio" utilizando una matriz (o una lista) de funciones (de punteros a).
Deberías ser más específico en el idioma si quieres mejores respuestas.
Las instrucciones de cambio a menudo se pueden reemplazar por un buen diseño de OO.
Por ejemplo, tiene una clase Account y está utilizando una instrucción switch para realizar un cálculo diferente según el tipo de cuenta.
Sugeriría que esto deba ser reemplazado por una cantidad de clases de cuenta, que representen los diferentes tipos de cuenta, y todas implementando una interfaz de Cuenta.
El cambio se vuelve innecesario, ya que puede tratar todos los tipos de cuentas de la misma manera y gracias al polimorfismo, se ejecutará el cálculo apropiado para el tipo de cuenta.
Los enunciados de conmutación no son antipatrón per se, pero si está orientado a objetos de codificación, debe considerar si el uso de un conmutador se resuelve mejor con polymorphism lugar de utilizar una instrucción de conmutación.
Con polimorfismo, esto:
foreach (var animal in zoo) {
switch (typeof(animal)) {
case "dog":
echo animal.bark();
break;
case "cat":
echo animal.meow();
break;
}
}
se convierte en esto:
foreach (var animal in zoo) {
echo animal.speak();
}
Los punteros de función son una forma de reemplazar una enorme instrucción de interruptor grueso, son especialmente buenos en los idiomas en los que puede capturar funciones por sus nombres y hacer cosas con ellas.
Por supuesto, no debe forzar declaraciones de cambio fuera de su código, y siempre existe la posibilidad de que esté haciendo todo mal, lo que resulta en piezas de código estúpidas y redundantes. (Esto es inevitable a veces, pero un buen lenguaje debería permitirle eliminar la redundancia mientras se mantiene limpio).
Este es un gran ejemplo de divide y vencerás:
Digamos que tiene un intérprete de algún tipo.
switch(*IP) {
case OPCODE_ADD:
...
break;
case OPCODE_NOT_ZERO:
...
break;
case OPCODE_JUMP:
...
break;
default:
fixme(*IP);
}
En cambio, puedes usar esto:
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.
};
Tenga en cuenta que no sé cómo eliminar la redundancia de opcode_table en C. Tal vez debería hacer una pregunta al respecto. :)
Otro voto para if / else. No soy un gran admirador de las declaraciones de mayúsculas y minúsculas porque hay algunas personas que no las usan. El código es menos legible si usa mayúsculas o minúsculas. Tal vez no menos legible para ti, sino para aquellos que nunca han necesitado usar el comando.
Lo mismo ocurre con las fábricas de objetos.
Si / else bloques son una construcción simple que todos obtienen. Hay algunas cosas que puede hacer para asegurarse de que no causa problemas.
En primer lugar, no intente y sangría si declaraciones más de un par de veces. Si te estás encontrando sangrando, entonces lo estás haciendo mal.
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
Es realmente malo, haz esto en su lugar.
if a = 1 then
do something
endif
if a = 2 then
do something else
endif
if a = 3 then
do something more
endif
Optimización sea condenado. No hace una gran diferencia en la velocidad de tu código.
En segundo lugar, no soy reacio a salir de un bloque If siempre que haya suficientes declaraciones de interrupciones dispersas a través del bloque de código particular para que sea obvio
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
EDITAR : En Switch y por qué creo que es difícil asimilar:
Aquí hay un ejemplo de una declaración de cambio ...
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));
}
Para mí, el problema aquí es que las estructuras de control normales que se aplican en C se han roto completamente. Existe la regla general de que si desea colocar más de una línea de código dentro de una estructura de control, usa llaves o una instrucción de inicio / finalización.
p.ej
for i from 1 to 1000 {statement1; statement2}
if something=false then {statement1; statement2}
while isOKtoLoop {statement1; statement2}
Para mí (y puedes corregirme si estoy equivocado), la declaración de caso arroja esta regla fuera de la ventana. Un bloque de código ejecutado de forma condicional no se coloca dentro de una estructura de inicio / finalización. Debido a esto, creo que Case es conceptualmente lo suficientemente diferente como para no ser utilizado.
Espero que eso conteste tus preguntas.
Para C ++
Si se refiere a, por ejemplo, una AbstractFactory, creo que un método registerCreatorFunc (..) por lo general es mejor que requerir agregar un caso para cada declaración "nueva" que se necesita. Luego, dejar que todas las clases creen y registren una función creadora (..) que se puede implementar fácilmente con una macro (si me atrevo a mencionar). Creo que este es un enfoque común que muchos frameworks hacen. Lo vi por primera vez en ET ++ y creo que muchos marcos que requieren una macro DECL e IMPL lo usan.
Si el interruptor está allí para distinguir entre varios tipos de objetos, es probable que falten algunas clases para describir con precisión esos objetos, o algunos métodos virtuales ...
Todo el mundo ama ENORME if else
bloquea. ¡Tan fácil de leer! Sin embargo, tengo curiosidad sobre por qué querría eliminar las declaraciones de cambio. Si necesita una instrucción switch, probablemente necesite una instrucción switch. En serio, diría que depende de lo que esté haciendo el código. Si todo lo que hace el conmutador es llamar a funciones (por ejemplo), podría pasar los punteros de función. Si es una mejor solución es discutible.
El lenguaje también es un factor importante aquí, creo.
Un interruptor es un patrón, ya sea implementado con una instrucción switch, si no es cadena, tabla de búsqueda, polimorfismo oop, coincidencia de patrones u otra cosa.
¿Desea eliminar el uso de la " declaración de cambio " o el " patrón de cambio "? El primero se puede eliminar, el segundo, solo si se puede usar otro patrón / algoritmo, y la mayoría de las veces no es posible o no es un mejor enfoque para hacerlo.
Si desea eliminar el enunciado de conmutación del código, la primera pregunta es dónde tiene sentido eliminar la instrucción de conmutación y usar alguna otra técnica. Lamentablemente, la respuesta a esta pregunta es específica del dominio.
Y recuerde que los compiladores pueden hacer varias optimizaciones para cambiar las declaraciones. Entonces, por ejemplo, si desea procesar los mensajes de manera eficiente, una declaración de cambio es prácticamente el camino a seguir. Pero, por otro lado, la ejecución de reglas comerciales basadas en una declaración de cambio probablemente no sea la mejor manera de hacerlo y la aplicación debe ser re-estructurada.
Aquí hay algunas alternativas para cambiar la declaración:
- tabla de búsqueda
- polymorphism
- concordancia de patrones (especialmente utilizada en programación funcional, plantillas C ++)
Use un lenguaje que no viene con una declaración de interruptor incorporada. Perl 5 viene a la mente.
En serio, ¿por qué querrías evitarlo? Y si tiene buenas razones para evitarlo, ¿por qué no simplemente evitarlo?
Vea el olor de las declaraciones de cambio :
Típicamente, declaraciones de cambio similares están dispersas por un programa. Si agrega o elimina una cláusula en un solo interruptor, a menudo también debe encontrar y reparar los otros.
Tanto Refactoring como Refactoring to Patterns tienen enfoques para resolver esto.
Si su (pseudo) código se ve así:
class RequestHandler {
public void handleRequest(int action) {
switch(action) {
case LOGIN:
doLogin();
break;
case LOGOUT:
doLogout();
break;
case QUERY:
doQuery();
break;
}
}
}
Este código viola el Principio Abierto Cerrado y es frágil para cada nuevo tipo de código de acción que se presente. Para remediar esto, podría introducir un objeto ''Comando'':
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();
}
}
Si su (pseudo) código se ve así:
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");
}
}
Entonces podrías introducir un objeto ''Estado''.
// 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();
}
}
Espero que esto ayude.
if-else
Sin embargo, refuto la premisa de que el cambio es intrínsecamente malo.
switch
declaraciones sería bueno reemplazar si se encuentra agregando nuevos estados o un nuevo comportamiento a las declaraciones:
int state; String getString() { switch (state) { case 0 : // behaviour for state 0 return "zero"; case 1 : // behaviour for state 1 return "one"; } throw new IllegalStateException(); } double getDouble() { switch (this.state) { case 0 : // behaviour for state 0 return 0d; case 1 : // behaviour for state 1 return 1d; } throw new IllegalStateException(); }
Agregar un nuevo comportamiento requiere copiar el switch
, y agregar nuevos estados significa agregar otro case
a cada declaración de switch
.
En Java, solo puede cambiar un número muy limitado de tipos primitivos cuyos valores conoce en tiempo de ejecución. Esto presenta un problema en sí mismo: los estados se representan como números mágicos o caracteres.
Se pueden usar combinaciones de patrones y múltiples bloques if - else
, aunque realmente tienen los mismos problemas al agregar comportamientos nuevos y nuevos estados.
La solución que otros han sugerido como "polimorfismo" es una instancia del patrón de Estado :
Reemplace cada uno de los estados con su propia clase. Cada comportamiento tiene su propio método en la clase:
IState state; String getString() { return state.getString(); } double getDouble() { return state.getDouble(); }
Cada vez que agrega un nuevo estado, debe agregar una nueva implementación de la interfaz de IState
. En un mundo de switch
, estarías agregando un case
a cada switch
.
Cada vez que agrega un nuevo comportamiento, necesita agregar un nuevo método a la interfaz de IState
y a cada una de las implementaciones. Esta es la misma carga que antes, aunque ahora el compilador comprobará que tiene implementaciones del nuevo comportamiento en cada estado preexistente.
Otros ya lo han dicho, que esto puede ser demasiado pesado, por lo que, por supuesto, hay un punto al que llegar donde se mueve de uno a otro. Personalmente, la segunda vez que escribo un cambio es el punto en el que refactorizo.