如何编写可读性强、可维护性易的代码
开发一个功能,老手能实现,新手也能实现。但是在代码的可读性和可维护性上可能就有很大差别了。新手很可能讲求的是功能的完成,而老手在功能健全的前提下,更会考虑代码的可读性和可维护性,简单的说就是”代码之美”。
如何编写优雅的代码?下面聊聊几个新手比较容易犯的问题。(有些老手也会犯,不知道是懒得改还是什么了,按理编码一定时间会有自我进化才对)
下面几个例子中,上半部分是待优化的,下半的是整理过后的。
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 )