Use the policy + factory pattern to completely kill if else in the code

Keywords: Java

For business development, the complexity of business logic is inevitable. With the development of business, the requirements will only become more and more complex. In order to consider various situations, many if else will inevitably appear in the code.

Once there are too many if else in the code, it will greatly affect its readability and maintainability.

First, readability. It goes without saying that too much if else code and nesting will make it difficult for code readers to understand what it means. Especially the code without comments.

The second is maintainability, because there are so many if else. When you want to add a new branch, it will be difficult to add, and it is extremely easy to affect other branches.

The author once saw a core payment application that supports online payment functions of many businesses, but each business has a lot of customization requirements, so there is a big lump of if else in many core codes.

When each new business needs to be customized, put your own if at the front of the whole method to ensure that your logic can be executed normally. The consequences of this approach can be imagined.
In fact, there are ways to eliminate if else. Among them, the more typical and widely used is to completely eliminate if else in the code with the help of policy pattern and factory pattern. To be precise, it is to use the ideas of these two design patterns.

This article introduces how to eliminate if else by combining these two design patterns, and how to combine it with the Spring framework, so that readers can immediately apply it to their own projects after reading this article.

This article involves some code, but the author tries to make the content less boring in the form of popular examples and pseudo code.
Disgusting if else

Suppose we want to build a takeout platform and have such needs:

1. In order to promote sales, a store on the takeout platform has set up a variety of member discounts, including 20% discount for super members, 10% discount for ordinary members and no discount for ordinary users.
2. It is hoped that when paying, the user can know which discount strategy the user meets according to the user's membership level, then discount and calculate the payable amount.
3. With the development of business, the new demand requires that exclusive members can enjoy a 70% discount only when the order amount in the store is greater than 30 yuan.
4. Then, there is another abnormal demand. If the user's super member has expired and the expiration time is within one week, the user's single order will be discounted according to the super member, and a strong reminder will be given at the cashier to guide the user to open the member again, and the discount will only be carried out once.

Then, we can see the following pseudo code:

public BigDecimal calPrice(BigDecimal orderPrice, String buyerType) {
    if (Users are exclusive members) {
        if (The order amount is more than 30 yuan) {
            returen 7 Discount price;
        }
    }
    if (The user is a super member) {
        return 8 Discount price;
    }
    if (The user is an ordinary member) {
        if(The user's super member has just expired and has not used a temporary discount){
            Temporary discount usage update();
            returen 8 Discount price;
        }
        return 9 Discount price;
    }
    return original price;
}

The above is a price calculation logic for this demand. The use of pseudo code is so complex. If you really write code, you can imagine the complexity.
In such code, there are many if else, and there are many if else nesting, which is very low in readability and maintainability.

So, how to improve it?

Strategy mode
Next, we try to introduce a policy pattern to improve the maintainability and readability of the code.

First, define an interface:

/**
 * @author mhcoding
 */
public interface UserPayService {
    /**
     * Calculate price payable
     */
    public BigDecimal quote(BigDecimal orderPrice);
}

Then define several policy classes:

/**
 * @author mhcoding
 */
public class ParticularlyVipPayService implements UserPayService {
    @Override
    public BigDecimal quote(BigDecimal orderPrice) {
         if (The consumption amount is more than 30 yuan) {
            return 7 Discount price;
        }
    }
}

public class SuperVipPayService implements UserPayService {
    @Override
    public BigDecimal quote(BigDecimal orderPrice) {
        return 8 Discount price;
    }
}

public class VipPayService implements UserPayService {
    @Override
    public BigDecimal quote(BigDecimal orderPrice) {
        if(The user's super member has just expired and has not used a temporary discount){
            Temporary discount usage update();
            returen 8 Discount price;
        }
        return 9 Discount price;
    }
}

After introducing the strategy, we can calculate the price as follows:

/**
 * @author mhcoding
 */
public class Test {

    public static void main(String[] args) {
        UserPayService strategy = new VipPayService();
        BigDecimal quote = strategy.quote(300);
        System.out.println("The final price of ordinary member products is:" + quote.doubleValue());

        strategy = new SuperVipPayService();
        quote = strategy.quote(300);
        System.out.println("The final price of super member products is:" + quote.doubleValue());
    }
}

The above is an example. You can new the policy classes of different members in the code, and then execute the corresponding price calculation method. Readers can learn about this example and the relevant knowledge of strategic patterns in the article how to explain what strategic patterns are to girlfriends.

However, if it is really used in code, such as in a web project, the above Demo can't be used directly.

First of all, in the web project, these policy classes we created above are managed by Spring. We won't create an instance of new ourselves.

Secondly, in a web project, if you really want to calculate the price, you also need to know the user's membership level in advance. For example, find out the membership level from the database, and then obtain different policy classes according to the level to execute the price calculation method.

Then, if the price is really calculated in a web project, the pseudo code should be as follows:

/**
 * @author mhcoding
 */
public BigDecimal calPrice(BigDecimal orderPrice,User user) {

     String vipType = user.getVipType();

     if (vipType == Exclusive member) {
        //Pseudo code: get the policy object of super member from Spring
        UserPayService strategy = Spring.getBean(ParticularlyVipPayService.class);
        return strategy.quote(orderPrice);
     }

     if (vipType == Super member) {
        UserPayService strategy = Spring.getBean(SuperVipPayService.class);
        return strategy.quote(orderPrice);
     }

     if (vipType == Ordinary member) {
        UserPayService strategy = Spring.getBean(VipPayService.class);
        return strategy.quote(orderPrice);
     }
     return original price;
}

