language-agnostic design-patterns

language agnostic - ¿Cuál es la mejor manera de reemplazar o sustituir if..else if..else trees en los programas?



language-agnostic design-patterns (21)

¡Usa un operador ternario!

Operador ternario (53 caracteres):

i===1?doOne():i===2?doTwo():i===3?doThree():doNone();

Si (108 caracteres):

if (i === 1) { doOne(); } else if (i === 2) { doTwo(); } else if (i === 3) { doThree(); } else { doNone(); }

Interruptor ((¡¡MÁS DE LARGO QUE SI!?!?) 114Caracteres):

switch (i) { case 1: doOne(); break; case 2: doTwo(); break; case 3: doThree(); break; default: doNone(); break; }

¡Esto es todo lo que necesita! ¡es solo una línea y es bastante ordenada, mucho más corta que el interruptor y si!

Esta pregunta está motivada por algo que últimamente he comenzado a ver demasiado a menudo, la estructura if..else if..else . Si bien es simple y tiene sus usos, algo sobre eso me sigue diciendo una y otra vez que podría ser sustituido por algo que sea más refinado, elegante y, en general, más fácil de mantener actualizado.

Para ser lo más específico posible, esto es lo que quiero decir:

if (i == 1) { doOne(); } else if (i == 2) { doTwo(); } else if (i == 3) { doThree(); } else { doNone(); }

Puedo pensar en dos formas simples de reescribir eso, ya sea por ternario (que es solo otra manera de escribir la misma estructura):

(i == 1) ? doOne() : (i == 2) ? doTwo() : (i == 3) ? doThree() : doNone();

o usando Map (en Java y creo que también en C #) o Dictionary o cualquier otra estructura K / V como esta:

public interface IFunctor() { void call(); } public class OneFunctor implemets IFunctor() { void call() { ref.doOne(); } } /* etc. */ Map<Integer, IFunctor> methods = new HashMap<Integer, IFunctor>(); methods.put(1, new OneFunctor()); methods.put(2, new TwoFunctor()); methods.put(3, new ThreeFunctor()); /* .. */ (methods.get(i) != null) ? methods.get(i).call() : doNone();

De hecho, el método Map anterior es lo que terminé haciendo la última vez, pero ahora no puedo dejar de pensar que tiene que haber mejores alternativas en general para este problema exacto.

Entonces, ¿qué otras -y más probablemente mejores- maneras de reemplazar el if..else if ... else están disponibles y cuál es tu favorito?

¡Tus pensamientos debajo de esta línea!

De acuerdo, aquí están tus pensamientos:

Primero, la respuesta más popular fue la declaración de cambio, así:

switch (i) { case 1: doOne(); break; case 2: doTwo(); break; case 3: doThree(); break; default: doNone(); break; }

Eso solo funciona para valores que pueden usarse en switches, que al menos en Java es un factor bastante limitante. Aceptable para casos simples, sin embargo, naturalmente.

La otra y tal vez un poco más elegante que pareces sugerir es hacerlo usando polimorfismo. La conferencia de Youtube enlazada por CMS es un reloj excelente, ve a verla aquí: "El código limpio habla: herencia, polimorfismo y pruebas". Hasta donde yo entendía, esto se traduciría en algo como esto:

public interface Doer { void do(); } public class OneDoer implements Doer { public void do() { doOne(); } } /* etc. */ /* some method of dependency injection like Factory: */ public class DoerFactory() { public static Doer getDoer(int i) { switch (i) { case 1: return new OneDoer(); case 2: return new TwoDoer(); case 3: return new ThreeDoer(); default: return new NoneDoer(); } } } /* in actual code */ Doer operation = DoerFactory.getDoer(i); operation.do();

Dos puntos interesantes de la charla de Google:

  • Use objetos nulos en lugar de devolver nulos (y por favor, solo ejecute excepciones de tiempo de ejecución)
  • Intenta escribir un pequeño proyecto sin si: s.

Además, una publicación que vale la pena mencionar es, en mi opinión, CDR, que nos proporcionó sus hábitos perversos y aunque no se recomienda su uso, es muy interesante de ver.

Gracias a todos por las respuestas (hasta ahora), ¡creo que podría haber aprendido algo hoy!


¡Utilizo la siguiente mano corta solo por diversión! No intente ninguno de estos si la claridad del código le preocupa más que la cantidad de caracteres escritos.

Para casos donde doX () siempre devuelve verdadero.

i==1 && doOne() || i==2 && doTwo() || i==3 && doThree()

Por supuesto, trato de asegurar que la mayoría de las funciones vacías devuelvan 1 simplemente para asegurar que estas manos cortas sean posibles.

También puede proporcionar asignaciones.

i==1 && (ret=1) || i==2 && (ret=2) || i==3 && (ret=3)

Como instad de escritura

if(a==2 && b==3 && c==4){ doSomething(); else{ doOtherThings(); }

Escribir

a==2 && b==3 && c==4 && doSomething() || doOtherThings();

Y en los casos donde no esté seguro de qué devolverá la función, agregue un || 1 :-)

a==2 && b==3 && c==4 && (doSomething()||1) || doOtherThings();

Todavía me resulta más rápido escribir que usar todos esos if-else y seguro que asusta a todos los noobs nuevos. Imagine una página completa de enunciado como este con 5 niveles de sangrado.

"if" es raro en algunos de mis códigos y le he dado el nombre "if-less programming" :-)


