如何提高代码可读性

2020-12-29  本文已影响0人  小码的小坑

一、前言


我希望我的代码在几个月后修改时,能快速的跟上当时的心路历程,减少理解代码的时间和复杂度,以及修改出现BUG的几率。我也希望我的代码可以被产品读懂,有需求变更的时候他来改(我就想看看他还要不要改需求)。带着这个愿景,最近梳理了代码可读性的一些想法。

二、复杂度


在聊可读性前先聊聊复杂度,对复杂度有所定义后,才能更好的探讨可读性的优化。
《博弈圣经》中提出信息熵是[信息论]中用于度量信息量的一个概念。一个系统越是有序,信息熵就越低;反之,一个系统越是混乱,信息熵就越高。所以,信息熵也可以说是系统有序化程度的一个度量。简单来说,"AAAAA"(熵为零)<"ABCDE"(有序信息熵)<"DMXTL"(无序信息熵),其中无序信息熵的熵值最大,复杂度最大。
而对于代码来说,需要实现逻辑功能就需要有信息熵。所以让代码有序在很大程度上可以降低代码的复杂度。

三、代码中的复杂度


代码中的复杂度往往来源于我们对代码不能顺畅的阅读和理解,而通过规范、公约、以及符合大脑阅读和理解的方式去写代码,把代码中的无序尽量控制在我们方便理解和接受的范围,从而帮助我们减少对代码的认知成本,降低认知的复杂度。

四、良好的方法命名降低复杂度


方法作为一个实现逻辑的入口,实现往往决定了代码的复杂性。方法在被调用时,调用者希望能更快速简单的找到对应服务类里面想要的方法,所以方法的命名尤为重要。方法的命名得好,可以大幅度缩短找到方法的时间,并且可以让别人快速理解方法的实现的功能是什么。
示例:我们需要提供一个根据:“商品类型ID统计有效的冰冻商品数量”的接口,命名为:

public Integer freezeGoodsCountInValidByGoodsTypeId(Integer goodsTypeId)

为了表达清楚方法实现的功能命名长了一点,方法名也能比较贴切的表达了方法功能,但是实际上要找到并理解这个方法名并不是很快速的事情,特别是商品类中提供的对外方法很多的时候。问题在于我们不是英语母语的国家,很多开发人员特别是经验不够丰富的开发人员本身对英语就不太敏感,对于这么一大段英文描述需要慢慢看了之后慢慢理解意思。如果调用者刚好记得冰冻的英文这么写,通过工具(IDEA)也还算快速能定位到商品类中freeze开头的方法。如下图,我想要快速找到map中cmpute相关的方法:


image.png

但是实际上命名时五花八门,这个方法命名为:goodsCountInFreezeAndValidByGoodsTypeId也没毛病,同时还有可能使用另外的单词描述冰冻商品的情况。所以即便有工具帮助我们也不一定能很好的快速定位到我们想要找到的方法。回到需求本身,我们对方法要做的事情进行拆分后得到:
冰冻商品(业务描述)+统计(限定词)+有效的(业务情况)+根据商品类型ID(条件)

public Integer freezeGoodsCountInValidByGoodsTypeId(Integer goodsTypeId)
public Integer count4FreezeGoodsInValid8GoodsTypeId(Integer goodsTypeId)

五、提高方法的可读性降低项目复杂度


(1)通过方法拆分提高代码可读性

方法作为最小的逻辑实现单位,方法的实现很大程度上决定了代码的复杂度。回到我们的实际场景中,当我们在开始阅读一个方法的时候,我们最关注的是什么?在开始阅读的时候,我们首先关注的是这个方法做了什么(what),每个步骤分别是什么,然后在定位到了我们想要找的步骤以后,才想要关注它是怎么实现的(how),实现逻辑是什么,在哪里修改,哪里出了问题等。
这是一种符合我们大脑认知的过程,而实际的开发中,我们可能看到这样的代码:

private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
            @Nullable String... ignoreProperties) throws BeansException {

        Assert.notNull(source, "Source must not be null");
        Assert.notNull(target, "Target must not be null");

        Class<?> actualEditable = target.getClass();
        if (editable != null) {
            if (!editable.isInstance(target)) {
                throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
                        "] not assignable to Editable class [" + editable.getName() + "]");
            }
            actualEditable = editable;
        }
        PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
        List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);

        for (PropertyDescriptor targetPd : targetPds) {
            Method writeMethod = targetPd.getWriteMethod();
            if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
                PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
                if (sourcePd != null) {
                    Method readMethod = sourcePd.getReadMethod();
                    if (readMethod != null &&
                            ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                        try {
                            if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                                readMethod.setAccessible(true);
                            }
                            Object value = readMethod.invoke(source);
                            if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                writeMethod.setAccessible(true);
                            }
                            writeMethod.invoke(target, value);
                        }
                        catch (Throwable ex) {
                            throw new FatalBeanException(
                                    "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                        }
                    }
                }
            }
        }
    }

例子中方法代码实现中的函数过长,全是细节的平铺,非常不利于看的人理解方法做了什么,需要耗费更多的精力去慢慢阅读和理解。同时堆在一起代码在功能调整或修复BUG时也更容易产生新的BUG。我们对代码重新梳理一下,发现方法主要实现了几个功能步骤:

private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
            @Nullable String... ignoreProperties) throws BeansException {
        //1、检查确定可复制的参数(复制的前置检查)
        Class<?> actualEditable = checkAndResolve4Editable(source, target, editable);
        //2、构建参数的属性信息
        PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
        //3、根据属性信息复制源对象内容到目标对象内容
        copyProperty4SourceToTarget(source, target, targetPds, ignoreProperties);
    
    }

private static  Class<?> checkAndResolve4Editable(Object source, Object target, @Nullable Class<?> editable) {
        Assert.notNull(source, "Source must not be null");
        Assert.notNull(target, "Target must not be null");

        Class<?> actualEditable = target.getClass();
        if (editable != null) {
            if (!editable.isInstance(target)) {
                throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
                        "] not assignable to Editable class [" + editable.getName() + "]");
            }
            actualEditable = editable;
        }
        return actualEditable;
}

private static void copyProperty4SourceToTarget(Object source, Object target, PropertyDescriptor[] targetPds,@Nullable String... ignoreProperties) {
        List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);

        for (PropertyDescriptor targetPd : targetPds) {
            Method writeMethod = targetPd.getWriteMethod();
            if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
                PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
                if (sourcePd != null) {
                    Method readMethod = sourcePd.getReadMethod();
                    if (readMethod != null &&
                            ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                        try {
                            if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                                readMethod.setAccessible(true);
                            }
                            Object value = readMethod.invoke(source);
                            if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                writeMethod.setAccessible(true);
                            }
                            writeMethod.invoke(target, value);
                        }
                        catch (Throwable ex) {
                            throw new FatalBeanException(
                                    "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                        }
                    }
                }
            }
        }
}

初步拆分之后,我们发现copyProperty4SourceToTarget方法还是比较不容易理解,可以对其进行进一步拆分梳理方法逻辑。

private static void copyProperty4SourceToTarget(Object source, Object target, PropertyDescriptor[] targetPds,@Nullable String... ignoreProperties) {
        //1、从target中获取写方法
        //2、判断target中当前属性是否可以进行写操作
        //3、从source中读取属性内容
        //4、把source中读到的属性内容写入target的属性里
        //偷懒,我就不写抽取方法的具体实现了(^_^)/~~ 
}
(2)什么时候拆分方法

什么时候拆分方法需要根据实际情况而定:

上一篇下一篇

猜你喜欢

热点阅读