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

事件起因

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();
}

简单观察者模式

我(继续思考):你刚那种情况太简单了,回归正题,咱们这个需求可以使用观察者模式解耦啊!

亮架构(犹豫道):不是不可以,但你想一下我们需要改动哪些代码吧。

:观察者的核心即通知方 + 处理方,如果我们使用JDK自带的观察者模式的话,改动如下:

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

亮架构:如果一段逻辑在设计之初就采用观察者模式的话,那还不错,但历史代码则不适合,因为它一个类里面包含大量的其他方法,如果未来需求中有第二种需要通知的情况,代码就会更难维护,毕竟JDK观察者模式是需要继承Observable类的,当然了,作为一个备选方案也不是不行。

AOP

我(突然想起来):亮架构,你说用AOP来处理合适吗?

亮架构:一般情况下我们用AOP来做什么动作呢?

:我的话,一般会用作权限处理、日志打印、缓存同步、特殊场景计数等等。

亮架构:是的,你想象一下如果我们把这些业务逻辑都堆在切面里会是什么样子?一个切点还好,两个呢,十个呢?大家拿到新项目的时候都会参考前人的代码风格,如果你开了一个坏的头,其他人就会跟着做同样的事,很快代码就会变成如同蜘蛛网一般,所以这种方式一定是要杜绝的。

MQ 解耦

我(突然想起来):对了,咱们的商品新建/修改都会有MQ的,我只用监听MQ然后做业务处理就好了。

亮架构:这个肯定是可行的,就是有点杀鸡焉用宰牛刀的感觉,毕竟我们需要处理的情况只是MQ中的一小部分,而且万一历史代码没有发送MQ怎么办呢?

Spring Event

亮架构:你有了解过Spring Event吗?

:以前研究过,确实用在这里还蛮合适的。

// 以下为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解耦
  • 自行根据当前需求和未来可能的需求考虑是否需要策略类
  • 终极方案:真正理解编程的七大原则及常用的设计模式、随机应变即可

那么请允许我推荐一下之前的文章:号称下一代可视化监控系统,结合SpringBoot使用,贼爽

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值