The article about strategy pattern, in fact the net is really too much, oneself have no what literary talent again, affirmation also can’t write what flower! But then I remembered Kong Yiji. Yes, kong Yiji, who knew that there were four ways to write hui. At that time, the Chinese teacher said that this scene was to show kong Yiji’s pedantic and feudal character. However, I have different views on kong Yiji’s “Hui”, which is written in four different ways. I think in some ways it’s good to know how to write four different ways. For example, if your boss assigns you a task, you can write it in four different ways. Therefore, I still feel it is necessary to write this article to show you different ways of writing hui.

Assuming that there is a business scenario of lottery at present, the prizes are divided into four categories: cash, coupons, points and thank you for participating. In the future, new types of prizes may be added, such as the number of times of complimentary lottery. Users will be sent to the user in real time. How to implement the logic of the award distribution, I will write pseudo-code:

 public void sendReward(Byte rewardType, String reward) {
        if (rewardType.equals("Points")) {
            log.info("Send bonus points [{}]", reward);
        } else if (rewardType.equals("Coupon")) {
            log.info("Send coupons for rewards [{}]", reward);
        } else if (rewardType.equals("Cash")) {
            log.info("Send cash rewards [{}]", reward);
        } else if (rewardType.equals("Thank you for playing.")) {
            log.info("Sorry, thanks for playing [{}]", reward); }}Copy the code

Take a look at the code and see if it feels similar and if you think, “I wrote it!” There’s no shame in having written this kind of code before, because most of us are normal people and we’re not all ten of us. I used to write this kind of code a lot, because I didn’t know what good code was and how to write good code at that time. Fowler, whose book Reconstruction is available. What I want to emphasize here is that it doesn’t matter that you didn’t write great code in the past. What matters is that you now know what great code is and how to write great code. Otherwise, where is the sediment of your years of experience?

Any fool can write code that a computer can understand. A good programmer is one who writes code that humans can easily understand. — Martin Fowler Fowler)

What’s the problem

Now let’s talk about what’s wrong with this pseudo-code:

  1. NPE. Use directly without checking the input parameterRewardType. Equals (" points ")To judge, if the rewardType value is null, this is where the null pointer exception will occur.
  2. Hard coding problem. There are multiple copies of the mana values for bonus types, which can be misspelled and don’t give a good indication of how many bonus types there are.
  3. Too much if else leads to deep nesting and complex logical expressions.
  4. Violation of open closed principle (OCP) and single responsibility principle (SRP). It is clearly proposed that the types of prizes for “giving lottery times” may be increased in the future. In order to increase the sending of new prizes according to the current structure of the code, it is bound to need to be modifiedsendReward()Code, adding a new branch, which modifies the original code, will require regression testing of the code there.

How to improve

According to the above mentioned problems, we give targeted optimization suggestions.

  1. Equals (rewardType) or use the java.util.objects.equals () method instead.

  2. Define an enumeration of reward types.

  3. Use guard statements to optimize judgment.

Using the above three optimization suggestions, we modify the pseudo-code, and the final pseudo-code is shown as follows:

Define an enumeration of reward types

@Getter
public enum RewardTypeEnum {
    /** * Cash bonus */
    CASH("1"."Cash"),
    /** * integral */
    POINT("2"."Points"),
    /** ** coupon */
    COUPON("3"."Coupon"),
    /** ** Thank you for joining */
    THANK_YOU("4"."Thank you for playing."),;private String code;

    private String desc;

    RewardTypeEnum(String code, String desc) {
        this.code = code;
        this.desc = desc; }}Copy the code

Modified send reward pseudocode:

 public void sendReward(String rewardType, String reward) {
        if (RewardTypeEnum.POINT.getCode().equals(rewardType)) {
            log.info("Send bonus points [{}]", reward);
            return;
        }
        if (RewardTypeEnum.COUPON.getCode().equals(rewardType)) {
            log.info("Send coupons for rewards [{}]", reward);
            return;
        }
        if (RewardTypeEnum.CASH.getCode().equals(rewardType)) {
            log.info("Send cash rewards [{}]", reward);
            return;
        }
        if (RewardTypeEnum.THANK_YOU.getCode().equals(rewardType)) {
            log.info("Sorry, thanks for playing [{}]", reward);
            return; }}Copy the code