Considero estas construcciones if-elseif -... como "ruido de palabras clave". Si bien puede estar claro lo que hace, le falta concisión; Considero que la concisión es una parte importante de la legibilidad. La mayoría de los idiomas proporcionan algo así como una declaración de switch . Construir un mapa es una forma de obtener algo similar en los idiomas que no lo tienen, pero ciertamente se siente como una solución, y hay un poco de sobrecarga (una instrucción switch se traduce en algunas operaciones simples de comparación y saltos condicionales, pero un mapa primero se construye en la memoria, luego se consulta y solo entonces tiene lugar la comparación y el salto).

En Common Lisp, hay dos construcciones de switch incorporadas, cond y case . cond permite condicionales arbitrarios, mientras que el case solo prueba la igualdad, pero es más conciso.

(cond ((= i 1) (do-one)) ((= i 2) (do-two)) ((= i 3) (do-three)) (t (do-none)))

(case i (1 (do-one)) (2 (do-two)) (3 (do-three)) (otherwise (do-none)))

Por supuesto, puede crear su propia macro tipo case para sus necesidades.

En Perl, puede usar la instrucción for , opcionalmente con una etiqueta arbitraria (aquí: SWITCH ):

SWITCH: for ($i) { /1/ && do { do_one; last SWITCH; }; /2/ && do { do_two; last SWITCH; }; /3/ && do { do_three; last SWITCH; }; do_none; };


Dependiendo del tipo de cosa que tenga si ... sigue, considere crear una jerarquía de objetos y usar polimorfismo. Al igual que:

class iBase { virtual void Foo() = 0; }; class SpecialCase1 : public iBase { void Foo () {do your magic here} }; class SpecialCase2 : public iBase { void Foo () {do other magic here} };

Luego, en su código solo llame a p-> Foo () y ocurrirá lo correcto.


El ejemplo dado en la pregunta es lo suficientemente trivial para funcionar con un simple cambio. El problema surge cuando los if-elses se anidan más y más. Ya no son "claros o fáciles de leer" (como argumentó otra persona) y agregar código nuevo o corregir errores en ellos se vuelve cada vez más difícil y más difícil de asegurar porque es posible que no termine donde esperaba si la lógica es complejo.

He visto esto suceder muchas veces (switches anidados a 4 niveles de profundidad y cientos de líneas de largo - imposible de mantener), especialmente dentro de las clases de fábrica que están tratando de hacer demasiado para demasiados tipos diferentes no relacionados.

Si los valores con los que compara no son enteros sin sentido, sino algún tipo de identificador único (es decir, que usa enumeraciones como polimorfismo de un hombre pobre), entonces quiere usar clases para resolver el problema. Si realmente son solo valores numéricos, entonces preferiría usar funciones separadas para reemplazar los contenidos de los bloques if y else, y no diseñar algún tipo de jerarquía artificial de clases para representarlos. Al final, esto puede dar como resultado un código más desordenado que los espaguetis originales.


El método Map es el mejor que hay. Te permite encapsular las declaraciones y divide las cosas bastante bien. El polimorfismo puede complementarlo, pero sus objetivos son ligeramente diferentes. También introduce árboles de clase innecesarios.

