¿Cómo refactorizar una aplicación con varios casos de cambio?

7

Tengo una aplicación que toma un 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, debemos agregar otro caso y llamar a un método estático diferente de una clase diferente. Ahora hay 50 casos en el interruptor 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 el uso de la construcción de caso de cambio.

¿Qué piensas?

    
pregunta Kaushik Chakraborty 23.04.2017 - 07:30

3 respuestas

10
  

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

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

No has sido un buen caso para refactorizar la estrategia. La refactorización tiene un nombre por cierto. Se llama Replace Conditional con Polymorphism .

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

  

Realmente solo tiene sentido si se repiten con frecuencia pruebas condicionales iguales o muy similares. Para pruebas simples, que se repiten pocas veces, 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 condicional requerida, daría lugar a un ejemplo de libro de texto de ofuscación de código.   Prefiere la claridad sobre la pureza dogmática. - DanMuller

Tienes un interruptor con 50 cajas y tu 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. Utilice esta refactorización cuando descubra que necesita crear otra instrucción de conmutación en la misma entrada en otra parte. Ahí es cuando esta refactorización ayuda porque convierte 100 en 50.

Siempre que te refieras a "el interruptor" como si fuera el único que tienes, no te lo recomiendo. La única ventaja que viene de la refactorización ahora es que reduce las posibilidades de que algunos tontos copien y peguen el cambio de 50 casos.

Lo que sí recomiendo es observar detenidamente estos 50 casos en busca de puntos en común que puedan ser eliminados. Me refiero a 50? De Verdad? ¿Seguro que necesitas tantos casos? Puede que estés tratando de hacer mucho aquí.

    
respondido por el candied_orange 23.04.2017 - 09:28
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 que se ven como

     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 deben llamarse en clases separadas, de una manera más uniforme (ya que todos los objetos de estrategia deberán 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, todavía no lo liberará de la carga de editar este archivo de código siempre que sea necesario 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 envío que en realidad llama la función asociada a un número específico, y esta última no contiene esas 50 repeticiones más, solo se verá como 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 los requisitos en los que necesita extender el código de envío, 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 switch se determina antes del tiempo de compilación), por lo que le da la oportunidad de hacer que la lógica de inicialización sea más flexible o ampliable. / p>

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

    
respondido por el Doc Brown 23.04.2017 - 08:36
0

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

Voy a sugerir una mejora en 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 aún mejor al principio Abierto-Cerrado.

Supongamos que implementas Strategy1 en 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 puedes ver, será un montón de código repetido. Para reducir la duplicación de código, puede usar una plantilla que se puede colocar en un archivo al que puedan acceder todas las clases Strategy .

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

Después de eso, lo único que tienes que 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 Strategy1 , usa ConcreteStrategy<1> .

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

Cambie la clase de ayuda para registrar Strategy s a:

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;
    
respondido por el R Sahu 24.04.2017 - 21:05

Lea otras preguntas en las etiquetas