如何编写可读性强、可维护性易的代码

如何编写可读性强、可维护性易的代码

开发一个功能,老手能实现,新手也能实现。但是在代码的可读性和可维护性上可能就有很大差别了。新手很可能讲求的是功能的完成,而老手在功能健全的前提下,更会考虑代码的可读性和可维护性,简单的说就是”代码之美”。

如何编写优雅的代码?下面聊聊几个新手比较容易犯的问题。(有些老手也会犯,不知道是懒得改还是什么了,按理编码一定时间会有自我进化才对)

下面几个例子中,上半部分是待优化的,下半的是整理过后的。

1 尽量减少嵌套

在代码布局上,层次多的读起来会比层次少而平整的代码读起来费劲,看起来也混乱。在一些新手直白的思维就是不细思考直接编码了,层次较多就是这样来由,且看下面例子。

例子 1

func() {
    if (a.success()) {
        if (b.success())) {
            // 块1 这里很多代码
            // 这里省略很多行代码
        } else {
            // 块2 这里简单
            log("b error");
        }
    } else {
        // 块3 这里简单
        log("a error");
    }
    // 块4 没有更多代码
}

上述代码,if 成立(块1)逻辑内的代码较多;而 else分支内(块2,3)逻辑简单(就是打印下日志)或者连 else也没有,而条件之外的位置(块4)位置也没有其他代码了。针对这种情况,把简单块挪到前面做逻辑取反,成立则直接return,这样会使代码逻辑简单清晰得多。如下:

func() {
    if (!a.success()) {
        log("a error");
        return;
    }

    if (!b.success())) {
        log("b error");
        return;
    }

    // 块1 这里很多代码
    // 这里省略很多行代码
}

对比上来两个代码,逻辑一样,上面的逻辑嵌套多看的杂乱,而下面的层次嵌套少了,逻辑读起来也清晰点。

例子 2

func(param) {
    switch(param) {
        case success:
            // 块1
            // 这里省略很多行代码
            break;
       case error:
       case cancel:
       default:
           // 这下面三个分支都是空
           break
    }
}

像上面这个逻辑,只有一个case 有代码,其他的都是空,写成 switch case 就显得臃肿(特别是块1代码量比较多的时候就越明显),倒不如直接写成 if 逻辑清晰。如下:

func(param) {
    if (param == success) {
        return;
    }

    // 块1
    // 这里省略很多行代码
}

例子 3

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Button downloadButton = (Button)findViewById(R.id.btn_download);

        downloadButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                DownloadEngine engine = new DownloadEngine();
                engine.download(new DownloadEngine.Callback() {
                     @Override
                     public void onSuccess(File saveFile) {
                         // 代码块1
                         // 这里省去很多行代码
                     }

                     @Override
                     public void onFailure(int statusCode, Throwable e, File saveFile) {
                         // 代码块2
                         // 这里省去很多行代码
                     }
                 });
            }
        });
    }

上述代码,Listener 里面有Callback(或者其他Listener)的情况,在Android 工程比较容易碰到。如果层次过于多,走读代码时候,对于这个嵌套太厉害的,读得会比较费劲,一看一团代码在那,长长短短的,看着心闷。像这种也应该尽早把它提取出来,逻辑会清晰一大半。如下:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Button downloadButton = (Button)findViewById(R.id.btn_download);

        downloadButton.setOnClickListener(new OnClickDownloadListener());
    }

    private class OnClickDownloadListener implements View.OnClickListener {
        @Override
        public void onClick(View view) {
            DownloadEngine engine = new DownloadEngine();
            engine.download, new DownloadCallback());
        }
    }

    private class DownloadCallback implements DownloadEngine.Callback {
        @Override
        public void onSuccess(File saveFile) {
            // 代码块1
            // 这里省去很多行代码
        }

        @Override
        public void onFailure(int statusCode, Throwable e, File saveFile) {
            // 代码块2
            // 这里省去很多行代码
        }
    }

对比例子3的这两块代码,下面的虽然行数比上面的多,但是清晰性可读性明显会比上面的好。块1 和块2较为复杂(比如里面也有嵌套、判断)的情况下,下面的可读性就更明显能体现出来了。

2 大面积相同的代码段,应该抽成函数作为公共代码

常有这样的情况,本来是一块代码的,后来业务增加,增加的刚好就是差不多的一个功能,新手常常做法或者就是直接拷贝处一份代码稍微做修改,便克隆出一份几乎一模一样的代码了。在业务继续增多的时候,可能相同的代码块就会越来越多,代码臃肿了,而且如果某天涉及到修改,一改得改多处,可维护性极差。这种,应该尽早到抽取出来作为功能公共代码。

例子 4