To solve problem 4, we need to think about it first. There are four types of the kinds of rewards, the follow-up is likely to increase the types of rewards, the different reward to the users must be involved in the different modules, calls the different interface, the final data must be written in a different data in the table, the issuance of this kind of situation is obviously different reward have different strategies, obviously everyone can think of in line with the strategy pattern, Here comes the hero of this article!

Strategy pattern is a kind of behavior pattern in design pattern. It mainly encapsulates algorithms and can be replaced with each other. When reading books on design patterns, a policy pattern first defines an interface, then a concrete policy class implements the policy interface, and finally a Context Context class holds a concrete policy for the caller to use. The problem with this is that a lot of books and blogs don’t explain how to elegantly let context classes hold a specific strategy, so I’ll frame the code and get to that later.

Defining a Policy Interface


public interface RewardSendStrategy {
    /** ** Send rewards *@paramMemberId memberId */
    void sendReward(Long memberId);

}
Copy the code
The strategy of sending coupons
@Slf4j
@Service
public class CouponRewardSendStrategy implements RewardSendStrategy {

    @Override
    public void sendReward(Long memberId) {
        log.info("Send coupons to [{}] for prizes [{}]", memberId); }}Copy the code
A strategy for sending credits
@Slf4j
@Service
public class PointRewardSendStrategy implements RewardSendStrategy {

    @Override
    public void sendReward(Long memberId) {
        log.info("Send bonus prizes to [{}]", memberId); }}Copy the code
Thank you for participating in the strategy
@Slf4j
@Service
public class ThankYouRewardSendStrategy implements RewardSendStrategy {

    @Override
    public void sendReward(Long memberId) {
        log.info("[{}], sorry, thanks for playing.", memberId); }}Copy the code
Cash delivery strategy
@Slf4j
@Service
public class CashRewardSendStrategy implements RewardSendStrategy {

    @Override
    public void sendReward(Long memberId) {
        log.info("Send cash prizes to [{}]", memberId); }}Copy the code
Define the Context Context class
public class RewardSendStrategyContext {

    /** * Context holds the specific policy */
    private RewardSendStrategy sendStrategy;

    public void setSendStrategy(RewardSendStrategy sendStrategy) {
        this.sendStrategy = sendStrategy;
    }

    public void excute(Long memberId) { sendStrategy.sendReward(memberId); }}Copy the code
Unit test, how to use it
@ContextConfiguration(locations = {"classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml"})
@RunWith(value = SpringJUnit4ClassRunner.class)
public class StrategyTest {

    @Test
    public void test(a) {
        RewardSendStrategyContext context = new RewardSendStrategyContext();
        context.setSendStrategy(new CouponRewardSendStrategy());
        context.excute(11L); }}Copy the code

There is no doubt about the execution result of the unit test, it must print out to send coupon prizes to [11], but I feel that this call method does not solve the if else multi-level logical judgment, why say so, because I know that I am calling the strategy of issuing coupons, but in the actual business call, I don’t know what type of prize to send, so I’ll have to decide which strategy to use depending on the type of prize that comes in.

public void test2(a) {
        String rewardType = RewardTypeEnum.THANK_YOU.getCode();
        RewardSendStrategyContext strategyContext = new RewardSendStrategyContext();
        if (RewardTypeEnum.POINT.getCode().equals(rewardType)) {
            strategyContext.setSendStrategy(new PointRewardSendStrategy());
            strategyContext.excute(11L);
            return;
        }
        if (RewardTypeEnum.COUPON.getCode().equals(rewardType)) {
            strategyContext.setSendStrategy(new CouponRewardSendStrategy());
            strategyContext.excute(11L);
            return;
        }
        if (RewardTypeEnum.CASH.getCode().equals(rewardType)) {
            strategyContext.setSendStrategy(new CashRewardSendStrategy());
            strategyContext.excute(11L);
            return;
        }
        if (RewardTypeEnum.THANK_YOU.getCode().equals(rewardType)) {
            strategyContext.setSendStrategy(new ThankYouRewardSendStrategy());
            strategyContext.excute(11L);
            return; }}Copy the code

