「论道架构师」优雅解决历史代码中的新需求

14,118 阅读10分钟

⚠️本文为掘金社区首发签约文章,未获授权禁止转载


事件起因

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自带的观察者模式的话,改动如下:

  1. 需要将历史代码中的类继承Observable
  2. 新的处理方法基于单一原则抽象成单独的类,实现Observer接口
  3. 在类初始化时把二者构建好通知关系

亮架构:如果一段逻辑在设计之初就采用观察者模式的话,那还不错,但历史代码则不适合,因为它一个类里面包含大量的其他方法,如果未来需求中有第二种需要通知的情况,代码就会更难维护,毕竟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就很能反映它的优势之处了

  1. 我们可以在单一方法内Publish多个事件,互不干扰
  2. 监听者可以基于表达式进行基本的过滤
  3. 一个事件可以被重复监听

:是的,而且它还可以支持异步事件处理!

亮架构(停顿了一下):你觉得支持异步是它独特的优势吗?哈哈哈,即使是同步监听到事件,你只要用线程池异步处理就好了。能够天然异步化,只是锦上添花的东西,不要弄混了哦。当然了,每种技术和特性都有其独特的使用场景,在使用的时候需要注意它的特殊情况,比如:

  1. 业务上是否允许异步处理(即使是延迟了比较久的时间)
  2. 能否完全相信事件通知里面的参数,是否需要反查等等。

还有别的方式吗

我(开心):如果我用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即可!但是,如果你的需求和我一样,那么推荐以下几种方案:

  1. 利用MQ解耦
  2. 利用SpringEvent解耦
  3. 自行根据当前需求和未来可能的需求考虑是否需要策略类
  4. 终极方案:真正理解编程的七大原则及常用的设计模式、随机应变即可

那么请允许我推荐一下之前的文章:设计模式总篇:从为什么需要原则到实际落地

如果你觉得这篇内容对你有帮助的话:

  1. 当然要点赞支持一下啦~
  2. 另外,可以搜索并关注公众号「是Kerwin啊」,一起在技术的路上走下去吧~ 😋