最近写代码的一些心得

一、不放过细节


很多的隐藏很深的bug就是代码里面一些小细节不注意导致的,比如一个变量被定义的位置,或者一个自己不了解细节的api调用等。很多细节的处理也决定了代码质量,比如命名、代码规范的处理等。
我们看下面这段代码:

private void reduceValueConvert(CouponCategoryDTO dto) {
    BigDecimal reduceValue = BigDecimal.ZERO;
    if (MarketConstant.CategoryType.ZKQ.equals(dto.getType()) || MarketConstant.CategoryType.SPQ.equals(dto.getType())) {
        reduceValue = dto.getReduceValue().multiply(new BigDecimal(10)).subtract(new BigDecimal(10)).abs();
    }
    if (MarketConstant.CategoryType.SPQ.equals(dto.getType()) && MarketConstant.DiscountType.FPQ.equals(dto.getDiscountType())) {
        reduceValue = dto.getReduceValue();
    }
    dto.setReduceValue(reduceValue);
}

这是我前段时间在排查问题的时候看到的代码,刚好这段代码的细节问题非常多。这里先简单介绍一下这个函数的目的:根据传入的CouponCategoryDTO判断优惠券是商品券还是折扣券,如果是商品券MarketConstant.CategoryType.SPQ,则不做转换,如果是MarketConstant.CategoryType.ZKQ,则将传入的dto里面的reduceValue设置成供页面显示的X折这种形式。逻辑非常简单,但代码看上去却差点意思,我们来剖析一下看看:

命名

这个方法名叫reduceValueConvert,从字面意思上理解的话,可以理解为“转换减少值”,应该是针对传入的dto对象中的reduceValue做转换的。但是具体要做什么样的转换,以及为什么要转换,单从方法名上是看不出来的,那假设换一种命名方式叫做:getDisplayReduceValue,这时候理解起来就知道,ok,这是因为reduceValue需要转换成ui需要的显示格式,所以定义一个函数专门用来干这件事情,方法名具体了很多,也更好理解了。

传参

我们看之前这个方法传了个CouponCategoryDTO进去,虽然说这样传参减少了参数的数量,但是一旦传入这个对象,这就是个引用传递,那么在方法内部是可以修改这个参数内部属性的数据的(这个函数确实这么干了),对外部调用来说,如果后续依赖这个参数对象的话,不建议直接传对象进方法,而是new一个新的对象,或者只传需要的参数进入这个方法就可以了。

简洁

我们看到,函数内部有2个if判断,而后面那个if判断会修改前面那个if代码块里面的reduceValue返回值,阅读起来还是比较啰嗦的,有简化空间;对于静态枚举变量,可以使用static import简化很多代码。

BUG

这个代码里面是隐藏一个bug的,细心就会发现,函数里面的2个if块,可能都没有满足,这种情况下,函数会将dto里面的reduceValue设置成BigDecimal.ZERO,这里就引发了bug了。产生这个BUG的原因其实还是写代码在细节处理上的坏习惯:不要在函数内部改变参数对象的属性

重构后代码

这里我对这段代码进行了一些重构,先看代码:

private BigDecimal getDisplayReduceValue(BigDecimal originalReduceValue, Integer categoryType, Integer discountType) {
    if (ZKQ.equals(categoryType) || SPQ.equals(categoryType)) {
        if (!FPQ.equals(discountType)) {
            return originalReduceValue.multiply(BigDecimal.TEN).subtract(BigDecimal.TEN).abs();
        }
    }
    return originalReduceValue;
}

我们看到,重构后的代码,函数名非常具体,就是去获取显示折扣,然后传入的3个参数名也很具体,使用了static import让代码相对简洁一些。只在指定的if块里面进行转换,其它情况仍然返回originalReduceValue。代码没有改变任何参数的值,具体对返回值做什么处理,由调用该函数的使用者来决定。代码看上去也比之前更容易维护了。

二、认真学习设计原则SOLID


SOLID原则是5大设计原则的首字母简称,分别为:

单一职责原则 SRP

