2010-06-08 45 views
19

He estado leyendo sobre patrones de diseño y quería algo de perspectiva. Considere lo siguiente:¿Cómo resolver el problema "Growing If Statement"?

Dim objGruntWorker as IGruntWorker 

if SomeCriteria then 
    objGruntWorker = new GoFor() 
else if SomeOtherCriteria then 
    objGruntWorker = new Newb() 
else if SomeCriteriaAndTheKitchenSink then 
    objGruntWorker = new CubeRat() 
end if 

objGruntWorker.GetBreakfast() 
system.threading.thread.sleep(GetMilliSecondsFromHours(4)) 
objGruntWorker.GetLunch() 

El código anterior crece cada vez que aparece un nuevo Criterio. He visto códigos como este por todas partes y, por ignorancia, escribí algunos de ellos. ¿Cómo se debe resolver esto? ¿Este tipo de antipatrón tiene un nombre más "formal"? ¡Gracias por tu ayuda!

Editar: Otra consideración es que quiero evitar tener que recompilar las implementaciones existentes de IGruntWorker simplemente para agregar una nueva implementación.

+3

'TimeSpan.FromHours (4) .Milliseconds' – SLaks

+0

¿Por qué cree que esto es un anti-patrón? –

+0

Cada vez que aparece un nuevo criterio (una versión diferente de la interfaz), entonces el código debe recompilarse y esa declaración if debe expandirse. – Achilles

Respuesta

2

Si está utilizando .NET se podría construir con la reflexión en su lugar. Por ejemplo, si estuviera creando un sistema de plugins, entonces tendría una carpeta a caer DLL de plugin en. Y tu fábrica sería mirar los archivos DLL disponibles, examinar cada uno de los atributos de reflexión apropiadas, a continuación, coinciden con los atributos contra todo lo que se aprobó en cadena para decidir cuál es el objeto de seleccionar e invocar.

Esto evita tener que volver a compilar su aplicación principal, aunque tendrá que crear sus trabajadores en otras DLL y luego tener una forma de decirle a su fábrica cuál usar.

Aquí hay un código de pseudo muy rápido y sucio para conseguir el punto a través de:

Asumiendo que tiene un conjunto de DLL llamada Workers.DLL

Establecer un atributo llamado WorkerTypeAttribute con una propiedad de cadena llamado nombre, y el constructor para poder establecer esa propiedad de Nombre.

[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)] 
public class WorkerTypeAttribute : Attribute 
{ 
    string _name; 
    public string Name { get { return _name; } } 
    public WorkerTypeAttribute(string Name) 
    { 
     _name = Name; 
    } 
} 

Se podría entonces aplicar este atributo a una clase trabajadora que ha definido como:

[WorkerType("CogWorker")] 
public class CogWorker : WorkerBase {} 

Luego, en la fábrica trabajador de su aplicación que iba a escribir un código como:

public void WorkerFactory(string WorkerType) 
    { 
     Assembly workers = Assembly.LoadFile("Workers.dll"); 
     foreach (Type wt in workers.GetTypes()) 
     { 
      WorkerTypeAttribute[] was = (WorkerTypeAttribute[])wt.GetCustomAttributes(typeof(WorkerTypeAttribute), true); 
      if (was.Count() == 1) 
      { 
       if (was[0].Name == WorkerType) 
       { 
        // Invoke the worker and do whatever to it here. 
       } 
      } 
     } 
    } 

