代码质量杂谈01

前言

从17年自学编程开始到现在写代码也有五年了。期间在不同公司和各式各样的开发者合作过,一个很大的感触就是大多数开发者要么并不在乎自己的代码质量,要么根本不清楚什么样的代码是好的。

为什么要重视代码质量

● 优秀的的代码质量能够减少线上问题发生的概率
● 持续维护低质量的代码会导致破窗效应
● 好的代码可以降低维护成本,提升工作效率
一句话总结,代码质量是团队生产力的重要保障

软件设计方法

在和团队同学交流时,一旦谈到代码质量或者编程原理时,“高内聚“,”低耦合“,”设计原则“,”“设计模式”,甚至于DDD这种词汇往往会高频出现,而且大家非常热衷于分享设计模式,光我记忆中听过有关设计模式的分享就超过七次(催眠效果挺好的)
我们不妨想想为什么会有这些东西。本质上所有设计行业追求的标准都应该是**“简单易懂可修改”。基于这个理念,大家提出了用抽象封装**这两个方式对某个概念进行包装和拆分,使之更符合人的认知规则。程序设计领域的高内聚、低耦合、开闭原则,单一职责原则等等名词都可以看作是对抽象和封装的进一步解释和说明,在这些解释和说明之下,才诞生了诸如设计模式,设计思想等等一系列工具和方法论,过多着眼于这些方法论不足以提升程序设计能力和代码质量
在这里插入图片描述
实质上我们需要核心关注的还是那两个点,抽象和封装

抽象

抽象是指为了某种目的,对一个概念或一种现象包含的信息进行过滤,移除不相关的信息,只保留与某种最终目的相关的信息。

抽象的核心要点在于归一化、通过定义接口,外部调用者不需要关注内部实现,可以一视同仁的使用实现了特定接口的对象,比如实现Comparable接口的一切对象都可以用来排序。我见过有人使用接口的default方法来实现代码复用的,这就是完全没有理解抽象的意义。

封装

就是将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体

封装可以看作是对抽象的一个补充,核心要点在于划分边界,通过标识外部成员可以使用的内容(方法或者数据)从而和外部调用者达成约定,实现者可以基于这个基础修改封装内的东西而不影响外部,外部调用者也知到哪里不能碰,这样就提供了一个良好的合作基础——只要边界这个基础约定不变,那么代码改变不足为虑。

如何评估代码质量

衡量应用代码是否优秀可以通过可读性和可维护性来衡量,前者关注于代码的理解成本,要求代码简单易懂。后者则关注于代码的修改成本,修改要简单并且不会引起其他问题。当然这两个要素的衡量标准都主要基于人的主观人认知,难以有统一标准。
在这里插入图片描述
更好的衡量标准应该是团队内部达成自己的共识。这是因为代码质量是服务于团队内部的,不同团队往往会基于不同的技术栈或者业务形态来开发,导致设计思路,抽象层级,技术框架等等一系列内容都不相同,例如安卓团队应用很广的RxJava放到服务端团队的某些场景可能就会变成噩梦。所以每当我负责重构或者代码治理时往往首先要做的就是统一思想,通过代码样板间或者固定文档让团队成员对“好“的代码形成统一标准,然后在日常工作中通过深度的codereview来落实标准
在这里插入图片描述

一些改善代码质量的小细节

最后我们聊一些我在日常的codereview中总结的能提升我们代码质量的一些小细节

仔细考虑抽象层级

从我的代码review的经验来看,大部分程序员是缺少抽象能力的,一个典型的例子就是很多人一旦觉得一个方法很长,就会直接把某些代码截取出来然后宣称自己遵循了【单一职责】这个原则,其实是这种观念是错误的,真正的单一职责应该是每个执行单元(方法或者类)仅包含其下一个抽象层级单元(可能有多个)。关键点在于层层抽象并金向外暴露一个抽象概念。我自己写代码或者重构时的习惯是自顶向下,先搭框架,先给出对外提供的接口定义,然后思考内部需要哪些接口协调实现功能,然后在方法内部按1、2、3…的顺序理清流程,最后填充细节。这种模式能人快速理解模块作用和内部运作原理,降低代码复杂度。这里用一个风控业务来举例(不能给出具体代码,仅给出抽象层级图示)
在这里插入图片描述

收敛变化

应用程序中几乎所有业务都依赖于各种状态实现,但绝大部分BUG和复杂度又因为状态而产生
一个典型的例子是责任链模式下的状态管理,很多人会模仿spring中的责任链用法,在节点内对上下文进行修改,其实在强业务场景下这样做会导致模块维护成本急剧上升
例如以下基于责任链模式开发的一段代码,执行节点中加入了一个对于上下文信息的修改,实际上要求开发者必须对于责任链中的每一个节点代码都足够了解才能增加新的节点并使用上下文,失去了封装的作用

public class NoticeInterceptor extends NoticePlanInterceptor {
 
    @Override
    public boolean preCheck(NoticePlanContext context) {
        ...
        //修改上下文中的状态
        context.getTemplateConfig().setExtInfo(JSON.toJSONString(templateMessages));
        return true;
    }
}