Los conmutadores tienen la desventaja de que faltan declaraciones de fallas y fracasan, y realmente alientan a no dividir el problema en partes más pequeñas.

Dicho esto: un pequeño árbol de if..else''s está bien (de hecho, argumenté a favor durante días acerca de tener 3 if..elses en lugar de utilizar Map recientemente). Es cuando comienzas a poner una lógica más complicada en ellos que se convierte en un problema debido a la facilidad de mantenimiento y legibilidad.


En Python, escribiría tu código como:

actions = { 1: doOne, 2: doTwo, 3: doThree, } actions[i]()


En el paradigma OO, podrías hacerlo usando un buen polimorfismo antiguo. Demasiado grande si las estructuras de otro tipo o las construcciones de los conmutadores a veces se consideran un olor en el código.


En este caso simple, podrías usar un interruptor.

De lo contrario, un enfoque basado en tablas se ve bien, sería mi segunda opción siempre que las condiciones sean lo suficientemente regulares como para que sea aplicable, especialmente cuando el número de casos es grande.

El polimorfismo sería una opción si no hay demasiados casos, y las condiciones y el comportamiento son irregulares.


En lenguajes orientados a objetos, es común usar polimorfismo para reemplazar el if.

Me gustó este Google Clean Code Talk que cubre el tema:

The Clean Code Talks - Herencia, Polimorfismo y Pruebas

ABSTRACTO

¿Está tu código lleno de declaraciones if? Cambiar declaraciones? ¿Tiene la misma declaración de cambio en varios lugares? Cuando haces cambios, ¿te encuentras haciendo el mismo cambio en el mismo si / cambias en varios lugares? ¿Olvidaste alguna vez?

Esta charla discutirá enfoques para usar técnicas orientadas a objetos para eliminar muchos de esos condicionales. El resultado es un código más limpio, más ajustado y mejor diseñado que es más fácil de probar, comprender y mantener.


Estos constructos a menudo pueden ser reemplazados por polimorfismo. Esto le dará un código más corto y menos frágil.


Fuera de usar una declaración de cambio, que puede ser más rápida, ninguna. Si Else es claro y fácil de leer. tener que buscar cosas en un mapa ofusca las cosas. ¿Por qué hacer que el código sea más difícil de leer?


Hay dos partes en esa pregunta.

¿Cómo despachar basado en un valor? Use una declaración de cambio. Muestra tu intención más claramente.

¿Cuándo despachar basado en un valor? Solo en un lugar por valor: cree un objeto polimórfico que sepa cómo proporcionar el comportamiento esperado para el valor.


Iría tan lejos como para decir que ningún programa debería usar nunca más. Si lo hace, está pidiendo problemas. Nunca debe suponer que, si no es una X, debe ser una Y. Sus pruebas deben probar cada una de ellas individualmente y fallar después de dichas pruebas.


La declaración de switch , por supuesto, mucho más bonita que todos esos si y lo demás.


Naturalmente, esta pregunta depende del idioma, pero una declaración de cambio podría ser una mejor opción en muchos casos. Un buen compilador C o C ++ podrá generar una tabla de salto , que será significativamente más rápida para grandes conjuntos de casos.


Si realmente debe tener un montón de pruebas if y desea hacer cosas diferentes cuando una prueba es verdadera, recomendaría un ciclo while con only ifs-no else. Cada uno si hace una prueba y llama a un método y luego sale del ciclo. No hay nada peor que un montón de if / else / if / else apilados, etc.


Una declaración de cambio:

switch(i) { case 1: doOne(); break; case 2: doTwo(); break; case 3: doThree(); break; default: doNone(); break; }


Use un interruptor / caja, es más limpio: p


switch declaración o clases con funciones virtuales como una solución elegante. O un conjunto de punteros a funciones. Todo depende de cuán complejas sean las condiciones, a veces no hay forma de evitarlas. Y nuevamente, la creación de series de clases para evitar una declaración de cambio es claramente incorrecta, el código debe ser lo más simple posible (pero no más simple)


switch (i) { case 1: doOne(); break; case 2: doTwo(); break; case 3: doThree(); break; default: doNone(); break; }

Después de haber tipeado esto, debo decir que no hay mucho problema con su declaración if. Como Einstein dijo: "Hazlo lo más simple posible, pero no más simple".