一个函数内的两块代码块基本相同的情况,如下:

    public void handleHasInputHashtags(Set<String> hasInputHashtags) {
        Hashtag tag = null;
        View view = null;

        // 我的标签
        int count = mMyHashtagLayout.getChildCount();
        for (int i = 0; i < count; i++) {
            view = mMyHashtagLayout.getChildAt(i);
            if (view instanceof TextView) {
                tag = getBindingHashtag(view);
                if (tag != null) {
                    ViewUtils.setEnable(view, !hasInputHashtags.contains(tag.getTitle()));
                }
            }
        }

        // 星主标签
        count = mGroupOwnerHashtagLayout.getChildCount();
        for (int i = 0; i < count; i++) {
            view = mGroupOwnerHashtagLayout.getChildAt(i);
            if (view instanceof TextView) {
                tag = view.getTag();
                if (tag != null) {
                    ViewUtils.setEnable(view, !hasInputHashtags.contains(tag.getTitle()));
                }
            }
        }
    }

上述代码, 两块代码长得一摸一样的,如果某天需要修改这个块,很可能一次修改需要改两个地方,而且比较容易忘记修改另一处,这个是很多人都碰到过的,如果相同的部分抽取成一个函数独立出去,提供给原函数传入参数调用,就能规避后续修改可能需要修改两处的问题,这样增强了可维护性和可读性。修改如下:

    private void handleInputtedHashtags(FlowLayout flowLayout, Set<String> inputedHashtags) {
        int count = flowLayout.getChildCount();
        for (int i = 0; i < count; i++) {
            View view = flowLayout.getChildAt(i);
            Object o = view.getTag();
            if (o instanceof Hashtag) {
                ViewUtils.setEnable(view, !inputedHashtags.contains(((Hashtag)o).getTitle()));
            }
        }
    }

    public void handleInputtedHashtags(Set<String> hasInputHashtags) {
        handleInputtedHashtags(mGroupOwnerHashtagFlowLayout, hasInputHashtags);
        handleInputtedHashtags(mLatestHashtagFlowLayout, hasInputHashtags);
    }

可以看到,抽取成函数后,代码量也少了。如果相同的块较多,这个好处就更下明显体现出来了。

上面例子是原来在一个函数内的两块(也可能多块)代码相似的情况。而实际工程中也可能会遇到两个函数非常接近的,那样更应该抽取公共代码函数,原来的两个函数通过传参数调用公共函数,这样代码在可维护性都会得到更好的提升。

3 去除过于唠叨的无用逻辑代码

有些代码逻辑,过于“小心翼翼”的做判断或者其他逻辑处理,可能有些事完全不必要的,显得唠叨。写上了不会错,但去掉了也不影响逻辑,这种应该去掉为好。留给代码足够的清晰。

例子 5

     private class OnClickListenerImpl implements OnClickListener {
         @Override
         public void onClick(View v) {
             if (!(v instanceof TextView)) {
                 return;
             }

             TextView textView = (TextView) v;
             Object tag = textView.getTag();

             if (!(tag instanceof Hashtag)) {
                 return;
             }

             Hashtag hashtag = (Hashtag) tag;
             mOnHashTagClickListener.onHashtagClick(hashtag);
         }
     }

在上面代码工程中,只有TextView 才会被设置tag 和Listener,里面的v instanceof TextView 就是唠叨的了,就算不是TextView 也没有关系,只要是View 亦可getTag(),这个判断逻辑去除也不影响实际的代码逻辑,理应去掉,保持代码简短简洁。修改如下:

         private class OnClickListenerImpl implements OnClickListener {
             @Override
             public void onClick(View v) {
                 Object tag = v.getTag();
                 if (tag instanceof Hashtag) {
                     mOnHashTagClickListener.onHashtagClick((Hashtag)tag);
                 }
             }
         }

看,实际上就是那么一点点的代码,何必写那么复杂?

4 善用面向对象思想,而不要瑞士军刀般的封装冗余数据

比如设计一个动物类,有两种方案,如下:

1)是设计一个类,把动物所包含的所有特性都包进去,比如 头、手、脚、翅膀、尾巴等,在加一个type 表示动物的类型;

2)设计一个动物基类,包含一些公共的特性,之后派生不同的类,由派生类去封装自己的特性。

毫无疑问大多数人(包括新手老手)都会认为方案2合理。但是在实际上,很多人不知不觉的使用了方案1 去实现了,而且实现后自己也没有觉察由什么问题。

下面例子也是实际工程里面碰到的,启动AdmireActivity 去赞赏主题或者赞赏评论,在工程中,这个要么是赞赏主题,要么是赞赏评论,原来的代码逻辑是这样,代码如下

例子6,代码如下:

public class AdmireActivity extends BaseActivity {
    public static void start(Context context, Topic topic) {
        if (context == null || topic == null) return;

        StartRewardParam param = StartRewardParam.builder()
                .topic(topic)
                .rewardType(REWARD_TYPE_TOPIC)
                .build();

        startWithParam(context, AdmireActivity.class, param);
    }

    public static void start(Context context, Topic topic, Comment comment) {
        if (context == null || topic == null || comment == null) return;

        StartRewardParam param = StartRewardParam
                .builder()
                .topic(topic)
                .comment(comment)
                .rewardType(REWARD_TYPE_COMMENT)
                .build();

        startWithParam(context, AdmireActivity.class, param);
    }

    private static final int REWARD_TYPE_TOPIC = 0;
    private static final int REWARD_TYPE_COMMENT = 1;

    @IntDef({REWARD_TYPE_COMMENT, REWARD_TYPE_TOPIC})
    @Retention(RetentionPolicy.SOURCE)
    public @interface REWARD_TYPE {
    }

    @Getter
    @Setter
    @Builder
    public static class StartRewardParam {
        private Topic mTopic;
        private Comment mComment;

        @Builder.Default
        @REWARD_TYPE
        private int mRewardType = REWARD_TYPE_TOPIC;
    }

    // 这里省去了很多代码
}

上述代码里面封装了个StartRewardParam作为启动这个Activity 的参数,这个 Param 里面有 type 来指明类型是主题或者评论。同时也定了 REWOARD_TYPE_TOPIC 类型和 REWOARD_TYPE_COMMENT 类型,Param内也需要带入了Topic 或者Comment 数据。也就是说,type为 REWOARD_TYPE_TOPIC 时候,需要带上Topic;为 REWOARD_TYPE_COMMENT 时候需要带上Comment。

这样封装就是类似瑞士军刀形式的封装了,因为在 type 为 REWARD_TYPE_TOPIC 的时候,StartRewardParam的mComment 也是一个成员,但它是无用项。这种不管是不是该类型的数据,所有数据统统堆在一起,这种设计,冗余了数据,如某些动物有尾巴和翅膀,人继承于动物,人也带着个尾巴和翅膀?挺奇怪的。
在使用上,必然也是类似这样

void func(StartRewardParam param) {
    if (param.mRewardType == REWARD_TYPE_TOPIC) {
        Topic topic = param.getTopic()
        // 注意,这里可以看见 有个param.getComment()接口活着成员变量,挺怪的
    }
}

对于上述代码这种设计,是没有很好的用向对象思想去设计了。

改进下,封装一个基类,之后派生不同类去封装不同的数据类型即可,比如赞赏主题类型的param 就不应该看到comment 节点元素才对。修改代码 如下:

public class AdmireActivity extends BaseActivity {
    public static void start(Context context, Topic topic) {
        startWithParam(context, AdmireActivity.class, new RewardTopicParam(topic));
    }

    public static void start(Context context, Comment comment) {
        startWithParam(context, AdmireActivity.class, new RewardCommentParam(comment));
    }

    public abstract static class RewardParam {
    }

    public static class RewardTopicParam extends RewardParam {
        private Topic mTopic;
        private RewardTopicParam(Topic topic) {
            mTopic = topic;
        }
    }
    
    public static class RewardCommentParam extends RewardParam {
        private Comment mComment;
        private RewardCommentParam(Comment comment) {
            mComment = comment;
        }
    }

    // 这里省去了很多代码
}

上面调整后的代码,使用了派生类去实现类型封装,而不是使用一大堆数据扔在一个类里面去。先看代码量大概减少了一半,清晰了不少,读起来爽多了。使用上,不需要的东西,在不同的类型分支内是不可见的,不会在 Topic 类型的情况下会带着个 Comment 节点,逻辑也明朗了。看使用上的差别:

void func(RewardParam param) {
    if (param instanceof RewardTopicParam) {
        Topic topic = ((RewardTopicParam)param).getTopic()
        // 注意:这里是看不到 getComment() 的,是不可以看到
    }
}

5 废弃的代码加上已过期标记,并且及时清理

已经无用的废弃代码,应该做上标记(如Java的@deprecated ),否则,很容易误导新加入团队的人,如果确保没有被引用了后,应该及时删掉,否则可能会被再次引用而导致代码无法收敛整理,而越来越恶化。

上面提到的几种情况,都是在团队code review 情况看到的,也是新手写代码较容易犯的问题,有点心得,便拿出来聊聊。
如何编写可读性好,可维护性易的代码,还有很多细节探讨的,比如不需要无谓的注释,工程统一风格编码等等,这里不细说了。总之,编码尽量向简洁、清晰、短小可复用等这些特点靠近就不会错了。

(全文完)

(欢迎转载本站文章,但请注明作者和出处 云域 – Yuccn

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注