La belleza de los patrones de diseño 60-Patrones de estrategia (Parte 1): ¿Cómo evitar largos códigos de juicio de rama if-else-switch?

60 | Patrón de estrategia (activado): ¿Cómo evitar el largo código de sentencia if-else/switch branch?

En las últimas dos lecciones, aprendimos sobre el patrón de plantilla. El modo de plantilla desempeña principalmente el papel de reutilización y extensión de código. Además, también hablamos sobre las devoluciones de llamada, que son similares al modo de plantilla, pero más flexibles de usar. La principal diferencia entre ellos es la implementación del código, el patrón de plantilla se implementa en función de la herencia y la devolución de llamada se implementa en función de la composición.

Hoy comenzamos a aprender otro patrón de comportamiento, el patrón de estrategia. En el desarrollo de proyectos reales, este modo también se usa comúnmente. El escenario de aplicación más común es usarlo para evitar juicios prolongados de if-else o switch branch. Sin embargo, hace más que eso. También puede proporcionar puntos de extensión del marco, etc., como el modo de plantilla.

Para el patrón de estrategia, lo explicaremos en dos clases. Hoy, explicamos el principio y la implementación del patrón de estrategia, y cómo usarlo para evitar la lógica de juicio de rama. En la próxima lección, usaré un ejemplo específico para explicar en detalle los escenarios de aplicación del patrón de estrategia y la intención de diseño real.

Sin más preámbulos, ¡comencemos oficialmente el estudio de hoy!

Principio e implementación del patrón de estrategia

Modo de estrategia, el nombre completo en inglés es Patrón de diseño de estrategia. En el libro "Patrones de diseño" de GoF, se define así:

Defina una familia de algoritmos, encapsule cada uno y hágalos intercambiables. La estrategia permite que el algoritmo varíe independientemente de los clientes que lo utilicen.

Traducido al chino es: definir una familia de clases de algoritmos, encapsular cada algoritmo por separado, para que puedan reemplazarse entre sí. El patrón de estrategia puede realizar cambios en los algoritmos independientemente de los clientes que los usen (los clientes aquí se refieren al código que usa los algoritmos).

Sabemos que el patrón de fábrica es desacoplar la creación y el uso de objetos, y el patrón del observador es desacoplar el observador y lo observado. El patrón de estrategia es similar a los dos y también puede desempeñar un papel de desacoplamiento.Sin embargo, desacopla las tres partes de la definición, creación y uso de la estrategia. A continuación, hablaré en detalle de las tres partes que debe contener un patrón de estrategia completo.

1. Definición de estrategia

La definición de una clase de estrategia es relativamente simple e incluye una interfaz de estrategia y un grupo de clases de estrategia que implementan esta interfaz. Debido a que todas las clases de estrategia implementan la misma interfaz, el código del cliente puede reemplazar de manera flexible diferentes estrategias basadas en la interfaz en lugar de la programación de implementación. El código de ejemplo es el siguiente:

public interface Strategy {
  void algorithmInterface();
}

public class ConcreteStrategyA implements Strategy {
  @Override
  public void  algorithmInterface() {
    //具体的算法...
  }
}

public class ConcreteStrategyB implements Strategy {
  @Override
  public void  algorithmInterface() {
    //具体的算法...
  }
}

2. Creación de políticas

Debido a que el modo de estrategia contendrá un conjunto de estrategias, al usarlas, el tipo (tipo) generalmente se usa para determinar qué estrategia crear para usar. Para encapsular la lógica de creación, necesitamos proteger los detalles de creación del código del cliente. Podemos extraer la lógica de crear estrategias basadas en tipo y ponerlas en la clase de fábrica. El código de ejemplo es el siguiente:

public class StrategyFactory {
  private static final Map<String, Strategy> strategies = new HashMap<>();

  static {
    strategies.put("A", new ConcreteStrategyA());
    strategies.put("B", new ConcreteStrategyB());
  }

  public static Strategy getStrategy(String type) {
    if (type == null || type.isEmpty()) {
      throw new IllegalArgumentException("type should not be empty.");
    }
    return strategies.get(type);
  }
}

En términos generales, si la clase de estrategia no tiene estado, no contiene variables miembro y es solo una implementación de algoritmo puro, dicho objeto de estrategia se puede compartir y usar, y no hay necesidad de crear uno nuevo cada vez que se usa getStrategy(). llamado del objeto de política. En respuesta a esta situación, podemos usar la implementación de clase de fábrica anterior para crear cada objeto de política por adelantado, almacenarlo en caché en la clase de fábrica y devolverlo directamente cuando se usa.

