编写可读代码的艺术:次·循环逻辑优化
前言:上篇记录了本书第一部分,即代码表面层次的改进,包括一些改进代码可读性的简单方法,一次一行,在没有很大风险或者花很大代价的情况下就可以应用,具体见这里。本文在第一部分的基础上记录分享第二部分。第二部分将深入讨论程序的“循环和逻辑”:控制流、逻辑表达式以及让代码正常运行的那些变量。
如何编写可读代码
- 简化命名、注释和格式的方法,使每行代码都言简意赅。
- 梳理程序中的循环、逻辑和变量来减小复杂度并理清思路。
- 在函数级别解决问题,例如重新组织代码块,使其一次只做一件事。
- 编写有效的测试代码,使其全面简洁,同时可读性更高。
把控制流变得易读
关键思想:把条件、循环以及其他对控制流的改变做的越“自然”越好。运用一种方式使读者不用停下来重读你的代码。
条件语句中参数的顺序
决定是 a < b 好一些,还是 b > a好一些。
比较的左侧 | 比较的右侧 |
---|---|
“被问询的”表达式,值更倾向于不断变化 | 用来做比较的表达式,值更倾向于常量 |
eg1: if(length >= 10)
eg2:while(bytes_received < bytes_expected)
if/else 语句块的顺序
- 首先处理正逻辑而不是负逻辑的情况。eg:if(debug)而不是 if(!debug)
- 先处理掉简单的情况。更可能使 if 和 else 在屏幕之内都可见
- 先处理有趣的或者是可疑的情况
三目运算符
建议默认情况下都用 if/else,三目运算符只有在最简单的情况下使用
从函数中提前返回
- 可以在函数中出现多条 return 语句,并且推荐在函数中提前返回。
- 想要单一出口点的一个动机是保证调用函数结尾的清理代码,但现代编程语言为这种保证提供了更精细的方式:
语言 | 清理代码的结构化术语 |
---|---|
C++ | 析构函数 |
Java、Python | try finally |
Python | with |
C# | using |
最小化嵌套
嵌套很深的代码难以理解,每个嵌套层次都是在读者的“思维栈”上又增加了一个文件。
- 嵌套如何累积形成?
插入新代码-------- 当你对代码改动时,从全新的角度审视它,把它作为一个整体来看待。 - 通过提早返回来减少嵌套
通过马上处理“失败情况”并从函数早返回来减少嵌套,例如:
改动前:
if(user_result == SUCCESS){
if(permisson_result != SUCCESS){
reply.WriteErrors("error reading permissions");
reply.Done();
return;
}
reply.WriteErrors("");
} else {
reply.WriteErrors(user_result);
}
reply.Done();
改动后:
if (user_result != SUCCESS){
reply.WriteErrors(user_result);
repley.Done();
return;
}
if (permisson_result != SUCCESS){
reply.WriteErrors("error reading permissions");
reply.Done();
return;
}
reply.WriteErrors("");
reply.Done();
- 减少循环内的嵌套
与 if(...) return; 在函数中所扮演的保护语句一样; if(...) continue; 语句是循环中的保护语句。
改动前:
for ( int i = 0; i < result.size(); i++) {
if( result[i] != NULL) {
non_null_count++;
if(result[i] -> name != ""){
cout << "Considering candidate..." << endl;
.....
}
}
}
改动后:
for ( int i = 0; i < result.size(); i++) {
if( result[i] == NULL) continue;
non_null_count++;
if(result[i] -> name == "") continue;
cout << "Considering candidate..." << endl;
....
}
你能理解执行的流程吗?
简化低层次控制流:把循环、条件和其他跳转写的简单一度
高层次流动也要清晰。
实践中,一些编程语言和库的结构让代码“幕后”运行或者流程难以理解,他们很有用,使代码更具有可读性,冗余更少,但是较难理解,难以追踪bug,要适度使用。
编程结构 | 高层次程序流程是如何变得不清晰的 |
---|---|
线程 | 不清楚什么时间执行什么代码 |
信号量/中断处理程序 | 有些代码随时都有可能执行 |
异常 | 可能会从多个函数调用中向上冒泡一样地执行 |
函数指针和匿名函数 | 很难知道到底会执行什么代码,因为在编译时还没有决定 |
虚方法 | object.virtualMethod()可能会调用一个未知子类的代码 |
拆分超长的表达式
代码中的表达式越长,他就越难以理解。把超长表达式拆分成更容易理解的小块。
-
用做解释的变量
解释变量:引入一个额外的变量,让它来表示一个小一点的子表达式。
目的:可以帮助解释子表达式的含义。 -
总结变量
总结变量:可以看出表达式含义,不需要解释,把它装入一个新变量中仍然有用。
目的:用一个短很多的名字来代替一大块代码,这个名字会更容易管理和思考。 -
使用德摩根定理
对于一个布尔表达式的两种等价写法:“分别取反,转换与/或”
(1)not ( a or b or c ) <=> ( not a ) and ( not b ) and ( not c )
(2)not ( a and b and c ) <=> ( not a ) or ( not b ) or ( not c)
改动前:
if (!(file_exists && !is_protected)) Error("Sorry, could not read file.");
改动后:
if( !file_exists || is_protected) Error("Sorry, could not read file.");
- 拆分巨大的语句
DRY——Dont't Repeat Yourself.
变量与可读性
变量的草率运用会让程序更加难以理解,产生很多问题:
(1)变量越多,越难全部跟踪它们的动向。
(2)变量的作用域越大,就需要跟踪它的动向越久。
(3)变量改变得越频繁,就越难以跟踪它的当前值。
减少变量
- 没有价值的临时变量:没有拆分复杂表达式;没做更多澄清;只用过一次。
- 减少中间结果:得到后立即处理
- 减少控制流变量:控制流变量通常可以通过更好地运用结构化编程而消除
缩小变量的作用域
避免全局变量,让所有的变量都缩小作用域,对尽量少的代码行可见。编程语言提供多重作用域/访问级别,包括模块、类、函数以及语句块作用域。通常越严格的访问控制越好,因为这意味着该变量对更少的代码行“可见”。比如,Java中private、protected、public和default区别:
类内部 | 本包 | 子类 | 外部包 | 说明 | |
---|---|---|---|---|---|
public | √ | √ | √ | √ | 具有最大的访问权限,可以访问任何一个在CLASSPATH下的类、接口、异常等。用于对外的情况,也就是对象或类对外的一种接口的形式。 |
protected | √ | √ | √ | 用来保护子类的。它的含义在于子类可以用它修饰的成员,其他的不可以,它相当于传递给子类的一种继承的东西。 | |
default | √ | √ | 针对本包访问而设计的,任何处于本包下的类、接口、异常等,都可以相互访问,即使是父类没有用protected修饰的成员也可以。 | ||
private | √ | 访问权限仅限于类的内部,是一种封装的体现,例如,大多数的成员变量都是修饰符为private的,它们不希望被其他任何外部的类访问。 |
只写一次的变量更好
操作一个变量的地方越多,越难确定它的当前值。那些只设置一次值得变量(或const、final、常量)使得代码更容易理解。
例子
运用上述内容所讲方法优化代码。
改动前:
var found = false;
var i = 1;
var elem = document.getElementById('input' + i);
while ( elem !== null){
if ( elem.value === '' ) {
found = true;
break;
}
i++;
elem = document.getElementById('input' + i);
}
if(found) elem.value = new_value;
return elem;
改动后:
for ( var i = 1; true; i++ ) {
var elem = document.getElementById('input' + i );
if ( elem === null )
return null;
if ( elem.value === ' ') {
elem.value = new_value;
return elem;
}
}