更好的方式是将上下文NoticePlanContext内属性改为不可变类型,在责任链发起时一次性构建出完整的上下文。或者明确哪部分变量为可变变量,哪部分代码不可变,通过final对字段进行约束

约束而不是约定

墨菲定律说“坏的事情一定会发生”,我说“约定一定会被打破”。永远无法消除的nullpointer就是证明。于是我们会针对所有字段判空,把所有的代码块都用try包裹,应用内部被冗余代码充斥。这种状态我称之为信任感缺失综合症,我们需要引入些强制性的约束来治疗。
例如,针对可能为空的场景,我们可以将返回值声明为Optional类型强制要求调用方进行空值校验,而一定有返回值的则不用,一个典型的场景是根据code获取枚举值
针对可能发生异常的情况,我们可以在方法声明中加入受检异常(关于受检异常的争议这里暂时不讨论)强制约束上级对异常进处理,而反之则不用。
架构设计也是如此,例如责任链的上下文中某些字段用final来进行约束,强制状态不可变

传递足够的信息

你有没有遇见过一种模型,内部表示状态的字段使用了String、Long或者Int或其他类型,但是共同的特点都是你没法直接使用它,比如在下面这段代码中我们不知道什么值代表“已回复”这个状态。也就没法使用它。只能通过ide的查找功能才能通过模仿来使用,这是就是因为字段声明的地方传递的信息不够

class MessageInfo{
 	private String id;
    private String content;
    private String readStatus;
    /**
     ** 是否已回复
    **/
    private String hasReply;
    private Long messageType;
    private String ip;
 }

一种改良方式是通过字段注释来增加信息,但我并不推荐,更好的方式是针对这类字段进行特殊建模,例如这里是否已回复实际上是一个二元状态,可以声明Boolean类型,messageType这类包含多种状态的变量则可以声明为枚举

class MessageInfo{
	@NotNull
 	private String id;
 	@NotNull
    private String content;
    @NotNull
    private Boolean read;
    @NotNull
    private Boolean reply;
    @NotNull
    private MessageTypeEnum messageType;
    @Nullable
    private String ip;
 }

你可以注意到上述代码中不能为空的字段上都添加了NotNull注解,这同样也是一种信息传递(也是因为方便参数校验),告知外部调用者该字段不可能为空,请在需要的场景放心使用

使用声明代替命令

“74军第1师2团3营2连一排,你的机枪阵地向右移动三米,然后朝着3点钟方向射击3分钟”每次听到这段对于运输大队长的调侃都会自然而然的带我们的日常代码开发中,因为我们常用的命令式编程模式和它太像了,关注于代码如何实现,基于一些和计算机底层词汇非常接近的赋值、分支条件、循环等完成业务,读起来类似于“先执行A,再更新B,…”。比如以下这段代码

Buyer maxOrderBuyer = null;
for(Buyer buyer : recentBuyers){
    if(buyer.hasSetPrivate()){
        continue;
    }
    if(selfId.equals(buyer.getId())){
        continue;
    }
    if(maxOrderBuyer == null || maxOrderBuyer.getOrderNum() < buyer.getOrderNum()){
        maxOrderBuyer = buyer;
    }
}

还有另一种模式我们称之为声明式编程,这种模式更专注于要做什么,先定义要完成的目标,然后让具体的系统来决定如何做,这种方式的好处是代码读起来就像是问题陈述,理解成本更低。在这方面发展到极致的就是SQL语句。
基于声明式的编程模式的好处是能够极大的提升代码可读性和可维护性。在jdk1.8中,引入的lamdba表达式就是基于这种编程思想,例如如下优化后的代码

Buyer maxOrderBuyer = recentBuyers.stream()
          .filter(buyer -> !contact.hasSetPrivate())
          .filter(buyer -> !contact.getId().equals(selfId))
          .max(Comparator.comparing(Buyer::getOrderNum)).orElse(null);

这种模式一方面要求我们具有抽象的能力,将一个大目标拆分为一系列小目标,另一方面要求我们掌握并熟练的使用语言特性

不要货物崇拜编程

货物崇拜编程(Cargo Cult
Programming)是一种计算机程序设计中的反模式,其特征为不明就里地、仪式性地使用代码或程序架构。货物崇拜编程通常是程序员既没理解他要解决的bug、也没理解表面上的解决方案的典型表现。

一个典型的例子是多线程,我记得面试的时候我们老板问我有没有用到过多线程和CountDownLatch、CyclicBarrier这些同步工具,我回答的是我接手的代码里很多地方都有这些东西,最后都被我想办法删掉了。
事实上我个人的感受是在业务场景下用这些东西,百分之90是为了炫技,大多数时候不仅画蛇添足增加维护成本,而且提升不了什么性能(能提升maybe 0.1%?),甚至很多时候会导致性能负优化(死锁导致系统挂掉的我都遇见过),这就叫做货物崇拜编程,自以为是的引入一些新的技术点,以为自己做了好事情,实际上可能带来负面影响。
还有一个典型的例子是上来就用DDD(这一点我可能会专门出一篇文章聊聊)

tobe continue

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值