前言
前端时间重构项目,于是......没错,我又想吐槽了,重构真的比开发新功能累的多,首先要去理解原来的代码逻辑,然后才能动手,更重要的是还得保证你重构的代码不能错,最重要的是原来的屎山代码......原来的项目很少有运用设计模式,今天借着重构经历,给大家介绍一个运用很广泛的设计模式 —— 策略模式,希望对大家有帮助。
策略模式简介
在策略模式 Strategy Pattern
中,一个类的行为或其算法可以在运行时更改。我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的上下文对象。策略对象改变上下文对象的执行算法。通常在面对符合多个行为的 if/else、switch
语句时,我们可以考虑使用策略模式来重构。
简单来说就是你的 if/else、switch
里面全都是干的相同的一件事情,只不过具体干这件事的过程略微有差异,那么这时候就可以考虑使用策略模式来代替。
策略模式改造创建虚拟账户
场景
项目有个业务是根据用户选择的还款渠道创建属于他的虚拟账户,先来看下原来的代码
switch (channel) {
case FASPAY:
//...创建虚拟账户
break;
case FASPAY_V2:
//...创建虚拟账户
break;
case BNI:
//...创建虚拟账户
break;
case INSTAMONEY:
//...创建虚拟账户
break;
case INSTAMONEY_V2:
//...创建虚拟账户
break;
case BCA:
//...创建虚拟账户
break;
default:
break;
}
复制代码
可以看到如果以后再接入其他渠道还得再加 case
代码块,(其实如果原来的代码真的把 generateVirtualAccount()
封装好,公共代码抽取好的话我觉得问题也不大,但是......你懂得)可以看到所有的 case
代码块都是创建虚拟账户,只不过不同的渠道创建的细节略微有差异。所以我们可以使用策略模式将其改造,如果你不明白策略模式相比 switch
和 if/else
好在哪,可以直接跳转 策略模式的优势。
定义策略接口及其实现
首先需要一个顶层策略接口 VirtualAccountGenerateStrategy
/** 虚拟账户生成策略 */
public interface VirtualAccountGenerateStrategy {
/** 是否支持当前渠道 */
boolean support(DepositChannel channel);
/** 生成方法 */
VirtualAccount generate(VirtualAccount virtualAccount);
}
复制代码
下面是针对不同渠道定义的具体策略类,实现 VirtualAccountGenerateStrategy
接口重写 gernerate()、support()
方法即可
BcaVirtualAccountGenerateStrategy
,BCA
渠道生成策略FaspayVirtualAccountGenerateStrategy
,FASPAY
渠道生成策略InstamoneyVirtualAccountGenerateStrategy
,INSTAMONEY
渠道生成策略InstamoneyV2VirtualAccountGenerateStrategy
,INSTAMONEY_V2
渠道生成策略BniVirtualAccountGenerateStrategy
,BNI
渠道生成策略
以 INSTAMONEY
渠道为例,观察其源码
/** Instamoney V1 生成虚拟账户策略 */
@Component
public class InstamoneyVirtualAccountGenerateStrategy implements VirtualAccountGenerateStrategy {
@Override
public boolean support(DepositChannel channel) {
return channel == DepositChannel.INSTAMONEY;
}
@Override
public VirtualAccount generate(VirtualAccount virtualAccount) {
//... todo 发送 HTTP 请求调用第三方
virtualAccount.setAccountNumber(response.getAccountNumber());
virtualAccount.setUniqueId(response.getId());
return virtualAccount;
}
}
复制代码
这里发送 HTTP 调用第三方的处理业务代码很多,观察到由于对于 INSTAMONEY、INSTAMONEY_V2
来说它们都需要发送 HTTP 请求调用第三方,这是一块公共代码,虽然处理逻辑很复杂,但只是入参的 key、secret
不同,所以我们可以将其抽象一个公共代码块共用
引入抽象策略
定义一个 AbstractInstamoneyVirtualAccountGenerateStrategy
实现 VirtualAccountGenerateStrategy
接口,将 INSTAMONEY、INSTAMONEY_V2
两种策略类继承该抽象策略,公共代码提取到父类中。
public abstract class AbstractInstamoneyVirtualAccountGenerateStrategy implements VirtualAccountGenerateStrategy {
@Autowired protected VirtualAccountService accountService;
@Autowired protected InstamoneyProperties properties;//注意,父类里面不能用 private 修饰,否则子类用不了
@Autowired protected OkHttpClient client;
@Override
public VirtualAccount generate(VirtualAccount virtualAccount) {
accountService.createVirtualAccount(actualGenerate(virtualAccount));
return virtualAccount;
}
/** 调用 Instamoney 创建虚拟账户,由子类实现 */
public abstract VirtualAccount actualGenerate(VirtualAccount virtualAccount);
/**
* 根据不同版本,调用第三方创建虚拟账户,提供给子类调用
*
* @param url 第三方接口 URL
* @param authorization 第三方接口访问的 authorization
*/
public InstamoneyVirtualAccountResponse callInstamoneyCreateVirtualAccount(VirtualAccount virtualAccount, String url, String authorization) {
//... todo 发送 Http 请求调用第三方
return response;
}
}
复制代码
然后 INSTAMONEY、INSTAMONEY_V2
两种渠道的策略就可以继承该抽象类共用公共代码。如果对于 INSTAMONEY
的抽象策略和其他策略还有可抽取的代码,我们也可以继续往上抽象一个策略类出来。将封装、继承、多态发挥到极致!
将策略添加到上下文
仿照 Spring
的策略模式,定义 VirtualAccountStrategyComposite
当做策略上下文,其源码
@Component
@Slf4j
public class VirtualAccountStrategyComposite {
//存放所有策略
private static final Map<DepositChannel, VirtualAccountGenerateStrategy> STRATEGY_HOLDER = new ConcurrentHashMap<>();
public static void addStrategy(DepositChannel channel, VirtualAccountGenerateStrategy strategy) {
STRATEGY_HOLDER.put(channel, strategy);
}
/** 根据不同的 DepositChannel 调用不同生成策略 */
public VirtualAccount generate(VirtualAccount virtualAccount) {
DepositChannel channel = virtualAccount.getChannel();
VirtualAccountGenerateStrategy strategy = STRATEGY_HOLDER.get(channel);
if (Objects.isNull(strategy) || !strategy.support(channel)) {
log.error("暂无该方式: {} 的虚拟账户生成策略", channel);
throw new ClientException("暂无该方式的生成策略");
}
return strategy.generate(virtualAccount);
}
}
复制代码
使用 @PostConstruct
添加策略到 VirtualAccountStrategyComposite.STRATEGY_HOLDER
@PostConstruct
public void init(){
VirtualAccountStrategyComposite.addStrategy(DepositChannel.INSTAMONEY, this);
}
复制代码
使用 VirtualAccountStrategyComposite
在业务代码中直接注入 VirtualAccountStrategyComposite
使用即可
@Autowired private VirtualAccountStrategyComposite virtualAccountStrategyComposite;
/** 获取用户虚拟账户,如果没有则创建一个 */
public VirtualAccountResponse virtualAccount(DepositChannel channel,AccountType type, long customerId) {
VirtualAccount virtualAccount = findVirtualAccount(customerId, channel, type);
if (Objects.isNull(virtualAccount)) {
virtualAccount = virtualAccountStrategyComposite.generate(VirtualAccount.builder().channel(channel).bankCode(depositMethod).type(type).customerId(customerId).build());
}
return virtualAccount.toDto();
}
复制代码
上述步骤已经基本完成了使用策略模式改造虚拟账户的创建,代码可读性大大提高,方法复杂度也大大降低,但你可能并没有发现这里隐藏着一个巨大的问题:事务失效。
解决策略模式事务失效
失效原因
大家应该都知道 Spring
事务是基于代理实现的,当 Service
类中存在 @Transactional
注解时,注入到 Spring
容器的其实是一个代理对象。Spring
对这个代理对象添加了事务支持,只有调用 @Transactional
注解的方法是代理对象时,事务才会生效,而上面我们在 @PostConstruct
注解的 init()
方法中使用
VirtualAccountStrategyComposite.addStrategy(DepositChannel.INSTAMONEY, this);
复制代码
这里的 this
并不是代理对象,所以在策略类中使用 @Transactional
事务将不会生效。当然解决这个问题也很简单,既然放进策略的不是代理对象,那我们把代理对象放进去就可以了。
拿到代理对象
参考 Spring
官方文档,我们可以把策略类实现 BeanNameAware
接口,此接口是一个通知接口,当 Bean
工厂创建代理对象完成之后会调用这个接口的 setBeanName()
方法,我们可以在这个方法中拿到代理对象的名字,再使用 ApplicationContext
根据 Bean
的名字拿到容器中真正的代理对象。
将 InstamoneyVirtualAccountGenerateStrategy
实现 BeanNameAware
接口,重写 setBeanName()
@Override
public void setBeanName(@NotNull String name) {
VirtualAccountStrategyComposite.addStrategy(DepositChannel.INSTAMONEY, name);//将 Bean 的名字放入集合
}
复制代码
这样在项目启动之后,VirtualAccountStrategyComposite.STRATEGY_HOLDER
中就存储了所有策略 Bean
的名字,然后再用 ApplicationContext
根据名称从容器中拿代理对象即可。再参考 Spring
官网我们可以监听 ContextRefreshedEvent
事件来拿到 ApplicationContext
实例,源码:
@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);
}
/**
* 监听Spring容器初始化事件,拿到 ApplicationContext
*/
@EventListener(ContextRefreshedEvent.class)
public void registerRequestHandleBeanMethod(ContextRefreshedEvent event) {
CONTEXT = event.getApplicationContext();
}
/**
* 根据不同的 DepositChannel 调用不同生成策略
*/
public VirtualAccount generate(VirtualAccount virtualAccount) {
DepositChannel channel = virtualAccount.getChannel();
String strategyName = STRATEGY_HOLDER.get(channel);
if (Objects.isNull(strategyName)) {
log.error("暂无该方式: {} 的虚拟账户生成策略", channel);
throw new ClientException("暂无该方式的生成策略");
}
//拿到 Spring 容器中的代理对象
VirtualAccountGenerateStrategy strategy = CONTEXT.getBean(strategyName, VirtualAccountGenerateStrategy.class);
if (strategy.support(channel)) {
return strategy.generate(virtualAccount); //如果策略支持就执行
}
return null;
}
}
复制代码
奇怪的现象
起初我是直接使用实现 BeanNameAware
的方式拿到代理对象,但这样也可以看出每次执行策略都需要执行一遍下面这行代码来从 Spring
容器获取策略对象
VirtualAccountGenerateStrategy strategy = CONTEXT.getBean(strategyName, VirtualAccountGenerateStrategy.class);
复制代码
但我的领导总觉得这样不好。后来他误解了别人的意思,给我推荐了 @PostConstruct
才出现了上面事务失效的一幕。于是本着好奇心我稍微尝试了一下,并且打印出 @PostConstruct
注解的方法里面的 this
和从 Spring
容器拿到的代理对象。
log.info("PostConstruct-Bean:"+this);//InstamoneyVirtualAccountGenerateStrategy@7b7bfa82
log.info("Spring-Bean:"+bean); //InstamoneyVirtualAccountGenerateStrategy@7b7bfa82
复制代码
结果惊人的发现冒号后面的东西是一样的,我误以为打印出的是地址(不知道曾经看了谁的视频或者问题被误导至今......),还疑惑了很久,既然打印的地址相同,说明应该是同一个对象,既然都是代理对象为什么一种方式事务生效,一种方式事务失效呢?后来看 Object.toStirng()
源码才发现这打印的结果其实主要是对象的 hashcode
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
复制代码
于是我又尝试用 ==
来比较 this
和代理对象,果然他们并不是同一个地址
log.info("==:"+(this == bean)); //false
log.info("==:"+(this.equals(bean));//false
复制代码
其实这似乎是我们刚毕业的时候面试会被问的问题,还记得那句话么?两个对象 equals
相同,hashcode
一定相同,反之两个对象的 hashcode
相同, equals
不一定相同。 那么问题来了,为什么代理对象和原对象有相同的 hashcode
?
为什么代理对象和原对象 hashcode 相同
这个问题就得清楚 Spring
的生命周期以及代理对象的创建过程了,等以后更吧......
策略模式的优势
我相信你可能会有疑问,使用策略模式真的比 if/else、switch
要好吗?因为突然的思维转变可能会让你觉得,使用策略模式之后会多了很多类甚至有可能会多写很多代码。从应用层面看,好像由传统的 if/else
改为策略模式似乎作用不大,反而增加了类的个数,策略模式就是变相的 if/else
而已。
然而我们更应该要从扩展性和设计原则上去看这个问题。以刚刚重构的为例,如果说以后新来了一种 channel
,我们又得去改 switch
代码,首先这违背了开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
其次我们平时都是写的业务代码,假如以后这个策略被封装在 jar
包里,或者以后让你写框架,我用策略模式只要写一个类实现策略接口即可。如果还是用 if/else
去写,这时候怎么去扩展呢?要扩展功能,自定义逻辑,总不能去改框架源码吧!参考 Spring
参数解析器策略,扩展性就很强,想要自己定义参数解析器,直接实现 HandlerMethodArgumentResolver
即可 SpringMVC 参数解析器 和 Spring 类型转换
最后更重要的是,别管对不对,反正用了设计模式你有没有感觉看起来就很高大上有逼格?这特么才会让领导和同事觉得你牛逼啊......
结语
本篇文章简单了使用了策略模式,相对于 Spring
框架中的策略模式还相差甚远,包括前置处理器、后置处理器等这里都没有体现,不过也算是完成了重构的初步尝试,后面有涉及的话再更新。大家有兴趣也可以参考 Spring
源码中对于策略模式的应用,如 InstantiationStrategy
、HandlerMethodArgumentResolver
等。