Through the above code, we find that the maintainability and readability of the code seem to be better, but it doesn't seem to reduce if else.

In fact, before< How to explain to your girlfriend what is the strategy model? >In this paper, we introduce the advantages of many strategic patterns. However, there is still a big disadvantage in the use of policy mode:

The client must know all policy classes and decide which policy class to use. This means that the client must understand the differences between these algorithms in order to select the appropriate algorithm class in time.
In other words, although there is no if else when calculating the price, it is inevitable to have some if else when selecting specific strategies.

In addition, in the above pseudo code, we obtain the member's policy object from Spring by pseudo code, so how should the code obtain the corresponding Bean?
Next, let's look at how to solve these problems with the help of Spring and factory pattern.

Factory mode
To facilitate us to obtain various policy classes of UserPayService from Spring, we create a factory class:

/**
 * @author mhcoding
 */
public class UserPayServiceStrategyFactory {

    private static Map<String,UserPayService> services = new ConcurrentHashMap<String,UserPayService>();

    public  static UserPayService getByUserType(String type){
        return services.get(type);
    }

    public static void register(String userType,UserPayService userPayService){
        Assert.notNull(userType,"userType can't be null");
        services.put(userType,userPayService);
    }
}

This UserPayServiceStrategyFactory defines a Map to save all policy class instances, and provides a getByUserType method to directly obtain the corresponding class instances according to the type. There is also a register method, which will be discussed later.

With this factory class, the price calculation code can be greatly optimized:

/**
 * @author mhcoding
 */
public BigDecimal calPrice(BigDecimal orderPrice,User user) {

     String vipType = user.getVipType();
     UserPayService strategy = UserPayServiceStrategyFactory.getByUserType(vipType);
     return strategy.quote(orderPrice);
}

In the above code, you no longer need if else. After you get the vip type of the user, you can call it directly through the getByUserType method of the factory.

Through strategy + factory, our code is optimized to a great extent, which greatly improves readability and maintainability.

However, there is still a problem left above, that is, how is the Map used to save all policy class instances in UserPayServiceStrategyFactory initialized? How are the instance objects of each policy inserted?

Registration of spring beans

Remember the register method provided in the UserPayServiceStrategyFactory we defined earlier? It is used to register policy services.

Next, we'll try to call the register method to register the Bean created by Spring through IOC.

For this requirement, you can borrow the InitializingBean interface provided by Spring. This interface provides the bean with the processing method after property initialization. It only includes the afterpropertieset method. All classes that inherit this interface will execute this method after the property initialization of the bean.

Then, we can slightly modify the previous policy classes:

/**
 * @author mhcoding
 */
@Service
public class ParticularlyVipPayService implements UserPayService,InitializingBean {

    @Override
    public BigDecimal quote(BigDecimal orderPrice) {
         if (The consumption amount is more than 30 yuan) {
            return 7 Discount price;
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        UserPayServiceStrategyFactory.register("ParticularlyVip",this);
    }
}

@Service
public class SuperVipPayService implements UserPayService ,InitializingBean{

    @Override
    public BigDecimal quote(BigDecimal orderPrice) {
        return 8 Discount price;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        UserPayServiceStrategyFactory.register("SuperVip",this);
    }
}

@Service  
public class VipPayService implements UserPayService,InitializingBean {

    @Override
    public BigDecimal quote(BigDecimal orderPrice) {
        if(The user's super member has just expired and has not used a temporary discount){
            Temporary discount usage update();
            returen 8 Discount price;
        }
        return 9 Discount price;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        UserPayServiceStrategyFactory.register("Vip",this);
    }
}

All the implementation classes of policy services need to implement the InitializingBean interface and implement their afterPropertiesSet method in this method.
UserPayServiceStrategyFactory.register.

In this way, when Spring initializes, when VipPayService, SuperVipPayService and particallyvippayservice are created, the Bean will be registered in the UserPayServiceStrategyFactory after the attribute of the Bean is initialized.

In fact, there are some duplicate codes in the above codes. The template method mode can be introduced to further simplify them. It will not be expanded here.

In addition, when calling UserPayServiceStrategyFactory.register, the first parameter needs to pass a string, which can also be optimized here.

For example, you can use enumeration, or customize a getUserType method in each policy class, which can be implemented separately.

summary

In this article, we have improved the readability and maintainability of the code through policy mode, factory mode and Spring's initializing bean, and completely eliminated a lump of if else.

You can try this method immediately. This practice is often used in our daily development, and there are many derived usages, which are also very easy to use. I'll introduce it later if I have the opportunity.

In fact, if readers understand the strategic model and factory model, the strategic model and factory model in the strict sense are not used in this paper.

Firstly, there is no important Context role in the policy mode. Without Context, the combination method is not used, but the factory is used instead.

In addition, the UserPayServiceStrategyFactory only maintains a Map and provides register and get methods, while the factory mode is actually used to help create objects, which is not used here.

Therefore, readers don't have to worry about whether they really use the strategy model and factory model. Moreover, the so-called GOF 23 design patterns are simple code examples no matter which book or blog we read, but many of our daily development are based on frameworks such as Spring, which can't be used directly.

Therefore, for the study of design patterns, it is important to learn their ideas, not code implementation!!

Posted by dabas on Fri, 03 Dec 2021 10:39:14 -0800