五星元老大飞哥,教半年Java实习生小飞飞:优雅解决历史代码中的新需求

事件起因

6月中旬,可爱的产品大飞哥穿着鸿星尔克,帅气极了,过来给我提了一个临时需求,需要我对商品创建/更新业务中由开放平台对接而来的请求做一个Check,如果符合要求,则再做一段稍微复杂的逻辑处理

这种Easy程度的需求怎么拦得住我,毕竟我经常喝汇源果汁,办事效率一级棒!不到半天我就Coding,Push一气呵成,正当我准备点一杯冰城蜜雪开始摸鱼的时候,我却收到了一封邮件。

邮件里有一堆的汉字和英文,但有几个字赫然在目:

您的代码已被驳回。

当我经历了茫然、震惊、不敢相信、最后喝了口冰城蜜雪后,无奈接受的情绪转变后,问了同样穿着鸿星尔克的评审同事,为什么要驳回我的代码,他说:“历史代码一般业务都很完整(跟屎山一样了…),那如果有新的需求不得不依赖它的话,怎么

做才是最佳方案,让代码有更好的拓展性,你有想过吗?”。

我肯定是没有想的,于是乎,我怀着些许愧疚的心情,找到了大飞哥,希望他能为我指点迷津。

找一个看起来合适的位置塞进去

大飞哥:小飞飞,这段代码是不是偷懒了?

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

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

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

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说白了是通过事件的传播,即方法直接调用来判断是否需要处理,本质都是一样的,那你知道未来的新需求你该怎么写了吗?

我(兴奋):我知道了,要写可拓展性的代码,像我今天改的这种代码就不行,太垃圾了!

大飞哥(摇了摇头,起身走了)小飞飞,你错了,你今天改的历史代码在当时可以说是最佳实践了,只是因为你遇到了之前的设计者未考虑到的问题而已。我们讲设计模式、讲七大原则,讲不要过度设计,就是为了你现在出现的情况,我们在编码过程中可能会遇到千奇百怪的代码,我们可以抱怨,可以吐槽,但记住,不要为了某些需求就把本来漂亮的代码变成屎山。所以你需要去学习编程的思想,学习设计的思想。

我(大声)那,大飞哥!如果有一段代码已经烂到不能再烂了呢!

“那就把它重构了!然后把作者的名字记下来,狠狠的吐槽他!”
在这里插入图片描述

“感觉你都要倒闭了还捐那么多钱”,这句话让人感到心疼,2020年亏损2.2亿捐款5000万!这家企业很久以来都无人问津,我们喊着买李宁、买安踏,但是好像很少说买鸿星尔克,估计不少人都觉得鸿星尔克早已不在了。

看看2020年营收:

2020年,安踏营收为355.1亿元,李宁营收为144.57亿元,特步为81.72亿元,361°是51.27亿元。

鸿星尔克营收仅为28.43亿元,净利润-2.2亿。

看到数据就知道为啥网友心疼鸿星尔克了吗?就这么困难还能伸以援手!在网友自发大量购买后:

鸿星尔克总裁吴荣照于23日凌晨1点出现在直播间,向消费者致谢同时,呼吁网友“理性消费”。啥也不说了,点赞!谢谢你这般善良!

  • 20
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值