Optimize as we suggested earlier, and the code looks like it’s back to square one. The above code clearly does not comply with the open and close principle and the single principle, it seems that we still need further optimization.

Feature improvements using the Spring framework

Previously we have split the logic for sending different prizes into separate classes, following the single responsibility principle. However, there is no uniform entry, resulting in a large number of if judgments where the policy is invoked, and the code still does not satisfy the open and closed principle. So the next thing we need to do is eliminate these if judgments and provide an easy-to-use call interface.

How can we reduce or eliminate the presence of if judgments in the code above? If you recall the new version of the policy pattern from a previous article, you may not have seen it before. In this article, I annotated each policy method with the value of the annotation as key, stored the policy method as value in the Map, and then used the Map get() to get the specific policy method, eliminating the if judgment. This is actually table-driven method, is a very classic programming skills. So let’s continue this approach here to solve our current problem. Of course, we don’t need custom annotations, we don’t need reflection calls, we don’t need that much work. Spring is a very good Java framework, generally do Java development is based on the Spring framework, the Spring framework has many features, techniques to help us write better code. Let me show you how we can use the Spring framework to take our code quality to the next level.

To do this, I need to modify the interface. We define two more methods in the interface, one of which identifies the reward type of the current class.

public interface RewardSendStrategy {

    /** * The type of reward to be given, which is a way to indicate different strategies *@return* /
    String type(a);
    /** * Whether to match *@paramType Reward type *@return* /
    boolean isTypeMatch(String type);
    /** ** Send rewards *@paramMemberId memberId */
    void sendReward(Long memberId);

}


Copy the code

In accordance with this interface definition, we implement related methods in the corresponding policy class, only one policy class is shown here as a demonstration.

@Slf4j
@Service
public class CouponRewardSendStrategy implements RewardSendStrategy {

    @Override
    public String type(a) {
        return RewardTypeEnum.COUPON.getCode();
    }

    @Override
    public boolean isTypeMatch(String type) {
        return Objects.equals(type, type());
    }

    @Override
    public void sendReward(Long memberId) {
        log.info("Send coupon prizes to [{}]", memberId); }}Copy the code

Now that we’re all set, let’s show the power of the Spring framework. Let it assemble the Map we need!

Plan a@Autowiredannotations

This annotation is used many times to inject other beans, but you may not know it:

 * <p>In case of a {@link java.util.Collection} or {@link java.util.Map}
 * dependency type, the container will autowire all beans matching the
 * declared value type. In case of a Map, the keys must be declared as
 * type String and will be resolved to the corresponding bean names.
Copy the code

This is the @autowired annotation from the source code, and I’ll give you an example

Spring automatically adds all of the interface's implementation class beans to the rewardSendStrategyList when the properties are defined this way@Autowired
privateList<RewardSendStrategy> rewardSendStrategyList; Spring automatically adds the beanName implementation class as the key and the bean as the value to the Map@Autowired
private  Map<String, RewardSendStrategy> strategyMap;
Copy the code

Using this feature we can easily implement our Map. We can modify the previous Context class a little bit and use the Lamda expression to get the following code:

@Slf4j
@Component
public class RewardSendStrategyFactory {

    @Autowired
    private List<RewardSendStrategy> strategyList;

