¿Se puede implementar el patrón de estrategia sin ramificación significativa?

14

El patrón de estrategia funciona bien para evitar grandes construcciones if ... else y hace que sea más fácil agregar o reemplazar funcionalidades. Sin embargo, todavía deja un defecto en mi opinión. Parece que en cada implementación todavía tiene que haber una construcción ramificada. Puede ser una fábrica o un archivo de datos. Como ejemplo, tome un sistema de pedidos.

Fábrica:

// All of these classes implement OrderStrategy
switch (orderType) {
case NEW_ORDER: return new NewOrder();
case CANCELLATION: return new Cancellation();
case RETURN: return new Return();
}

El código después de esto no necesita preocuparse, y solo hay un lugar para agregar un nuevo tipo de orden ahora, pero esta sección de código aún no es extensible. Sacarlo a un archivo de datos ayuda un poco a la legibilidad (discutible, lo sé):

<strategies>
   <order type="NEW_ORDER">com.company.NewOrder</order>
   <order type="CANCELLATION">com.company.Cancellation</order>
   <order type="RETURN">com.company.Return</order>
</strategies>

Pero esto aún agrega código repetitivo para procesar el archivo de datos: un código más fácil de probar y relativamente estable, pero complejidad adicional no obstante.

Además, este tipo de construcción no prueba bien la integración. Cada estrategia individual puede ser más fácil de probar ahora, pero cada nueva estrategia que agregue es una complejidad adicional para probar. Es menos de lo que tendría si no hubiera usado el patrón, pero todavía está allí.

¿Hay alguna manera de implementar el patrón de estrategia que mitigue esta complejidad? ¿O es tan simple como parece, e intentar ir más allá solo agregaría otra capa de abstracción para poco o ningún beneficio?

Michael K
fuente
Hmmmmm ... podría ser posible simplificar cosas con cosas como eval... ¿podría no funcionar en Java pero quizás en otros idiomas?
FrustratedWithFormsDesigner
1
La reflexión de @FrustratedWithFormsDesigner es la palabra mágica en java
fanático del trinquete
2
Aún necesitará esas condiciones en alguna parte. Al enviarlos a una fábrica, simplemente cumple con DRY, ya que de lo contrario la declaración if o switch podría haber aparecido en varios lugares.
Daniel B
1
La falla que está mencionando a menudo se llama violación de la cerrado-abierto priciple
k3b
la respuesta aceptada en la pregunta relacionada sugiere un diccionario / mapa como alternativa a if-else y switch
mosquito

Respuestas:

16

Por supuesto no. Incluso si usa un contenedor de IoC, tendrá que tener condiciones en algún lugar, decidiendo qué implementación concreta inyectar. Esta es la naturaleza del patrón de estrategia.

Realmente no veo por qué la gente piensa que esto es un problema. Hay declaraciones en algunos libros, como Fowler's Refactoring , que si ve un interruptor / caso o una cadena de si / elses en el medio de otro código, debe considerar que huele y busca moverlo a su propio método. Si el código dentro de cada caso es más que una línea, tal vez dos, entonces debería considerar convertir ese método en un Método de Fábrica, devolviendo Estrategias.

Algunas personas han considerado que esto significa que un interruptor / caso es malo. Este no es el caso. Pero debería residir solo, si es posible.

pdr
fuente
1
Tampoco lo veo como algo malo. Sin embargo, siempre estoy buscando formas de reducir la cantidad de código que se mantendría en contacto, lo que provocó la pregunta.
Michael K
Las declaraciones de cambio (y los bloques largos if / else-if) son malos y deben evitarse si es posible para mantener su código mantenible. Dicho esto, "si es posible", reconoce que hay algunos casos en los que el interruptor debe existir, y en esos casos, trate de mantenerlo en un solo lugar, y uno que haga que mantenerlo sea menos complicado (es muy fácil accidentalmente omite 1 de los 5 lugares en el código que necesitaba mantener sincronizados cuando no lo aísla correctamente).
Shadow Man
10

¿Se puede implementar el patrón de estrategia sin ramificación significativa?

, utilizando un hashmap / diccionario donde se registra cada implementación de Estrategia. El método de fábrica se convertirá en algo así como

Class strategyType = allStrategies[orderType];
return runtime.create(strategyType);

Cada implementación de estrategia debe registrar una fábrica con su orderType y alguna información sobre cómo crear una clase.

factory.register(NEW_ORDER, NewOrder.class);

Puede usar el constructor estático para el registro, si su idioma lo admite.

El método de registro no hace más que agregar un nuevo valor al hashmap:

void register(OrderType orderType, Class class)
{
   allStrategies[orderType] = class;
}

[actualización 2012-05-04]
Esta solución es mucho más compleja que la "solución de cambio" original que preferiría la mayor parte del tiempo.

Sin embargo, en un entorno donde las estrategias cambian a menudo (es decir, el cálculo del precio según el cliente, el tiempo, ...), esta solución de mapa hash combinada con un IoC-Container podría ser una buena solución.

