¿Cómo refactorizar una aplicación con múltiples casos de cambio?

10

Tengo una aplicación que toma un número entero como entrada y se basa en las llamadas de entrada métodos estáticos de diferentes clases. Cada vez que se agrega un nuevo número, necesitamos agregar otro caso y llamar a un método estático diferente de una clase diferente. Ahora hay 50 casos en el conmutador y cada vez que necesito agregar otro caso, me estremezco. Hay una mejor manera de hacer esto.

Pensé un poco y se me ocurrió esta idea. Yo uso el patrón de estrategia. En lugar de tener un caso de cambio, tengo un mapa de objetos de estrategia con la clave como el entero de entrada. Una vez que se invoca el método, buscará el objeto y llamará al método genérico para el objeto. De esta manera puedo evitar usar la construcción de la caja del interruptor.

¿Qué piensas?

Kaushik Chakraborty
fuente
2
¿Cuál es el problema real con el código actual?
Philip Kendall el
¿Qué sucede cuando tienes que hacer uno de estos cambios? ¿Tiene que agregar un switchcaso y llamar a un método preexistente en su sistema complejo, o tiene que inventar tanto el método como su llamada?
Kilian Foth el
@KilianFoth He heredado este proyecto como desarrollador de mantenimiento y aún no he tenido que hacer ningún cambio. Sin embargo, haré cambios pronto, así que quiero refactorizar ahora. Pero para responder a su pregunta, sí a lo último.
Kaushik Chakraborty
2
Creo que necesitas mostrar un ejemplo condensado de lo que está sucediendo.
whatsisname
1
@KaushikChakraborty: luego invente un ejemplo de memoria. Hay situaciones en las que un interruptor súper de más de 250 casos es apropiado, y hay casos en los que el cambio es malo, independientemente de los pocos casos que tenga. El diablo está en los detalles y no tenemos detalles.
whatsisname

Respuestas:

13

Ahora hay 50 casos en el conmutador y cada vez que necesito agregar otro caso, me estremezco.

Amo el polimorfismo. Me encanta SOLID. Me encanta la programación orientada a objetos puros. Odio ver que estos tengan una mala reputación porque se aplican dogmáticamente.

No ha presentado un buen caso para refactorizar la estrategia. La refactorización tiene un nombre por cierto. Se llama Reemplazar condicional con polimorfismo .

He encontrado algunos consejos pertinentes para usted de c2.com :

Realmente solo tiene sentido si las pruebas condicionales iguales o muy similares se repiten con frecuencia. Para pruebas simples, raramente repetidas, reemplazar un condicional simple con la verbosidad de múltiples definiciones de clase, y probablemente alejarse todo del código que realmente requiere la actividad condicionalmente requerida, daría como resultado un libro de texto de ofuscación de código. Prefiere la claridad sobre la pureza dogmática. - DanMuller

Tiene un interruptor con 50 cajas y su alternativa es producir 50 objetos. Ah y 50 líneas de código de construcción de objetos. Esto no es progreso. Por qué no? Debido a que esta refactorización no hace nada para reducir el número de 50. Usted usa esta refactorización cuando encuentra que necesita crear otra instrucción de cambio en la misma entrada en otro lugar. Ahí es cuando esta refactorización ayuda porque convierte 100 nuevamente en 50.

Mientras te refieras a "el interruptor" como si fuera el único que tienes, no lo recomiendo. La única ventaja de refactorizar ahora es que reduce las posibilidades de que algún bobo copie y pegue su interruptor de 50 cajas.

Lo que sí recomiendo es mirar de cerca estos 50 casos en busca de puntos en común que se puedan tener en cuenta. Me refiero a 50? De Verdad? ¿Seguro que necesitas tantos casos? Puede que estés tratando de hacer mucho aquí.

naranja confitada
fuente
Estoy de acuerdo con lo que está diciendo. El código tiene muchas redundancias, podría ser que muchos de los casos ni siquiera sean necesarios, pero desde un punto de vista superficial no lo parece. Cada uno de los casos llama a un método que llama a múltiples sistemas y agrega los resultados y regresa al código de llamada. Cada clase es autónoma, realiza un trabajo y me temo que violaré el principio de alta cohesión si redujera el número de casos.
Kaushik Chakraborty
2
Puedo obtener 50 sin violar la alta cohesión y mantener las cosas autónomas. Simplemente no puedo hacerlo con un número. Necesitaría un 2, un 5 y otro 5. Por eso se llama factorización. En serio, mire toda su arquitectura y vea si no puede encontrar una salida a este infierno de 50 casos en el que se encuentra. Refactorizar consiste en deshacer las malas decisiones. No perpetuarlos en nuevas formas.
candied_orange
Ahora, si puede ver una manera de reducir los 50 utilizando esta refactorización, hágalo. Para aprovechar la idea de Doc Browns: Un mapa de mapas puede tomar dos claves. Algo sobre lo que pensar.
candied_orange
1
Estoy de acuerdo con el comentario de Candied. El problema no son los 50 casos en la declaración de cambio, el problema es el diseño arquitectónico de nivel superior que está causando que llame a una función que necesita decidir entre 50 opciones. He diseñado algunos sistemas muy grandes y complejos y nunca me he visto forzado a una situación como esa.
Dunk
@Candied "Usas esta refactorización cuando encuentras que necesitas crear otra declaración de cambio en la misma entrada en otro lugar". ¿Puedes elaborar esto? Como tengo un caso similar donde tengo casos de cambio pero en diferentes capas como tenemos en nuestro proyecto primero autorización, validación, procedimientos CRUD luego dao. Entonces, en cada capa hay casos de cambio en la misma entrada, es decir, un número entero, pero que realiza diferentes funciones como auth, válido. Entonces, ¿deberíamos crear una clase para cada tipo que tenga métodos diferentes? ¿Encaja nuestro caso con lo que intenta decir "repitiendo el mismo interruptor en la misma entrada"?
Siddharth Trikha
9