    /** * Get policy instance *@param type
     * @return* /
    public RewardSendStrategy getImpl(String type) {
        return strategyList.stream()
                .filter(strategy -> strategy.isTypeMatch(type))
                .findAny()
                .orElseThrow(() -> new RuntimeException("No policy implementation found")); }}Copy the code

This way we call much more convenient, and very silky oh!

@ContextConfiguration(locations = {"classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml"})
@RunWith(value = SpringJUnit4ClassRunner.class)
public class StrategyTest {

    @Autowired
    private RewardSendStrategyFactory factory;

    @Test
    public void test(a) {
        RewardSendStrategy strategy = factory.getImpl(RewardTypeEnum.POINT.getCode());
        strategy.sendReward(11L); }}Copy the code

Looking at the unit test output, I feel like I’ve finally found the perfect solution to implement the strategy pattern. Ha ha, in fact, there are several ways, this way I think is the easiest, and I use the most. So I’ll write it first, and then I’ll talk about the last two.

Scenario two uses the Spring framework’s ApplicationContextAware extension point

Org. Springframework. Context. ApplicationContextAware interface, can get ApplicationContext also means that you can get all the Bean in the Spring container, since got all Bean, Then we can go through the beans and put them into the Map as needed. Context class name of the class is changed, I also changed to RewardSendStrategyFactory, specific code is as follows:

@Slf4j
@Component
public class RewardSendStrategyFactory implements ApplicationContextAware {

    /** * Save all policies */
    private final static Map<String, RewardSendStrategy> STRATEGY_MAP = new LinkedHashMap<>();


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        Map<String, RewardSendStrategy> beans = applicationContext.getBeansOfType(RewardSendStrategy.class);
        if (MapUtils.isEmpty(beans)) {
            return;
        }
        beans.values().forEach(bean -> STRATEGY_MAP.put(bean.type(), bean));
    }

    /** * Get the policy instance **@paramType Reward type *@return* /
    public static RewardSendStrategy getStrategyInstance(String type) {
        log.info("Policy Instance [{}]", STRATEGY_MAP);
        returnSTRATEGY_MAP.get(type); }}Copy the code

How callers use:

@ContextConfiguration(locations = {"classpath:spring/spring-dao.xml","classpath:spring/spring-service.xml"})
@RunWith(value = SpringJUnit4ClassRunner.class)
public class StrategyTest {

    @Test
    public void test(a){
        String code = RewardTypeEnum.CASH.getCode();
        RewardSendStrategy strategyInstance = RewardSendStrategyFactory2.getStrategyInstance(code);

        strategyInstance.sendReward(12L); }}Copy the code
Scenario three uses the Spring framework’s InitializingBean extension point

Specific strategies to achieve org. Springframework. Beans. Factory. InitializingBean interface. Once you hand the class over to Spring to manage and implement the interface, Spring automatically executes the afterPropertiesSet() method when it initializes the bean, which we put into a Map. Here I show only one specific strategy, the rest is similar, the specific code is as follows:

@Slf4j
@Service
public class PointRewardSendStrategy implements RewardSendStrategy.InitializingBean {
    @Override
    public String type(a) {
        return RewardTypeEnum.POINT.getCode();
    }

    @Override
    public boolean isTypeMatch(String type) {
        return Objects.equals(type, type());
    }

    @Override
    public void sendReward(Long memberId) {
        log.info("Send bonus points to [{}]", memberId);
    }

    @Override
    public void afterPropertiesSet(a) throws Exception {
        RewardSendStrategyFactory.registerStrategyInstance(type(), this); }}Copy the code

If every specific policy class implements the InitializingBean interface, then the afterPropertiesSet() method overridden is clearly a duplicate code block. (Otherwise how can I write the next hydrology)

The corresponding Context class code is modified as follows:

@Slf4j
@Component
public class RewardSendStrategyFactory {

    /** * Save the policy set */
    private final static Map<String, RewardSendStrategy> STRATEGY_MAP = new ConcurrentHashMap<>(16);

    /** * Add policy instance **@param type
     * @param strategy
     */
    public static void registerStrategyInstance(String type, RewardSendStrategy strategy) {
        STRATEGY_MAP.put(type, strategy);
    }

    /** * Get policy instance *@param type
     * @return* /
    public static RewardSendStrategy getStrategyInstance(String type) {
        log.info("Policy Instance [{}]", STRATEGY_MAP);
        returnSTRATEGY_MAP.get(type); }}Copy the code

Call as shown in the unit test:

  @Test
    public void test(a) {
        String rewardType = RewardTypeEnum.POINT.getCode();
        RewardSendStrategy strategy = RewardSendStrategyFactory.getStrategyInstance(rewardType);
        strategy.sendReward(12L);
    }
Copy the code
Other options

There are many other solutions, such as the annotation-based form mentioned in the previous article, where the Spring container starts up and scans the specified annotations. This is up to you, as long as it meets the standards of good code and does not have bugs, it is a great solution.

conclusion

A thousand Hamlets for a thousand people. Everyone has an opinion about everything. A thousand people might have a thousand ideas. Something seems to be a bad thing, but if you look at it from a different Angle, you may get a different idea.

Good and evil are mutually identified with each other, which is culture.