Refactorización de declaraciones de cambio y ¿hay algún uso real para las declaraciones de cambio?

28

Estaba leyendo este artículo y me preguntaba: ¿nos deshacemos de todas las declaraciones de cambio al reemplazarlas con un Diccionario o una Fábrica para que no haya ninguna declaración de cambio en mis proyectos?

Algo no cuadró del todo.

La pregunta es, ¿las declaraciones de cambio tienen un uso real o seguimos adelante y las reemplazamos con un diccionario o un método de fábrica? usando la fábrica ... pero eso es todo).

Kanini
fuente
10
Suponiendo que implemente una fábrica, ¿cómo decidirá qué tipo de objeto crear?
CodeART
@CodeWorks: Por supuesto, tendré condiciones en algún lugar, decidiendo qué implementación concreta usar.
Kanini
@CodeWorks: con un constructor virtual, obviamente. (Y si no puede implementar un patrón de fábrica de esa manera, necesita un mejor lenguaje).
Mason Wheeler

Respuestas:

44

Tanto las switchdeclaraciones como el polimorfismo tienen su uso. Sin embargo, tenga en cuenta que también existe una tercera opción (en idiomas que admiten punteros / lambdas de funciones y funciones de orden superior): asignar los identificadores en cuestión a las funciones de controlador. Está disponible en, por ejemplo, C, que no es un lenguaje OO, y C #, que es *, pero no (todavía) en Java, que también es OO *.

En algunas lenguas de procedimiento (que no tienen funciones polimorfismo ni de orden superior) switch/ if-elsedeclaraciones eran la única manera de resolver una clase de problemas. Muchos desarrolladores, habiéndose acostumbrado a esta forma de pensar, continuaron usando switchincluso en lenguajes OO, donde el polimorfismo es a menudo una mejor solución. Es por eso que a menudo se recomienda evitar / refactorizar las switchdeclaraciones a favor del polimorfismo.

En cualquier caso, la mejor solución siempre depende del caso. La pregunta es: ¿ qué opción le ofrece un código más limpio, más conciso y más sostenible a largo plazo?

Las declaraciones de cambio a menudo pueden volverse difíciles de manejar, teniendo docenas de casos, lo que dificulta su mantenimiento. Como debe mantenerlos en una sola función, esa función puede crecer enormemente. Si este es el caso, debe considerar la refactorización hacia una solución basada en mapas y / o polimórficos.

Si lo mismo switchcomienza a aparecer en varios lugares, el polimorfismo es probablemente la mejor opción para unificar todos estos casos y simplificar el código. Especialmente si se espera agregar más casos en el futuro; Cuantos más lugares necesite actualizar cada vez, mayores serán las posibilidades de errores. Sin embargo, a menudo los manejadores de casos individuales son tan simples, o hay tantos de ellos, o están tan interrelacionados, que refactorizarlos en una jerarquía de clase polimórfica completa es excesivo, o resulta en una gran cantidad de código duplicado y / o enredado, difícil mantener la jerarquía de clases. En este caso, puede ser más simple usar funciones / lambdas en su lugar (si su idioma lo permite).

Sin embargo, si tiene una switchen un solo lugar, con solo unos pocos casos haciendo algo simple, puede ser la mejor solución para dejarlo como está.

* Uso el término "OO" libremente aquí; No me interesan los debates conceptuales sobre lo que es "real" o "puro" OO.

Péter Török
fuente
44
+1. Además, esas recomendaciones tienden a estar redactadas, " prefieren cambiar el polimorfismo" y esa es una buena elección de palabras, muy acorde con esta respuesta. Reconoce que hay circunstancias en las que un codificador responsable podría tomar la otra opción.
Carl Manaster
1
Y hay momentos en los que desea el enfoque de cambio, incluso si hay una gran cantidad de ellos en su código. Tengo en mente un código que genera un laberinto 3D. El uso de la memoria aumentaría si las celdas de un byte en la matriz fueran reemplazadas por clases.
Loren Pechtel
14