在任何一个软件模块中,应该有且只有一个被修改的原因。这里强调2个点:
1、职责要单一,其它地方动了,不应该影响我,我动了,也不应该影响别人。
2、不能有多于一个职责,因为一旦职责多了,一个改动会影响另一个。
举个实际场景的例子,比如我有一个类,是叫OrderService,那这里面应该都是和订单相关的函数,如果万一出现一个支付的、优惠券的、购物车的,那就破坏了单一职责原则(其实这个例子不好,因为订单服务职责也太多了,应该拆解为订单查询,下单,订单支付等)。

开闭原则 OCP

软件实体应该对扩展开放,对修改关闭。这里也强调2点:

  • 有新需求时,可以对现有代码进行修改,以适应新的变化;
  • 类一旦设计完成,就可以独立进行工作,不要再对其做任何修改。

不修改就意味着不影响现有业务,也就不会引发BUG。所以我们在设计类时,要考虑怎么样既可以实现扩展功能,又不需要修改代码。
这一点我体会很深刻,我现在公司的一个项目由于已经上线了一年左右了,迭代了无数版本,业务逻辑已经比较复杂了,大家现在写代码有点像是在修水管,拧上一处阀门,往往导致另外一处地方漏水了。。最后很多精力花在救火上面,导致生产效率越来越低。
这里有几个设计模式我推荐大家学习一下,可以让代码避免过早陷入复杂性:

  • 装饰者模式:Wrap一个新类来扩展功能
  • 策略模式:制定一个策略接口,让不同的策略实现成为可能
  • 适配器模式:不改变原有类的基础上适配新功能
  • 观察者模式:灵活添加和删除观察者(Listener)来扩展系统功能

里氏代换原则 LSP

程序中的父类都应该可以正确地被子类替换。我发现很多程序员写代码的时候,其实不太擅长使用继承和抽象关系。其实面向对象设计里面最伟大的概念就是抽象,理解了抽象,才能对现实世界的业务进行建模,才能化繁为简,设计出简洁的软件架构。
在进行抽象设计的时候,程序中不应该出现instanceof关键词,因为这种设计破坏了LSP原则,将导致父类无法被复用(因为只有在特定子类时才生效)。子类中使用的函数应该在父类中被定义,子类和父类在行为表现上一定要一致。

接口隔离原则 ISP

多个特定场景的接口,要好过一个宽泛的通用接口。
1、不要强迫用户依赖那些他们并不使用的接口
2、使用多个专门的接口比使用一个总接口要好
这个比较好理解,就是我们现在都喜欢写一个XXXService,然后在里面定义一大堆的函数,这样的写法是违背ISP原则的,因为一个接口里面定义了太多的依赖函数,其实我们在其他地方调用该接口时,可能只依赖其中某几个函数,但是却要引入一个很大的依赖关系,如果修改了其中某个东西,很容易导致其他地方出错。所以尽量分成多个接口来开发。比如查询、修改分成2个接口来开发。

依赖倒转原则 DIP

模块之间交互应该依赖于抽象,而不是具体实现。
1、大家依赖的是一个约定,而不关心具体实现细节
2、领域层不应该依赖基础设施层
这2点非常重要,我们在写代码时,经常会涉及到调用第三方模块,比如我订单服务可能会查询商品服务,那么我们之间应该首先建立好一个抽象接口约定,当我调用的时候,我只需要调用这个抽象接口就可以了,不需要关心其他服务是用什么框架、甚至编程语言实现的。这里面Java里面的Spring框架是通过依赖注入方式来实现,还有类似FeignClient这种也是一个很好的方式。然后领域层(业务逻辑层)不应该依赖基础设施层(框架、中间件等),意思也是说,我们不应该让自己的业务代码被框架侵入太深,否则一旦这个框架有问题不满足需要,那我们就比较被动了。

总结

最近写代码比较多,遇到很多问题,也总结了很多经验,以上只是一部分。
PS:上周又遇到一个重大线上事故,我自己复盘下来,收获很大,回头抽时间写出来跟大家分享,避免踩坑。

你可能感兴趣的:(最近写代码的一些心得)