preface

Front-end time reconstruction project, so…… Yes, I would like to make fun of it, refactoring is really more tired than developing new features, first to understand the original code logic, and then to start, more important is to ensure that you refactoring the code can not be wrong, the most important is the original shitshan code…… The original project seldom uses the design pattern. Today, through the reconstruction experience, I will introduce a widely used design pattern — the strategic pattern, hoping to be helpful to you.

Introduction to The Policy Pattern

In the Strategy Pattern, the behavior of a class or its algorithm can be changed at run time. We create objects that represent various policies and a context object whose behavior changes as the policy object changes. The policy object changes the execution algorithm of the context object. In general, we can consider using the policy pattern for refactoring when faced with if/else and switch statements that match multiple behaviors.

Your if/else and switch are all doing the same thing, but the specific process of doing the same thing is slightly different, so you can consider using strategy mode instead.

Policy mode Change Create a virtual account

scenario

One business of the project is to create virtual accounts belonging to users according to the repayment channels they choose. Let’s take a look at the original code first

switch (channel) { case FASPAY: //... Create a virtual account break. case FASPAY_V2: //... Create a virtual account break. case BNI: //... Create a virtual account break. case INSTAMONEY: //... Create a virtual account break. case INSTAMONEY_V2: //... Create a virtual account break. case BCA: //... Create a virtual account break. default: break; }Copy the code

If generateVirtualAccount() is encapsulated in the original code, I don’t think it’s a problem, but…… You can see that all the case blocks create virtual accounts, but the details vary slightly from channel to channel. So we can modify it using policy mode, and if you don’t understand why policy mode is better than switch and if/else, you can jump to the advantage of policy mode.

Define the policy interface and its implementation

You first need to a top-level VirtualAccountGenerateStrategy strategy interface

Virtual account creation strategy * * * / public interface VirtualAccountGenerateStrategy {/ * * * / whether to support the current channel Boolean support (DepositChannel channel); /** generate VirtualAccount generate(VirtualAccount VirtualAccount); }Copy the code

Below is for the concrete strategy of the definition of different channels, implementing rewrite gernerate VirtualAccountGenerateStrategy interface (), the support () method

  • BcaVirtualAccountGenerateStrategyBCAChannel generation strategy
  • FaspayVirtualAccountGenerateStrategyFASPAYChannel generation strategy
  • InstamoneyVirtualAccountGenerateStrategyINSTAMONEYChannel generation strategy
  • InstamoneyV2VirtualAccountGenerateStrategyINSTAMONEY_V2Channel generation strategy
  • BniVirtualAccountGenerateStrategyBNIChannel generation strategy

Take INSTAMONEY channel as an example to observe its source code

/ * * Instamoney V1 generated virtual account strategy * / @ Component public class InstamoneyVirtualAccountGenerateStrategy implements VirtualAccountGenerateStrategy { @Override public boolean support(DepositChannel channel) { return channel == DepositChannel.INSTAMONEY; } @Override public VirtualAccount generate(VirtualAccount virtualAccount) { //... Todo sending HTTP requests call third-party virtualAccount. SetAccountNumber (response) getAccountNumber ()); virtualAccount.setUniqueId(response.getId()); return virtualAccount; }}Copy the code

Here, there are many processing codes for sending HTTP to call a third party. It is observed that INSTAMONEY and INSTAMONEY_V2 both need to send HTTP requests to call a third party. This is a common code, although the processing logic is very complicated. However, only the key and secret of the input parameter are different, so we can abstract it into a common code block

Introducing an abstraction strategy

Define a AbstractInstamoneyVirtualAccountGenerateStrategy VirtualAccountGenerateStrategy interface, The INSTAMONEY and INSTAMONEY_V2 policy classes inherit the abstract policy, and the common code is extracted into the parent class.

public abstract class AbstractInstamoneyVirtualAccountGenerateStrategy implements VirtualAccountGenerateStrategy { @Autowired protected VirtualAccountService accountService; @Autowired protected InstamoneyProperties properties; @autoWired protected OkHttpClient client; @Override public VirtualAccount generate(VirtualAccount virtualAccount) { accountService.createVirtualAccount(actualGenerate(virtualAccount)); return virtualAccount; } /** Public abstract VirtualAccount actualGenerate(VirtualAccount VirtualAccount); */ public abstract VirtualAccount actualGenerate(VirtualAccount VirtualAccount); /** * Call a third party to create a virtual account. Provided to subclasses to call * * @param URL url of third-party interfaces * @param authorization Authorization of third-party interface access */ public InstamoneyVirtualAccountResponse callInstamoneyCreateVirtualAccount(VirtualAccount virtualAccount, String url, String authorization) { //... Todo sends an Http request to invoke a third party return response; }}Copy the code

INSTAMONEY and INSTAMONEY_V2 channel policies can then inherit this abstract class and share common code. If there is more code available for INSTAMONEY’s abstract policy and other policies, we can also go up and abstract a policy class. Encapsulation, inheritance, polymorphism to the extreme!

Adds the policy to the context

Follows the strategy pattern of Spring, define VirtualAccountStrategyComposite as policy context, its source code

@ Component @ Slf4j public class VirtualAccountStrategyComposite {/ / storage strategy all private static final Map < DepositChannel, VirtualAccountGenerateStrategy> STRATEGY_HOLDER = new ConcurrentHashMap<>(); public static void addStrategy(DepositChannel channel, VirtualAccountGenerateStrategy strategy) { STRATEGY_HOLDER.put(channel, strategy); Public VirtualAccount generate(VirtualAccount VirtualAccount) {DepositChannel generate(VirtualAccount VirtualAccount) {DepositChannel generate(VirtualAccount VirtualAccount) channel = virtualAccount.getChannel(); VirtualAccountGenerateStrategy strategy = STRATEGY_HOLDER.get(channel); if (Objects.isNull(strategy) || ! Strategy.support (channel)) {log.error(" no virtual account generation policy for this mode: {} ", channel); Throw new ClientException(" no generation policy for this mode yet "); } return strategy.generate(virtualAccount); }}Copy the code

Using the @ PostConstruct strategy is added to the VirtualAccountStrategyComposite STRATEGY_HOLDER

@PostConstruct
public void init(){
  VirtualAccountStrategyComposite.addStrategy(DepositChannel.INSTAMONEY, this);
}
Copy the code

Using VirtualAccountStrategyComposite

In the business code directly into use VirtualAccountStrategyComposite can

@Autowired private VirtualAccountStrategyComposite virtualAccountStrategyComposite; Public VirtualAccountResponse virtualAccount public VirtualAccountResponse virtualAccount public VirtualAccountResponse virtualAccount public VirtualAccountResponse virtualAccount long customerId) { VirtualAccount virtualAccount = findVirtualAccount(customerId, channel, type); if (Objects.isNull(virtualAccount)) { virtualAccount = virtualAccountStrategyComposite.generate(VirtualAccount.builder().channel(channel).bankCode(depositMethod).type(type).cu stomerId(customerId).build()); } return virtualAccount.toDto(); }Copy the code

The above steps have basically completed the creation of virtual accounts using the policy pattern. The code is much more readable and the method complexity is much reduced, but you may not realize that there is a huge problem hidden here: transaction failure.

Resolve policy mode transaction invalidation

The failure reason

Spring transactions are proxy-based. When a Service class has the @Transactional annotation, what is injected into the Spring container is actually a proxy object. Spring adds transaction support to this proxy object. Transactions only work if the @Transactional annotation method is a proxy object, as we used in the init() method of the @PostConstruct annotation

VirtualAccountStrategyComposite.addStrategy(DepositChannel.INSTAMONEY, this);
Copy the code

This is not a proxy object, so using a @Transactional transaction in a policy class will not work. Of course, the solution to this problem is simple, since it is not the proxy object that is put into the policy, we can put the proxy object into it.

Get a proxy

According to the Spring documentation, we can implement the policy class to the BeanNameAware interface, which is a notification interface. The setBeanName() method of the interface is called when the Bean factory has created the proxy object. Then use the ApplicationContext to get the actual proxy object in the container based on the Bean name.

Will InstamoneyVirtualAccountGenerateStrategy realize BeanNameAware interface, rewrite setBeanName ()

@Override public void setBeanName(@NotNull String name) { VirtualAccountStrategyComposite.addStrategy(DepositChannel.INSTAMONEY, name); // Put the Bean name into the collection}Copy the code

. So, after the project starts VirtualAccountStrategyComposite STRATEGY_HOLDER store all strategy in the name of the Bean, next reoccupy ApplicationContext by name from the container take proxy objects. We can listen to ContextRefreshedEvent events to get an ApplicationContext instance.

@Component @Slf4j public class VirtualAccountStrategyComposite { private static ApplicationContext CONTEXT; private static final Map<DepositChannel, String> STRATEGY_HOLDER = new ConcurrentHashMap<>(); public static void addStrategy(DepositChannel channel, String name) { STRATEGY_HOLDER.put(channel, name); } /** * listen for Spring container initialization events, Get ApplicationContext * / @ EventListener (ContextRefreshedEvent. Class) public void registerRequestHandleBeanMethod(ContextRefreshedEvent event) { CONTEXT = event.getApplicationContext(); } public VirtualAccount generate(VirtualAccount VirtualAccount) {DepositChannel generate(VirtualAccount VirtualAccount) {DepositChannel generate(VirtualAccount VirtualAccount) {DepositChannel generate(VirtualAccount VirtualAccount) channel = virtualAccount.getChannel(); String strategyName = STRATEGY_HOLDER.get(channel); If (objects.isNULL (strategyName)) {log.error(" there is no virtual account generation policy for this mode: {} ", channel); Throw new ClientException(" no generation policy for this mode yet "); } / / proxy objects of the Spring container VirtualAccountGenerateStrategy strategy = CONTEXT. The getBean (strategyName, VirtualAccountGenerateStrategy.class); if (strategy.support(channel)) { return strategy.generate(virtualAccount); } return null; }}Copy the code

Strange phenomenon

At first I used the BeanNameAware implementation directly to get the proxy object, but you can see that each time you execute the policy you need to execute the following line of code to get the policy object from the Spring container

VirtualAccountGenerateStrategy strategy = CONTEXT.getBean(strategyName, VirtualAccountGenerateStrategy.class);
Copy the code

But my boss always thought it was bad. Then he misunderstood what others meant and recommended @Postconstruct to me, which led to the failure of the above transaction. So out of curiosity I tried a little bit and printed out the @PostConstruct annotation method for this and the proxy object I got from the Spring container.

The info (" PostConstruct - Bean: "+ this); / / InstamoneyVirtualAccountGenerateStrategy @ 7 b7bfa82 log. The info (" Spring - Bean: "+ Bean). //InstamoneyVirtualAccountGenerateStrategy@7b7bfa82Copy the code

Surprisingly, I found the same thing after the colon, I mistakenly thought it was the address (I don’t know who I have watched the video or the question was misled so far……). “, also puzzled for a long time, since the printed address is the same, it should be the same object, since they are both proxy objects why one way transaction effect, another way transaction failure? It turns out that the printout is basically the Object’s HashCode

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
Copy the code

So I tried to compare this with the proxy object using ==, and sure enough they are not the same address

Log.info ("== : "+(this == bean)); //false log.info("==:"+(this.equals(bean)); //falseCopy the code

In fact, this seems to be one of the interview questions we were asked when we first graduated, remember that phrase? If two equals objects have the same hashcode, they must have the same hashcode. If two equals objects have the same hashcode, they must not have the same hashcode. So why does the proxy object have the same Hashcode as the original object?

Why is the proxy object the same as the original object hashCode

This is a matter of understanding the Spring lifecycle and proxy object creation process, more on…… later

Advantages of the strategic model

I’m sure you might be wondering, is using policy mode really better than if/else or Switch? Because a sudden shift in thinking might make you feel like you’re going to have a lot more classes and maybe even write a lot more code after using the policy pattern. From the application level, it seems that the change from the traditional if/else mode to the policy mode does not work, but increases the number of classes, the policy mode is just a disguised if/else mode.

However, we should look at it from the perspective of extensibility and design principles. For example, if a new channel is added, we have to change the switch code. First, this violates the open closed principle: a software entity such as classes, modules, and functions should be open for extension and closed for modification.

Secondly, we usually write business code, if this policy is packaged in jar package in the future, or let you write framework, I use the policy mode as long as write a class to implement the policy interface. If I’m still writing if/else, how do I extend that? To expand the function, custom logic, always can’t change the framework source bar! Parser strategy reference Spring parameters, scalability is very strong, want to define the parameters parser, direct implementation HandlerMethodArgumentResolver parser can for SpringMVC parameters and Spring type conversion

And finally, and more importantly, whether it’s right or not, do you ever feel like you’re going to look good or cool in design mode? This will let the leadership and colleagues think you awesome……

conclusion

This article simply uses the policy pattern, which is far from the policy pattern in the Spring framework, including the pre-processor, post-processor, etc., are not reflected here, but it is a preliminary attempt to complete the refactoring, and will be updated later if there is anything involved. We are interested in can also be reference for the application of the strategy pattern in Spring source, such as InstantiationStrategy, HandlerMethodArgumentResolver.

If this article has helped you, please like it and follow it! Your support is my motivation to continue to create!