Aquí es donde vuelvo a ser un dinosaurio ...

Las declaraciones de cambio no son malas en sí mismas, es el uso que se hace de ellas lo que está en cuestión.

La más obvia es la "misma" declaración de cambio que se repite una y otra vez a través de su código que es malo (estado allí, hecho eso, haría todo lo posible para no volver a hacerlo), y es este último caso el que puede tratar. con el uso de polimorfismo. Por lo general, también hay algo bastante horrible en los casos anidados (solía tener un monstruo absoluto; no estoy del todo seguro de cómo tratarlo ahora que no sea "mejor").

El diccionario como interruptor me parece más desafiante, fundamentalmente sí, si su interruptor cubre el 100% de los casos, pero cuando desea tener casos predeterminados o sin acción, comienza a ser un poco más interesante.

Creo que es una cuestión de evitar la repetición y de asegurarnos de que estamos componiendo nuestros gráficos de objetos en los lugares correctos.

Pero también está el argumento de la comprensión (mantenibilidad) y esto corta en ambos sentidos: una vez que comprende cómo funciona todo (el patrón y la aplicación en la que se implementó) es fácil ... pero si llega a una sola línea de código donde tienes que agregar algo nuevo y luego tienes que saltar por todo el lugar para resolver lo que necesitas agregar / cambiar.

A pesar de que nuestros entornos de desarrollo son enormemente capaces, sigo sintiendo que es deseable poder comprender el código (como si fuera) impreso en papel. ¿Puede seguir el código con el dedo? Acepto que en realidad no, con muchas buenas prácticas hoy en día no se puede y por buenas razones, pero eso significa que ponerse al día con el código es más difícil de comenzar (o tal vez solo soy viejo ...)

Murph
fuente
44
Tuve un maestro que dijo que idealmente el código debería estar escrito "para que alguien que no supiera nada sobre programación (como quizás tu madre) pudiera entenderlo". Desafortunadamente, es un ideal, ¡pero tal vez deberíamos incluir a nuestras mamás en las revisiones de códigos de todos modos!
Michael K
+1 para "pero si llegas a una sola línea de código donde tienes que agregar algo nuevo, entonces tienes que saltar por todos lados para resolver lo que necesitas agregar / cambiar"
rápidamente_ahora
8

Las declaraciones de cambio frente al polimorfismo de subtipo es un problema antiguo y a menudo se hace referencia en la comunidad de FP en los debates sobre el problema de expresión .

Básicamente, tenemos tipos (clases) y funciones (métodos). ¿Cómo codificamos cosas para que sea fácil agregar nuevos tipos o nuevos métodos más adelante?

Si programa en estilo OO, es difícil agregar un nuevo método (ya que eso significaría refactorizar todas las clases existentes), pero es muy fácil agregar nuevas clases que usan los mismos métodos que antes.

Por otro lado, si usa una declaración de cambio (o su equivalente OO, el Patrón de Observador), entonces es muy fácil agregar nuevas funciones, pero es difícil agregar nuevos casos / clases.

No es fácil tener una buena extensibilidad en ambas direcciones, por lo tanto, al escribir su código, determine si usará polimorfismo o cambiará las declaraciones dependiendo de en qué dirección es más probable que se extienda más adelante.

hugomg
fuente
4
¿Nos deshacemos de todas las declaraciones de cambio al reemplazarlas con un Diccionario o una Fábrica para que no haya ninguna declaración de cambio en mis proyectos?

No. Tales absolutos rara vez son una buena idea.

En muchos lugares, un envío de diccionario / búsqueda / fábrica / polimórfico proporcionará un mejor diseño que una declaración de cambio, pero aún debe completar el diccionario. En ciertos casos, eso oscurecerá lo que realmente está sucediendo y simplemente tener la instrucción switch en línea es más fácil de leer y mantener.

