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
BcaVirtualAccountGenerateStrategy
,BCA
Channel generation strategyFaspayVirtualAccountGenerateStrategy
,FASPAY
Channel generation strategyInstamoneyVirtualAccountGenerateStrategy
,INSTAMONEY
Channel generation strategyInstamoneyV2VirtualAccountGenerateStrategy
,INSTAMONEY_V2
Channel generation strategyBniVirtualAccountGenerateStrategy
,BNI
Channel 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.