Por el contrario, si la clase de política tiene estado, de acuerdo con las necesidades del escenario comercial, esperamos que cada vez que obtengamos un objeto de política recién creado del método de fábrica, en lugar de almacenar en caché un objeto de política compartible, debemos seguir La clase de fábrica de políticas se implementa de la siguiente manera.

public class StrategyFactory {
  public static Strategy getStrategy(String type) {
    if (type == null || type.isEmpty()) {
      throw new IllegalArgumentException("type should not be empty.");
    }

    if (type.equals("A")) {
      return new ConcreteStrategyA();
    } else if (type.equals("B")) {
      return new ConcreteStrategyB();
    }

    return null;
  }
}

3. Uso de estrategias

Recién hablamos de la definición y creación de estrategias, ahora veamos el uso de las estrategias.

Sabemos que el patrón de estrategia contiene un conjunto de estrategias opcionales, ¿cómo determina generalmente el código del cliente qué estrategia usar? El más común es determinar dinámicamente qué estrategia usar en tiempo de ejecución, que también es el escenario de aplicación más típico del patrón de estrategia.

La "dinámica de tiempo de ejecución" aquí significa que no sabemos qué estrategia se usará de antemano, pero decidimos dinámicamente qué estrategia usar de acuerdo con factores inciertos como la configuración, la entrada del usuario y los resultados de los cálculos durante la ejecución del programa. A continuación, vamos a explicarlo con un ejemplo.

// 策略接口:EvictionStrategy
// 策略类:LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy...
// 策略工厂:EvictionStrategyFactory

public class UserCache {
  private Map<String, User> cacheData = new HashMap<>();
  private EvictionStrategy eviction;

  public UserCache(EvictionStrategy eviction) {
    this.eviction = eviction;
  }

  //...
}

// 运行时动态确定,根据配置文件的配置决定使用哪种策略
public class Application {
  public static void main(String[] args) throws Exception {
    EvictionStrategy evictionStrategy = null;
    Properties props = new Properties();
    props.load(new FileInputStream("./config.properties"));
    String type = props.getProperty("eviction_type");
    evictionStrategy = EvictionStrategyFactory.getEvictionStrategy(type);
    UserCache userCache = new UserCache(evictionStrategy);
    //...
  }
}

// 非运行时动态确定,在代码中指定使用哪种策略
public class Application {
  public static void main(String[] args) {
    //...
    EvictionStrategy evictionStrategy = new LruEvictionStrategy();
    UserCache userCache = new UserCache(evictionStrategy);
    //...
  }
}

Del código anterior, también podemos ver que la "determinación dinámica sin tiempo de ejecución", es decir, el método de uso en la segunda aplicación, no puede aprovechar el modo de estrategia. En este escenario de aplicación, el patrón de estrategia en realidad degenera en "características polimórficas orientadas a objetos" o "principios de programación basados ​​en interfaces en lugar de implementación".

¿Cómo usar el modo de estrategia para evitar el juicio de rama?

De hecho, el modo que puede eliminar la lógica de juicio de bifurcación no es solo el modo de estrategia, sino también el modo de estado del que hablaremos más adelante. Qué modo usar depende del escenario de la aplicación. El patrón de estrategia es adecuado para un escenario de aplicación que decide qué estrategia usar en función de diferentes tipos de dinámica.

Tomemos un ejemplo para ver cómo se genera la lógica de juicio de bifurcación if-else o switch-case. El código específico es el siguiente. En este ejemplo, no usamos el patrón de estrategia, sino que acoplamos directamente la definición, creación y uso de estrategias.

public class OrderService {
  public double discount(Order order) {
    double discount = 0.0;
    OrderType type = order.getType();
    if (type.equals(OrderType.NORMAL)) { // 普通订单
      //...省略折扣计算算法代码
    } else if (type.equals(OrderType.GROUPON)) { // 团购订单
      //...省略折扣计算算法代码
    } else if (type.equals(OrderType.PROMOTION)) { // 促销订单
      //...省略折扣计算算法代码
    }
    return discount;
  }
}

¿Cómo eliminar la lógica de juicio de rama? Ahí es donde el patrón de estrategia es útil. Usamos el patrón de estrategia para refactorizar el código anterior y diseñar las estrategias de descuento de diferentes tipos de pedidos en clases de estrategia, y la clase de fábrica es responsable de crear objetos de estrategia. El código específico es el siguiente:

// 策略的定义
public interface DiscountStrategy {
  double calDiscount(Order order);
}
// 省略NormalDiscountStrategy、GrouponDiscountStrategy、PromotionDiscountStrategy类代码...

// 策略的创建
public class DiscountStrategyFactory {
  private static final Map<OrderType, DiscountStrategy> strategies = new HashMap<>();

  static {
    strategies.put(OrderType.NORMAL, new NormalDiscountStrategy());
    strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy());
    strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy());
  }

  public static DiscountStrategy getDiscountStrategy(OrderType type) {
    return strategies.get(type);
  }
}

// 策略的使用
public class OrderService {
  public double discount(Order order) {
    OrderType type = order.getType();
    DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type);
    return discountStrategy.calDiscount(order);
  }
}

El código refactorizado no tiene declaración de juicio de rama if-else. De hecho, esto es gracias a la clase de fábrica de políticas. En la clase de fábrica, usamos Map para almacenar en caché la estrategia y obtener la estrategia correspondiente directamente del Map según el tipo, evitando así la lógica de juicio de bifurcación if-else. Cuando hablemos más adelante sobre el uso del patrón de estado para evitar la lógica de juicio de bifurcación, encontrará que usan la misma rutina. En esencia, utiliza el "método de búsqueda de tabla" para buscar la tabla según el tipo (las estrategias en el código son la tabla) en lugar de juzgar según la rama de tipo.

Sin embargo, si el escenario comercial necesita crear diferentes objetos de política cada vez, tenemos que usar otra implementación de clase de fábrica. El código específico es el siguiente:

public class DiscountStrategyFactory {
  public static DiscountStrategy getDiscountStrategy(OrderType type) {
    if (type == null) {
      throw new IllegalArgumentException("Type should not be null.");
    }
    if (type.equals(OrderType.NORMAL)) {
      return new NormalDiscountStrategy();
    } else if (type.equals(OrderType.GROUPON)) {
      return new GrouponDiscountStrategy();
    } else if (type.equals(OrderType.PROMOTION)) {
      return new PromotionDiscountStrategy();
    }
    return null;
  }
}

Esta implementación es equivalente a transferir la lógica de bifurcación if-else original de la clase OrderService a la clase factory, pero en realidad no la elimina. Con respecto a cómo resolver este problema, lo cerraré temporalmente hoy. Puede hablar sobre sus pensamientos en el área de mensajes, y lo explicaré en la próxima clase.

revisión clave

Bueno, eso es todo por el contenido de hoy. Resumamos y revisemos juntos, en qué necesitas concentrarte.

El patrón de estrategia define una familia de clases de algoritmos y encapsula cada algoritmo por separado para que puedan reemplazarse entre sí. El patrón de estrategia puede realizar cambios en los algoritmos independientemente de los clientes que los usen (los clientes aquí se refieren al código que usa los algoritmos).

El patrón de estrategia se utiliza para desvincular la definición, creación y uso de estrategias. De hecho, un patrón de estrategia completo se compone de estas tres partes.

  • La definición de una clase de estrategia es relativamente simple e incluye una interfaz de estrategia y un grupo de clases de estrategia que implementan esta interfaz.
  • La creación de políticas la realiza la clase de fábrica, que encapsula los detalles de la creación de políticas.
  • El modo de estrategia contiene un conjunto de estrategias opcionales.Hay dos formas de determinar cómo el código del cliente elige qué estrategia usar: determinación estática en tiempo de compilación y determinación dinámica en tiempo de ejecución. Entre ellos, la "determinación dinámica en tiempo de ejecución" es el escenario de aplicación más típico del patrón de estrategia.

Además, también podemos eliminar el juicio de rama if-else a través del modo de estrategia. De hecho, esto se beneficia de la clase de fábrica de estrategia y, más esencialmente, utiliza el "método de tabla de búsqueda" para reemplazar el juicio basado en la rama de tipo buscando la tabla basada en el tipo.

discusión en clase

Hoy dijimos que en la clase de fábrica de estrategia, si necesitamos devolver un nuevo objeto de estrategia cada vez, todavía necesitamos escribir la lógica de juicio de bifurcación if-else en la clase de fábrica, entonces, ¿cómo resolver este problema?

Bienvenido a dejar un mensaje y compartir sus pensamientos conmigo. Si obtienes algo, puedes compartir este artículo con tus amigos.

Supongo que te gusta

Origin blog.csdn.net/fegus/article/details/130498836
Recomendado
Clasificación