Telastyn
fuente
Y realmente, si desenrolla la fábrica y el diccionario y la búsqueda, resultaría ser básicamente una declaración de cambio: si array [hash (búsqueda)], entonces llame a array [hash (búsqueda)]. Aunque sería extensible en tiempo de ejecución qué conmutadores compilados no lo son.
Zan Lynx
@ZanLynx, todo lo contrario es cierto, al menos con C #. Si observa el IL producido para una declaración de cambio no trivial, verá que el compilador lo convierte en un diccionario, ya que es más rápido.
David Arno
@DavidArno: "todo lo contrario"? Según lo leí, dijimos exactamente lo mismo. ¿Cómo puede ser opuesto?
Zan Lynx
2

Al ver cómo esto es independiente del lenguaje, el código de caída funciona mejor con las switchdeclaraciones:

switch(something) {
   case 1:
      foo();
   case 2:
      bar();
      baz();
      break;

   case 3:
      bang();
   default:
      bizzap();
      break;
}

Equivalente a if, con un caso predeterminado muy incómodo. Y tenga en cuenta que cuanto más fallas haya, más larga será la lista de condiciones:

if (1 == something) {
   foo();
}
if (1 == something || 2 == something) {
   bar();
   baz();
}
if (3 == something) {
   bang();
}
if (1 != something && 2 != something) {
   bizzap();
}

(Sin embargo, dado lo que dicen algunas de las otras respuestas, siento que he perdido el punto de la pregunta ...)

Izkata
fuente
Consideraría que el uso de declaraciones fallidas en a switchestá en el mismo nivel que a goto. Si el algoritmo que se va a realizar requiere flujos de control que se ajusten a las construcciones de programación estructuradas, uno debería usar tales construcciones, pero si el algoritmo no se ajusta a tales construcciones, usarlas gotopuede ser mejor que intentar agregar indicadores u otra lógica de control para adaptarse a otras estructuras de control .
supercat
1

Las declaraciones de cambio como diferenciación de casos tienen un uso real. Los lenguajes de programación funcional usan algo llamado coincidencia de patrones que le permite definir funciones de manera diferente según la entrada. Sin embargo, en los lenguajes orientados a objetos, se puede alcanzar el mismo objetivo de manera más elegante utilizando el polimorfismo. Simplemente llame al método y, según el tipo real del objeto, se ejecutará la implementación correspondiente del método. Esta es la razón detrás de la idea, que las declaraciones de cambio son un olor a código. Sin embargo, incluso en lenguajes OO, puede encontrarlos útiles para implementar el patrón abstracto de fábrica.

tl; dr: son útiles, pero a menudo puedes hacerlo mejor.

Scarfridge
fuente
2
Huelo un sesgo allí, ¿por qué el polimorfismo es más elegante que la coincidencia de patrones? ¿Y qué te hace pensar que el polimorfismo no existe en la PF?
tdammers
@tdammers En primer lugar, no quise decir que el polimorfismo no existe en FP. Haskell admite polimorfismo ad hoc y paramétrico. La coincidencia de patrones tiene sentido para un tipo de datos algebraico. Pero para módulos externos esto requiere exponer a los constructores. Claramente, esto rompe la encapsulación de un tipo de datos abstractos (realizado con tipos de datos algebraicos). Es por eso que siento que el polimorfismo es más elegante (y posible en FP).
scarfridge
Estás olvidando el polimorfismo a través de clases de tipos e instancias.
tdammers
¿Alguna vez has oído hablar del problema de expresión ? El OO y el FP son mejores en diferentes situaciones, si te detienes a pensarlo.
hugomg
@tdammers ¿De qué tipo de polimorfismo crees que estaba hablando cuando mencioné el polimorfismo ad hoc? El polimorfismo ad hoc se realiza en Haskell a través de clases de tipos. ¿Cómo puedes decir que lo olvidé? Simplemente no es verdad.
scarfridge