estoy seguro de que hay otros ejemplos de cómo hacer esto por ahí, pero si usted necesita un poco más punteros, que me haga saber. La clave es que todos los trabajadores deben tener un padre o interfaz común para que se les puede invocar la misma manera. (Es decir, todos los trabajadores necesitan un método común "Ejecutar" o algo que se puede llamar desde la fábrica, o donde quiera que utilizar el objeto.

+0

Necesita una forma de correlacionar algunos criterios con el tipo de trabajo real. Aparte de eso, este código tiene mucho sentido. – Achilles

+0

Sí, aún debe saber cuándo seleccionar qué trabajador. Pero con los atributos, podría proporcionarle una lista al usuario, por ejemplo, para que elija qué trabajador usar, o para manejarlo a través de otra lógica en el sistema. Si se pudiera resumir todos los datos que se necesitaría utilizar para tomar la decisión también podría pasar a ese objeto de datos para cada uno de los trabajadores a través de llamadas de reflexión y llamar a un método como "CanExecuteRequest (DataObject de datos)" Esta El método luego permitiría al trabajador decidir si se suponía que manejaría la solicitud o si simplemente la ignoraría. –

+0

En otras palabras: cargar el ensamblado encontrar las clases que son trabajadores envían cada uno el paquete de datos a través de CanExecuteRequest() trabajador responde de vuelta con un verdadero/falso (o sólo lo hace la petición) Llame a cada trabajador que respondió Atrás verdadero Envíe los resultados. Sus trabajadores nuevamente tendrían que tener una interfaz estándar, y tendrían que tener algún resultado estándar que su aplicación principal pueda confiar y comprender. –

0

Creo que siempre que sus criterios más probables sean ordenados primero para permitir que el tiempo de ejecución salte el resto de los casos, está bien.

Si su preocupación es solo la legibilidad, podría utilizar el operador ternario o si las evaluaciones de criterios son solo ==, podría usar una instrucción switch.

7

Ese tipo de lógica a menudo se encapsula con el Factory method pattern. (Consulte el ejemplo de ImageReaderFactory en Encapsulation.)

+3

según tengo entendido, "método de fábrica" ​​es lo que ya tiene. No estoy seguro, ese código existente puede ser más conciso en cualquiera de los lenguajes .net a excepción de F #. La coincidencia de patrones probablemente sea de gran ayuda. – Roman

+0

@Roman: Parece que tiene esta lógica en línea en el código donde está instanciando y luego usando 'objGruntWorker', no en un método separado. –

+0

El método de fábrica simplemente refactoriza el problema en otra clase. Entonces esa clase tiene que cambiar cada vez que se agrega una nueva implementación de IGruntWorker. – Achilles

5

El tipo de patrón que se adaptaría a la solución anterior sería el Factory Pattern. Tiene una situación en la que no necesita conocer el tipo concreto de objeto que necesita, solo tiene que implementar IGruntWorker. Entonces usted crea una fábrica que toma un criterio y basado en ese criterio, usted devolvería el objeto específico IGruntWorker. Por lo general, es una buena idea asignar los criterios a algún identificador, es decir, una enumeración o constante para legibilidad, p.

public enum WorkerType 
{ 
    Newbie, 
    Average, 
    Expert 
} 

public class WorkerFactory 
{ 
    public static IGruntWorker GetWorker(WorkerType type) 
    { 
     switch (type) 
     { 
      case WorkerType.Newbie: 
       return new NewbieWorker(); 
      case WorkerType.Average: 
       return new AverageWorker(); 
      case WorkerType.Expert: 
       return new ExpertWorker(); 
     } 
    } 
} 

Por lo tanto, en su caso podría tener un pequeño método auxiliar que resuelva el tipo correcto de trabajador requerido según los criterios. Esto podría incluso incluirse en una propiedad de solo lectura que acaba de pasar a la fábrica.

+3

Lo único que no me gusta de esta solución es que parece refactorizar el problema en otra clase. Todavía tengo una declaración de cambio creciente. – Achilles

+4

Sí, pero es claro y está aislado en cuanto a su propósito: la mayoría de la gente reconocerá que es una clase de fábrica, construye diferentes tipos de trabajadores y simplifica el uso en el código de la aplicación. – MikeJ

+0

Es más fácil probar la unidad al menos. Solo tiene que subclasificar la clase Factory. De lo contrario, es realmente difícil probar el código como su ejemplo. –

5

Puede crear fábricas para cada tipo de objeto, y esas fábricas podrían tener una función que tome criterias como parámetro y devuelva un IGruntWorker si los parámetros están satisfechos (o null en caso contrario).

A continuación, puede crear una lista de esas fábricas y bucle a través de ellos como (lo siento, soy aC# chico):

Dim o as IGruntWorker; 
foreach (IGruntWorkerFactory f in factories) 
{ 
    o = f.Create(criterias); 
    if (o != null) 
     break; 
} 

Cuando se necesita un nuevo criterio, sólo agregarlo a la lista de fábricas, no es necesario modificar el ciclo.

Probablemente hay algunos aspectos más bellos

Mis 2 centavos

+3

:. 'o = factories.Select (f => f.Create (criterios)) FirstOrDefault(); ' – tzaman

0

creo que este patrón está bien, siempre y cuando los criterios y las operaciones son simples líneas/llamadas a métodos. Esto es fácil de leer y con precisión refleja su lógica:

if (ConditionOne()) 
    { 
    BuildTheWidget(); 
    } 
    else if (ConditionTwo()) 
    { 
    RaiseTheAlarm(); 
    } 
    else if (ConditionThree()) 
    { 
     EverybodyGetsARaise(); 
    } 

Incluso si hay 20 condiciones diferentes, que probablemente es un reflejo exacto de algo de lógica de negocio complejas de su aplicación.

Por otra parte, esto es un desastre legibilidad

if ( ((A && B) || C && 
     (D == F) || (F == A))) 
{ 
    AA; 
    BB; 
    //200 lines of code 
} 
else if ((A || D) && B) 
{ 
    // 200 more lines 
} 
+1

El problema es que no es extensible. Este bloque único de código debe conocer todos los tipos y cuáles son sus criterios. Si se agrega una nueva clase, el código aquí tiene que hacer referencia a ella y agregar una nueva prueba. Creo que una solución en la línea de lo sugerido por Mike Gleason jr Couturier ofrece lo mejor de ambos mundos. –

+0

Ese es mi punto.Solo quiero compilar el código que necesito, y las compilaciones de "efectos secundarios" no son buenas en un sistema grande. – Achilles

+0

Una forma de hacer esto sería hacer que cada hijo se registre con la clase base en un constructor estático. Esto le permitirá conectar a un niño al agregar una referencia y luego llamar a cualquier miembro estático. –

1

Si se puede definir un objeto con un método checkCriteria, entonces se puede maquillaje . esta tabla impulsado por código de no sé C#, así que tengan paciencia conmigo en la sintaxis:

public class WorkerFactory { 
    IGruntWorker makeWorkerIfCriteria(criteria_parameters parms); 
} 

extern WorkerFactory worker_factories[]; /* table with factories in order */ 

IGruntWorker makeJustTheRightWorker(criteria_parameters actual_critera) { 
    for (i = 0; i < worker_factories.length(); i++) { 
    IGruntWorwer w = worker_factories[i].makeWorker(actual_criteria); 
    if (!null(w)) return w; 
    } 
    --- grim error --- /* table not initiailized correctly */ 
} 

a continuación, algunos de los objetos en el aspecto tabla como ésta

public class MakeGoFor(critera_parameters cp) { 
    if SomeCriteria then 
     return new GoFor(); 
    else 
     return NULL; 
} 

puede volver a compilar la mesa en una módulo separado sin tener que volver a compilar el código de selección. De hecho, si se vuelve ambicioso, incluso podría compilar la tabla en tiempo de ejecución según los argumentos de la línea de comandos o el contenido de un archivo ...

0

Creo que mucho depende de cuán predecibles sean sus 'condiciones' son. Su 'IF en crecimiento' ES esencialmente una fábrica, y tal vez la refactorización a su propio método o clase ayudaría, pero aún PUEDE ser un IF en crecimiento. Si tus condiciones son cosas que no puedes predecir, como "si joe.is.on.fire" o "si x == 2" o "si! Shuttle.is.launched", entonces estás atascado con IF's.

Una cosa negativa sobre estos súper-IF es el alcance que puedan tener sobre su aplicación. Es decir, todo lo que no es necesario llamar a/touch/comprueba para determinar qué 'si' debe ser verdad? Es posible que termine teniendo toneladas de cruft globales o muchos parámetros para pasar a su 'fábrica'. Una cosa que hice hace poco para ayudar con esto fue implementar una fábrica que contenía un conjunto de booleanos delegados (Func) y tipos.Registraría los tipos y delegados booleanos en el momento de la inicialización, e iteraré a través de la lista en la fábrica llamando a cada delegado hasta que obtuve un 'verdadero' y luego instanciara ese tipo. Eso funcionó bien para mí porque pude 'registrar' nuevas condiciones sin editar la fábrica.

Sólo una idea

1

Podría utilizar una variante del patrón de visitante en su lugar? Llámalo al visitante fábrica (tal vez)

excusa el pseudo-código, pero mi VB es oxidado

Dim objGruntWorker as IGruntWorker 

objGruntWorker = null 

// all your objects implement IFactoryVisitor 
Dim factory as IFactoryVisitor 
while objGruntWorker == null 
    factory = factoryCollection.GetNext 
    objGruntWorker = factory.TryBuild(...) 
end 

objGruntWorker.GetBreakfast() 
system.threading.thread.sleep(GetMilliSecondsFromHours(4)) 
objGruntWorker.GetLunch() 
+0

Parece que Mike Gleason jr Couturier sugirió, pero +1 por darle un nombre. Visitante de la fábrica –

0

Sé que su .NET pero esta es la forma en que hago algo similar en una aplicación web Java, donde mis "si-si" estaban creciendo ... aún requiere una recompilación, pero es fácil agregar otras acciones o en su caso, los trabajadores del gruñido.

private HashMap actionMap = new HashMap(); 

actionMap.put("cubeRat", new CubeRatAction()); 
actionMap.put("newb", new NewbAction()); 
actionMap.put("goFor", new goForAction()); 
actionMap.put("other", new otherAction()); 

String op = request.getParameter("criteria"); // not sure how your criteria is passed in but this is through a parameter in my URL. 
ControllerAction action = (ControllerAction) actionMap.get(op); 
if (action != null) { 
    action.GetBreakfast(); 
    action.Sleep(); 
    action.GetLunch();    
} else { 
    String url = "views/errorMessage_v.jsp"; 
    String errMessage = "Operation '" + op + "' not valid for in '" + request.getServletPath() + "' !!"; 
    request.setAttribute("message", errMessage); 
    request.getRequestDispatcher(url).forward(request, response); 
} 
0

Puede utilizar la reflexión para encontrar un constructor de un tipo dado y crear la instancia por el constructor. Por causa, los constructores deben seguir cierto patrón. En su ejemplo anterior, todos son constructores por defecto.

Cuestiones relacionadas