Un mapa de objetos de estrategia solo, que se inicializa en alguna función de su código, donde tiene varias líneas de código parecidas

     myMap.Add(1,new Strategy1());
     myMap.Add(2,new Strategy2());
     myMap.Add(3,new Strategy3());

requiere que usted y sus colegas implementen las funciones / estrategias que se llamarán en clases separadas, de una manera más uniforme (ya que sus objetos de estrategia tendrán que implementar la misma interfaz). Tal código es a menudo un poco más completo que

     case 1:
          MyClass1.Doit1(someParameters);
          break;
     case 2:
          MyClass2.Doit2(someParameters);
          break;
     case 3:
          MyClass3.Doit3(someParameters);
          break;

Sin embargo, aún no lo liberará de la carga de editar este archivo de código cada vez que necesite agregar un nuevo número. Los beneficios reales de este enfoque son diferentes:

  • la inicialización del mapa ahora se separa del código de despacho que realmente llama a la función asociada a un número específico, y este último ya no contiene esas 50 repeticiones, simplemente se verá myMap[number].DoIt(someParameters). Por lo tanto, no es necesario tocar este código de envío cada vez que llega un nuevo número y se puede implementar de acuerdo con el principio de Abierto-Cerrado. Además, cuando obtiene requisitos en los que necesita extender el código de envío en sí, ya no tendrá que cambiar 50 lugares, sino solo uno.

  • el contenido del mapa se determina en tiempo de ejecución (mientras que el contenido de la construcción del conmutador se determina antes del tiempo de compilación), por lo que le brinda la oportunidad de hacer que la lógica de inicialización sea más flexible o ampliable.

Entonces, sí, hay algunos beneficios, y este es seguramente un paso hacia un código más SÓLIDO. Sin embargo, si vale la pena refactorizar, es algo que usted o su equipo tendrán que decidir por sí mismos. Si no espera que se cambie el código de envío, se cambie la lógica de inicialización y la legibilidad del mismo switchno sea un problema real, entonces su refactorización podría no ser tan importante ahora.

Doc Brown
fuente
Si bien soy reacio a reemplazar ciegamente cada interruptor con polimorfismo, diré que usar un mapa de la manera que Doc Brown sugiere aquí me ha funcionado muy bien en el pasado. Cuando se implementa la misma interfaz reemplace Doit1, Doit2, etc, con un Doitmétodo que tiene muchas implementaciones diferentes.
candied_orange
Y si tiene control sobre el tipo de símbolo de entrada utilizado como clave, puede ir un paso más allá haciendo doTheThing()un método del símbolo de entrada. Entonces no necesitas mantener el mapa.
Kevin Krumwiede
1
@KevinKrumwiede: lo que sugiere significa simplemente pasar los objetos de estrategia por sí mismos en el programa, como reemplazo de los enteros. Sin embargo, cuando el programa toma un número entero como entrada de alguna fuente de datos externa, debe haber un mapeo del número entero a la estrategia relacionada al menos en un lugar del sistema.
Doc Brown
Ampliando la sugerencia de Doc Brown: también podría crear una fábrica que contendría la lógica para crear los objetos de estrategia, si decide seguir este camino. Dicho esto, la respuesta proporcionada por CandiedOrange tiene más sentido para mí.
Vladimir Stokic
@DocBrown A eso me refería con "si tienes control sobre el tipo de símbolo de entrada".
Kevin Krumwiede
0

Estoy totalmente a favor de la estrategia descrita en la respuesta de @DocBrown .

Voy a sugerir una mejora a la respuesta.

Las llamadas

 myMap.Add(1,new Strategy1());
 myMap.Add(2,new Strategy2());
 myMap.Add(3,new Strategy3());

Se puede distribuir. No tiene que volver al mismo archivo para agregar otra estrategia, que se adhiere al principio de Open-Closed aún mejor.

Digamos que implementa Strategy1en el archivo Strategy1.cpp. Puede tener el siguiente bloque de código en él.

namespace Strategy1_Impl
{
   struct Initializer
   {
      Initializer()
      {
         getMap().Add(1, new Strategy1());
      }
   };
}
using namespace Strategy1_Impl;

static Initializer initializer;

Puede repetir el mismo código en cada archivo StategyN.cpp. Como puede ver, será mucho código repetido. Para reducir la duplicación de código, puede usar una plantilla que se puede colocar en un archivo que sea accesible para todas las Strategyclases.

namespace StrategyHelper
{
   template <int N, typename StrategyType> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new StrategyType());
      }
   };
}

Después de eso, lo único que debe usar en Strategy1.cpp es:

static StrategyHelper::Initializer<1, Strategy1> initializer;

La línea correspondiente en StrategyN.cpp es:

static StrategyHelper::Initializer<N, StrategyN> initializer;

Puede llevar el uso de plantillas a otro nivel utilizando una plantilla de clase para las clases de estrategia concretas.

class Strategy { ... };

template <int N> class ConcreteStrategy;

Y luego, en lugar de Strategy1usar ConcreteStrategy<1>.

template <> class ConcreteStrategy<1> : public Strategy { ... };

Cambie la clase auxiliar para registrar Strategys en:

namespace StrategyHelper
{
   template <int N> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new ConcreteStrategy<N>());
      }
   };
}

Cambie el código en Strateg1.cpp a:

static StrategyHelper::Initializer<1> initializer;

Cambie el código en StrategN.cpp a:

static StrategyHelper::Initializer<N> initializer;
R Sahu
fuente