「论道架构师」优雅解决历史代码中的新需求
事件起因
6月中旬,可爱的产品大大给我提了一个临时需求,需要我对商品创建/更新业务中由开放平台对接而来的请求做一个Check,如果符合要求,则再做一段稍微复杂的逻辑处理
。
这种Easy程度的需求怎么拦得住我,不到半天我就Coding,Push一气呵成,正当我准备点一杯喜茶开始摸鱼的时候,我却收到了一封邮件。
邮件里有一堆的汉字和英文,但有几个字赫然在目:
您的代码已被驳回。
当我经历了茫然、震惊、不敢相信、最后无奈接受的情绪转变后,问了评审的同事,为什么要驳回我的代码,他说:“历史代码一般业务都很完整(跟屎山一样了...),那如果有新的需求不得不依赖它的话,怎么
做才是最佳方案,让代码有更好的拓展性,你有想过吗?”。
我肯定是没有想的,于是乎,我怀着些许愧疚的心情,找到了架构师,希望他能为我指点迷津。
找一个看起来合适的位置塞进去
亮架构:Kerwin,这段代码是不是偷懒了?
try {
// 忽略历史业务代码,以下为新增内容
} catch (Exception) {
// TODO
} finally {
SkuMainBean retVal = skuMainBridgeService.updateSkuBridgeMainBean(skuMainBean);
if(retVal != null){
// 商品创建/修改异步处理逻辑
SimpleThreadPool.executeRunnable(SimpleThreadPool.ThreadPoolName.openSkuHandle, () -> {
skuOperateBusinessService.checkOpenSkuReview(retVal);
});
}
}
我(虽然我觉得不妥,但还是强装镇定):没偷懒啊,你看这块业务代码既没有影响原功能,又用线程池的方式异步处理,不会影响整体接口效率,而且还把复杂逻辑都封装到了Business
层里,这还叫偷懒吗?
亮架构:你觉得这个商品创建/修改流程重要吗?是不是咱们的最核心的流程?下次产品再提新的需求,继续 if 然后叠罗汉吗
?我咋记得你说过你最讨厌在代码里看到 if
呢?
我(小声):我讨厌看到别人的 if,但是自己的还是可以接受的...
亮架构(气笑):不跟你耍贫嘴了,一起想想怎么改吧。
PS:【找一个看起来合适的位置塞进去】这种方式是我们使用最频繁,影响面相对较小,开发效率最高的方式了,但它带来的问题就是后期不好维护,而且随着需求变多,它就会变得和叠罗汉一样,本来一个很简单的方法函数,会变成百上千行的 “屎山”,因此需要酌情使用。
优先校验终止
我(开始思考):如果需求是不满足某种情况即可终止执行,那这种情况可太简单了,就不絮叨了。
亮架构:其实还是有一点可说的,比如你需要在不满足时返回标识符结果加细节原因,你怎么处理?
我:直接定义一个字符串然后返回,后续判断字符串是否为NULL即可。
亮架构:如果就是失败了,且原因也为NULL或空字符串呢?其实我们利用泛型
有更优雅的解决方案,比如这样定义一个元组:
public class ValueMsgReturn<A, B> {
/** 结果 **/
private final A value;
/** 原因 **/
private final B msg;
public ValueMsgReturn(A value, B msg) {
this.value = value;
this.msg = msg;
}
// 省略Get方法
}
这样做的好处是,通用,简单,不必定义重复的对象,你自己在代码中试试就能明白它有多香,整体代码就如下所示:
// 省略干扰代码
ValueMsgReturn<Boolean, String> check = check();
if (check.getValue()) {
return check.getValue();
}
PS:此种情况较为简单,但仍然有技巧优化代码,详情请见历史文章:
简单观察者模式
我(继续思考):你刚那种情况太简单了,回归正题,咱们这个需求可以使用观察者模式解耦
啊!
亮架构(犹豫道):不是不可以,但你想一下我们需要改动哪些代码吧。
我:观察者的核心即通知方
+ 处理方
,如果我们使用JDK自带的观察者模式的话,改动如下:
- 需要将历史代码中的类继承
Observable
类 - 新的处理方法基于单一原则抽象成单独的类,实现
Observer
接口 - 在类初始化时把二者构建好通知关系
亮架构:如果一段逻辑在设计之初就采用观察者模式的话,那还不错,但历史代码则不适合,因为它一个类里面包含大量的其他方法
,如果未来需求中有第二种需要通知的情况,代码就会更难维护,毕竟JDK观察者模式是需要继承Observable
类的,当然了,作为一个备选方案也不是不行。
PS:以上描述的JDK观察者模式对应的是JDK1.8版本,关于观察者模式的详情,请见历史文章
AOP
我(突然想起来):亮架构,你说用AOP
来处理合适吗?
亮架构:一般情况下我们用AOP来做什么动作呢?
我:我的话,一般会用作权限处理、日志打印、缓存同步、特殊场景计数等等。
亮架构:是的,你想象一下如果我们把这些业务逻辑都堆在切面里会是什么样子?一个切点还好,两个呢,十个呢?大家拿到新项目的时候都会参考前人的代码风格,如果你开了一个坏的头,其他人就会跟着做同样的事,很快代码就会变成如同蜘蛛网一般,所以这种方式一定是要杜绝的。
MQ 解耦
我(突然想起来):对了,咱们的商品新建/修改都会有MQ的,我只用监听MQ然后做业务处理
就好了。
亮架构:这个肯定是可行的,就是有点杀鸡焉用宰牛刀的感觉,毕竟我们需要处理的情况只是MQ中的一小部分,而且万一历史代码没有发送MQ怎么办呢?
Spring Event
亮架构:你有了解过Spring Event
吗?
我:以前研究过,确实用在这里还蛮合适的。
PS:Spring Event是Spring体系中的事件通知机制,其原理可以理解为Spring实现的观察者模式。
注:上文中的简单观察者模式指的是JDK(1.8)实现的观察者模式。
// 以下为Demo代码
@RestController
public class EventRequest implements ApplicationContextAware {
private ApplicationContext appContext;
@RequestMapping("/testEvent")
public String testEventSave(String name) {
appContext.publishEvent(new User(this, name));
return "ok";
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
appContext = applicationContext;
}
}
// 监听者
@Component
public class WebEventListener {
/**
* 仅监听字段值为 foo 时,类为 User.class 时
*/
@EventListener(classes = User.class, condition = "#event.name == 'foo'")
public void listen(User event){
// TODO
}
/**
* 监听 User.class 情况
*/
@EventListener(classes = User.class)
public void listen1(User event){
// TODO
}
}
亮架构:是的,这个Demo就很能反映它的优势之处了
- 我们可以在单一方法内Publish多个事件,互不干扰
- 监听者可以基于表达式进行基本的过滤
- 一个事件可以被重复监听
我:是的,而且它还可以支持异步事件处理!
亮架构(停顿了一下):你觉得支持异步是它独特的优势吗?哈哈哈,即使是同步监听到事件,你只要用线程池异步处理就好了。能够天然异步化,只是锦上添花的东西,不要弄混了哦。当然了,每种技术和特性都有其独特的使用场景,在使用的时候需要注意它的特殊情况,比如:
- 业务上是否允许异步处理(即使是延迟了比较久的时间)
- 能否完全相信事件通知里面的参数,是否需要反查等等。
还有别的方式吗
我(开心):如果我用Spring Event的话,我只需要稍微改动一下就好了,代码的拓展性,可维护性一下子就上来了,不过刚咱们聊了那么多方式方法,怎么感觉全是观察者模式
啊?
亮架构:是的,无论是JDK的还是Spring,亦或是AOP、MQ,这些统统都是观察者模式的思想,毕竟观察者模式的特点就是解耦
。
我:难道不能用别的设计模式思想吗?
亮架构:当然可以,就是改动可能略大一点,毕竟这个类都快几千行了,还是尽量少加东西了。
我:比如呢,可以用什么其他的方式?
亮架构:额...你既然想听的话,可以这样,回顾一下你最初的代码:
finally {
SkuMainBean retVal = skuMainBridgeService.updateSkuBridgeMainBean(skuMainBean);
if(retVal != null){
// 商品创建/修改异步处理逻辑
SimpleThreadPool.executeRunnable(SimpleThreadPool.ThreadPoolName.openSkuHandle, () -> {
skuOperateBusinessService.checkOpenSkuReview(retVal);
});
}
}
在这个业务方法里处理的肯定是skuMainBean
对象,因为整个方法都是在操作它,那我们完全可以抽象出一个个策略类,然后利用工厂来处理,比如改成这样:
// 修改后代码
finally {
skuMainBeanFactory.checkAndHandle(skuMainBean);
}
// 工厂方法
public void checkAndHandle (SkuMainBean skuMainBean) {
for (策略集合: 策略) {
if (check(skuMainBean)) {
// TODO
}
}
}
亮架构:你看这样是不是也具有很好的拓展性?
我(兴奋):是的,我突然感觉这种方式和SpringEvent有异曲同工之妙!
亮架构(笑了笑):孺子可教也,这种策略+工厂的方式是基于接口编程,通过check方法判断是否需要处理,而SpringEvent说白了是通过事件的传播,即方法直接调用来判断是否需要处理,本质都是一样的,那你知道未来的新需求你该怎么写了吗?
我(兴奋):我知道了,要写可拓展性的代码,像我今天改的这种代码就不行,太垃圾了!
亮架构(摇了摇头,起身走了):Kerwin,你错了,你今天改的历史代码在当时可以说是最佳实践了,只是因为你遇到了之前的设计者未考虑到的问题而已。我们讲设计模式、讲七大原则,讲不要过度设计,就是为了你现在出现的情况,我们在编码过程中可能会遇到千奇百怪的代码,我们可以抱怨,可以吐槽,但记住,不要为了某些需求就把本来漂亮的代码变成屎山。所以你需要去学习编程的思想,学习设计的思想。
我(大声):那,架构师!如果有一段代码已经烂到不能再烂了呢!
“那就把它重构了!然后把作者的名字记下来,狠狠的吐槽他!🤪”
最后
回顾全文做一个总结,如果你的需求是允许前置校验返回的,那么毫不犹豫的CheckAndReturn即可!但是,如果你的需求和我一样,那么推荐以下几种方案:
- 利用MQ解耦
- 利用SpringEvent解耦
- 自行根据当前需求和未来可能的需求考虑是否需要策略类
- 终极方案:真正理解编程的七大原则及常用的设计模式、随机应变即可
那么请允许我推荐一下之前的文章:设计模式总篇:从为什么需要原则到实际落地
如果你觉得这篇内容对你有帮助的话:
- 当然要点赞支持一下啦~
- 另外,可以搜索并关注公众号「是Kerwin啊」,一起在技术的路上走下去吧~ 😋
转载自:https://juejin.cn/post/6987538947400269861