k3b
fuente
3
Entonces, ¿ahora tiene todo un Hashmap de Estrategias, para evitar un cambio / caso? ¿Cómo es exactamente las filas de factory.register(NEW_ORDER, NewOrder.class);limpiador, o menos una violación de OCP, que las filas de case NEW_ORDER: return new NewOrder();?
pdr
55
No es más limpio en absoluto. Es contrario a la intuición. Sacrifica el principio de mantener el estímulo estúpido para mejorar el principio abierto-cerrado. Lo mismo se aplica a la inversión de control y la inyección de dependencia: son más difíciles de entender que una solución simple. La pregunta era "implementar ... sin ramificación significativa" y no "cómo crear una solución más intuitiva"
k3b
1
No veo que hayas evitado ramificarte tampoco. Acaba de cambiar la sintaxis.
pdr
1
¿No hay un problema con esta solución en idiomas que no cargan clases hasta que se necesitan? El código estático no se ejecutará hasta que se cargue la clase, y la clase nunca se cargará porque ninguna otra clase se refiere a ella.
Kevin Cline
2
Personalmente, me gusta este método, porque le permite tratar sus estrategias como datos mutables , en lugar de tratarlas como código inmutable.
Tacroy
2

La "estrategia" consiste en tener que elegir entre algoritmos alternativos al menos una vez , no menos. En algún lugar de su programa, alguien tiene que tomar una decisión, tal vez el usuario o su programa. Si utiliza un IoC, una reflexión, un evaluador de archivos de datos o una construcción de interruptor / caso para implementar esto, no cambia la situación.

Doc Brown
fuente
No estoy de acuerdo con tu declaración de una vez. Las estrategias se pueden intercambiar en tiempo de ejecución. Por ejemplo, después de no procesar una solicitud utilizando una ProcessingStrategy estándar, se puede elegir una VerboseProcessingStrategy y volver a ejecutar el procesamiento.
Dmux
@dmux: claro, edité mi respuesta en consecuencia.
Doc Brown
1

El patrón de estrategia se usa cuando se especifican posibles comportamientos y se usa mejor cuando se designa un controlador en el inicio. La especificación de qué instancia de la estrategia usar se puede hacer a través de un Mediador, su contenedor de IoC estándar, alguna Fábrica como usted describe, o simplemente usando la correcta según el contexto (ya que a menudo, la estrategia se suministra como parte de un uso más amplio de la clase que lo contiene).

Para este tipo de comportamiento en el que se deben invocar diferentes métodos basados ​​en datos, entonces sugeriría utilizar las construcciones de polimorfismo proporcionadas por el lenguaje en cuestión; No es el patrón de estrategia.

Telastyn
fuente
1

Al hablar con otros desarrolladores donde trabajo, encontré otra solución interesante, específica para Java, pero estoy seguro de que la idea funciona en otros idiomas. Construya una enumeración (o un mapa, no importa demasiado cuál) usando las referencias de clase e instancia usando la reflexión.

enum FactoryType {
   Type1(Type1.class),
   Type2(Type2.class);

   private Class<? extends Type> clazz;

   private FactoryType(Class<? extends Type> clazz) {
      this.clazz = clazz;
   }

   public Class<? extends Type> getTypeClass() {
      return clazz;
   }
}

Esto reduce drásticamente el código de fábrica:

public Type create(FactoryType type) throws Exception {
   return type.getTypeClass().newInstance();
}

(Por favor ignore el mal manejo de errores - código de ejemplo :))

No es el más flexible, ya que aún se requiere una compilación ... pero reduce el cambio de código a una línea. Me gusta que los datos estén separados del código de fábrica. Incluso puede hacer cosas como establecer parámetros, ya sea utilizando clases / interfaces abstractas para proporcionar un método común o creando anotaciones en tiempo de compilación para forzar firmas de constructor específicas.

Michael K
fuente
No estoy seguro de qué causó que esto volviera a la página de inicio, pero esta es una buena respuesta y en Java 8 ahora puede crear referencias de Constructor sin reflexión.
JimmyJames
1

La extensibilidad se puede mejorar haciendo que cada clase defina su propio tipo de orden. Luego, su fábrica selecciona la que coincida.

Por ejemplo:

public interface IOrderStrategy
{
    OrderType OrderType { get; }
}

public class NewOrder : IOrderStrategy
{
    public OrderType OrderType { get; } = OrderType.NewOrder;
}

public class OrderFactory
{
    private IEnumerable<IOrderStrategy> _strategies;

    public OrderFactory(IEnumerable<IOrderStrategy> strategies) // Injected by IoC container
    {
        _strategies = strategies;
    }

    public IOrderStrategy Create(OrderType orderType)
    {
        IOrderStrategy strategy = _strategies.FirstOrDefault(s => s.OrderType == orderType);

        if (strategy == null)
            throw new ArgumentException("Invalid order type.", nameof(orderType));

        return strategy;
    }
}
Eric Eskildsen
fuente
1

Creo que debería haber un límite en cuántas ramas son aceptables.

Por ejemplo, si tengo más de ocho casos dentro de mi declaración de cambio, entonces vuelvo a evaluar mi código y busco lo que puedo refactorizar. Muy a menudo encuentro que ciertos casos se pueden agrupar en una fábrica separada. Mi suposición aquí es que hay una fábrica que construye estrategias.

De cualquier manera, no puede evitar esto y en algún lugar tendrá que verificar el estado o el tipo del objeto con el que está trabajando. Luego construirá una estrategia para eso. Buena separación de preocupaciones.

CodeART
fuente