The Beauty of Design Patterns 60-Strategy Patterns (Part 1): How to avoid lengthy if-else-switch branch judgment codes?

60 | Strategy pattern (on): How to avoid lengthy if-else/switch branch judgment code?

In the last two lessons, we learned about the template pattern. The template mode mainly plays the role of code reuse and extension. In addition, we also talked about callbacks, which are similar to the template mode, but more flexible to use. The main difference between them is the code implementation, the template pattern is implemented based on inheritance, and the callback is implemented based on composition.

Today, we begin to learn another behavioral pattern, the strategy pattern. In actual project development, this mode is also commonly used. The most common application scenario is to use it to avoid lengthy if-else or switch branch judgments. However, it does more than that. It can also provide framework extension points, etc., like the template mode.

For the strategy pattern, we will explain it in two classes. Today, we explain the principle and implementation of the strategy pattern, and how to use it to avoid branch judgment logic. In the next lesson, I will use a specific example to explain in detail the application scenarios of the strategy pattern and the real design intent.

Without further ado, let's officially start today's study!

Principle and Implementation of Strategy Pattern

Strategy mode, the English full name is Strategy Design Pattern. In the GoF's "Design Patterns" book, it is defined like this:

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

Translated into Chinese is: define a family of algorithm classes, encapsulate each algorithm separately, so that they can replace each other. The Strategy pattern can make changes to algorithms independent of the clients that use them (clients here refer to the code that uses the algorithms).

We know that the factory pattern is to decouple the creation and use of objects, and the observer pattern is to decouple the observer and the observed. The strategy pattern is similar to the two and can also play a decoupling role. However, it decouples the three parts of strategy definition, creation, and use. Next, I will talk about the three parts that a complete strategy pattern should contain in detail.

1. Definition of strategy

The definition of a strategy class is relatively simple, including a strategy interface and a group of strategy classes that implement this interface. Because all strategy classes implement the same interface, client code can flexibly replace different strategies based on the interface rather than implementation programming. The sample code is as follows:

public interface Strategy {
  void algorithmInterface();
}

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

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

2. Policy creation

Because the strategy mode will contain a set of strategies, when using them, the type (type) is generally used to determine which strategy to create for use. In order to encapsulate the creation logic, we need to shield the creation details from the client code. We can extract the logic of creating strategies based on type and put them in the factory class. The sample code is as follows:

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);
  }
}

Generally speaking, if the strategy class is stateless, does not contain member variables, and is just a pure algorithm implementation, such a strategy object can be shared and used, and there is no need to create a new one every time getStrategy() is called of the policy object. In response to this situation, we can use the factory class implementation above to create each policy object in advance, cache it in the factory class, and return it directly when it is used.

On the contrary, if the policy class is stateful, according to the needs of the business scenario, we hope that every time we get a newly created policy object from the factory method, instead of caching a shareable policy object, then we need to follow The policy factory class is implemented as follows.

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. Use of strategies

We just talked about the definition and creation of strategies. Now, let's look at the use of strategies.

We know that the strategy pattern contains a set of optional strategies, how does the client code generally determine which strategy to use? The most common is to dynamically determine which strategy to use at runtime, which is also the most typical application scenario of the strategy pattern.

The "runtime dynamic" here means that we don't know which strategy will be used in advance, but dynamically decide which strategy to use according to uncertain factors such as configuration, user input, and calculation results during the running of the program. Next, let's explain it with an example.

// 策略接口: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);
    //...
  }
}

From the above code, we can also see that "non-runtime dynamic determination", that is, the usage method in the second Application, cannot take advantage of the strategy mode. In this application scenario, the strategy pattern actually degenerates into "object-oriented polymorphic features" or "programming principles based on interfaces rather than implementation".

How to use strategy mode to avoid branch judgment?

In fact, the mode that can remove the branch judgment logic is not only the strategy mode, but also the state mode we will talk about later. Which mode to use depends on the application scenario. The strategy pattern is suitable for an application scenario that decides which strategy to use based on different types of dynamics.

Let's take an example to see how the if-else or switch-case branch judgment logic is generated. The specific code is as follows. In this example, we did not use the strategy pattern, but directly coupled the definition, creation, and use of strategies.

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;
  }
}

How to remove the branch judgment logic? That's where the strategy pattern comes in handy. We use the strategy pattern to refactor the above code, and design the discount strategies of different types of orders into strategy classes, and the factory class is responsible for creating strategy objects. The specific code is as follows:

// 策略的定义
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);
  }
}

The refactored code has no if-else branch judgment statement. In fact, this is thanks to the policy factory class. In the factory class, we use Map to cache the strategy, and obtain the corresponding strategy directly from the Map according to the type, thus avoiding the if-else branch judgment logic. When we talk about using the state pattern to avoid branch judgment logic later, you will find that they use the same routine. In essence, it uses the "table look-up method" to look up the table based on the type (strategies in the code is the table) instead of judging based on the type branch.

However, if the business scenario needs to create different policy objects each time, we have to use another factory class implementation. The specific code is as follows:

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;
  }
}

This implementation is equivalent to transferring the original if-else branch logic from the OrderService class to the factory class, but it does not actually remove it. Regarding how to solve this problem, I will temporarily shut it down today. You can talk about your thoughts in the message area, and I will explain it in the next class.

key review

Well, that's all for today's content. Let's summarize and review together, what you need to focus on.

The strategy pattern defines a family of algorithm classes, and encapsulates each algorithm separately so that they can replace each other. The Strategy pattern can make changes to algorithms independent of the clients that use them (clients here refer to the code that uses the algorithms).

The strategy pattern is used to decouple the definition, creation, and use of strategies. In fact, a complete strategy pattern is composed of these three parts.

  • The definition of a strategy class is relatively simple, including a strategy interface and a group of strategy classes that implement this interface.
  • The creation of policies is done by the factory class, which encapsulates the details of policy creation.
  • The strategy mode contains a set of optional strategies. There are two ways to determine how the client code chooses which strategy to use: static determination at compile time and dynamic determination at runtime. Among them, "dynamic determination at runtime" is the most typical application scenario of the strategy pattern.

In addition, we can also remove the if-else branch judgment through the strategy mode. In fact, this benefits from the strategy factory class, and more essentially, it uses the "look-up table method" to replace the judgment based on the type branch by looking up the table based on the type.

class disscussion

Today we said that in the strategy factory class, if we need to return a new strategy object every time, we still need to write the if-else branch judgment logic in the factory class, so how to solve this problem?

Welcome to leave a message and share your thoughts with me. If you gain something, you are welcome to share this article with your friends.

Guess you like

Origin blog.csdn.net/fegus/article/details/130498836