Bjarne Stroustrup,C++之父对好代码的定义是这样的:
- 逻辑应该是清晰的,bug难以隐藏;
- 依赖最少,易于维护;
- 错误处理完全根据一个明确的策略;
- 性能接近最佳化,避免代码混乱和无原则的优化;
- 整洁的代码只做一件事。
代码规范占据了十分重要的地位,好代码具有不可估量的意义:
- 在团队合作中,规范的代码能够促进团队合作;
- 规范的代码可以减少bug的处理;
- 规范的代码可以降低维护成本;
- 规范的代码有助于代码审查;
- 养成写好代码的习惯,有助于自身的成长。
我在看完《代码整洁之道》之后,对它的一部分内容进行的了归纳总结:
(我归纳得比较笼统,想要更深入了解的还是建议去看原书,我的另一篇博客里面有英文版和中文版的下载连接)
一、有意义的命名
二、函数
三、注释
四、格式
五、对象和数据结构
六、错误处理
七、边界
八、类
九、系统
十、并发编程
一、有意义的命名
1、指明计量名称和计量单位
2、不使用魔术数(指未经定义直接在代码中出现的数值)
3、注意作为变量名时的字母I和o(很像数字1和0)
4、做有意义的区分
5、使用读得出来的名称(正确的单词)
6、使用可搜索的名称
单字母的名称和数字变量很难被找出来
名字长短应与其作用域大小相对应
7、不使用成员后缀/前缀
8、避免使用编码,但若接口和实现必须选一个编码,则选实现;
ShapeFactoryImpl,甚至CShapeFactory都比IShapeFactory好得多
9、避免思维映射
10、类名和对象名应该是名词或名词短语
11、方法名应当是动词或动词短语
12、不使用俗语或俚语命名
13、每个概念对应一个词
(Get、fetch、retrieve)、(controler、manager、driver)等同义或近义的词在同一个项目中只使用一个
14、别用双关语,即避免将同一单词用于不同目的
15、可以使用解决方案领域名称以及所涉及问题领域的名称(专业术语)
16、添加有意义的语境
可以添加前缀以提供语境,但更好的方案是创建类,然后把变量当作该类的成员字段
二、函数
1、函数应该短小,20行封顶最佳
if、else、while语句中的代码块应该只有一行,写函数调用语句是一个不错的选择
2、一个函数只做一件事
3、每个函数一个抽象层级,自顶向下读代码:向下规则
public void B(){
C();
}
public void C(){
D();
}
public void D(){
...
}
4、将switch语句埋到抽象工厂底下
5、使用描述性语句,别害怕长名称
例如:SetupTeardownIncluder
6、函数参数
- 尽量减少函数参数,没有最好
- 如果函数要对输入参数转换,则结果体现在返回值上;
- 不使用表示参数(即不传入布尔值);
- 函数二个及以上可考虑封装成类;
- 对于一元函数,函数与参数应形成 动词/名词对 形式,如:write(name)
7、避免做其他被藏起来的事情
8、分隔指令与询问,例如将设置方法set和判断是否存在attributeExits分成两个方法
9、使用异常代替返回错误码
- 使用异常能把错误处理代码从主路径代码中分离出来
- 抽离try/catch代码块,另外形成函数
- 错误处理函数只做错误处理这一件事情
10、消除重复
11、结构化编程
每个函数、函数中的每个代码块都应只有一个入口、一个出口,即只有一个return,循环中不能有break或continue,且不能有goto语句,但在小函数中,偶尔出现return、break、continue没有坏处,而goto只要在大函数中才有道理,应避免使用。
小结:一开始可以想写什么就写什么,最后再按规则慢慢修改,没有必要强制自己一开始就写出非常完美的代码,这几乎没有能做到
三、注释
1、注释不能美化糟糕的代码
2、用代码来阐述自己的意图
3、好注释的类型:
- 法律信息
- 提供信息的注释
- 对意图的解释
- 阐释:把晦涩难懂的参数或返回值的意义翻译成可读形式
- 警示:警告会出现某种后果
- TODO注释:表示该功能代码待编写或修改
- 放大某种不合理之物的重要性
- 公共API的javadoc
4、坏注释的类型
- 读者看不懂的喃喃自语
- 多余的注释(不能比代码本身提供更多的信息,没有证明代码的意义,也没有给出代码的意图或逻辑)
- 误导式注释
- 循规式注释(例如:每个变量都要有注释的规矩)
- 日志式注释
- 废话注释(例如:/ 默认的构造器 /)
- 能用函数或变量时就别用注释
- 位置标记尽量少用
- 大括号后的注释(如;try{……} //try)
- 归属与署名:源代码控制系统是这类信息最好的归属地
- 注释掉的代码
- Html注释
- 非本地信息
- 信息过多
- 不明显的联系
- 函数头
- 非公共代码的javadoc
- 范例
四、格式
1、垂直格式
- 名称应当一目了然,函数短小而精悍
- 封包声明、导入声明和每个函数之间都有空白行隔开
- 紧密相关的代码应相互靠近
- 变量声明应尽可能靠近其使用位置:
1)本地变量(局部)应在函数内顶部出现
2)循环中的控制变量在循环语句中声明
3)实体变量(成员变量)应在类的顶部声明
4)调用函数应尽可能放在被调用函数上面;
5)概念相关的代码应该放在一起;
5.自上向下展示函数调用依赖顺序
public void B(){
C();
}
public void C(){
D();
}
public void D(){
...
}
2、横向格式
- 尽力保持代码行短小
- 在赋值操作符周围加上空格,如示例A;
不在函数名和左圆括号之间加空格,函数调用括号中的参数一一隔开,如示例B;
//示例A
int i = 3;
//示例B
determinant(a, b, c);
//示例C
int temp = b*b - 4*a*c;
/* 乘法间不加空格,因为乘法的优先级比减法高 */
3、缩进
4、while、for、if等语句的语句体用“{” “}”括起来
五、对象和数据结构
1、对象:暴露行为,隐藏数据。便于添加新对象类型而无需修改既有行为,但难以在既有对象中添加新行为;
public class Square{
public Point topLeft;
public double side;
}
public class Circle{
public Point center;
public double radius;
}
public class Geometry{
public double are(Object Shape){
//求面积的行为
}
}
/** 1\若在Geometry类中添加一个求周长的方法,不会影响Square类和Circle类
2\但添加一个Rectangle类有可能需要修改方法are(Object Shape)
*/
2、数据结构:暴露数据,没有明显行为。便于向既有数据结构添加新行为,同时也难以向既有函数添加数据结构;
public class Shape implements Shape{
private Point topLeft;
private double side;
public double area(){
//计算面积的方法
}
}
public class Circle implements Shape{
private Poit center;
private double radius;
private final double PI = 3.14159;
public double area(){
//计算面积的方法
}
}
/** 添加一个Rectangle类,不会影响现有方法area();
但在Shape中添加一个求周长的方法时,所有类都要加上该方法
*/
六、错误处理
1、使用异常代替错误返回码
2、先写try-catch-finally语句
3、使用不可控异常(运行时异常),可控异常违反开放/闭合原则
4、给出异常发生的环境说明
5、依调用者的需求定义异常
6、定义常规流程
7、避免返回null值,可以改为返回空列表、空数组……,避免NullPointerException;
8、别传递null值
七、边界
1、不要将类似Map这样的边界接口在系统中传递(即避免从公共API中返回边界接口,或将边界接口作为参数传递给公共API)
八、类
1、类的组织
类应该从一组变量列表开始,之后是公共函数
公共静态常量 -> 私有静态变量 -> 私有实体变量
2、类应该短小
-
单一权责原则(SRP)
-
保持内聚性
若一个类中的每一个变量都被每个方法所使用,则该类具有最大的内聚性
应当尝试将这些变量和方法拆分到两个或多个类中,让新的类更为内聚;
3、为了修改而组织
-
修改现存类有可能会破环其他代码,应在编码代码的时候,将有可能修改的类(如:sql类)的每个接口方法都重构到从sql类派生出来的类中。
-
开放-闭合原则(OCP):类应当对外扩展开放,对修改封闭;
九、系统
1、将系统的构造与使用分离
- 分解main;将全部构造过程搬迁到main或被称之为main的模块
- 使用抽象工厂模式
- 依赖注入(控制反转)
十、并发编程
1、为什么要并发?
并发是一种解耦策略。它帮助我们把做什么(目的)和何时做(时间)分解开。并发能明显地改进应用程序的吞吐量和结构
2、并发防御原则
- 单一权责原则,建议:分离并发代码与其他代码
- 限制数据作用域
- 谨记封装数据;严格限制对可能被共享的数据的访问。可以采用synchronized关键字在代码中保护一块使用共享对象的临界区。
- 使用数据复本
- 线程应尽可能独立:尝试将数据分解到可被独立线程操作的子集
3、警惕同步方法之间的依赖
- 避免使用一个共享对象的多个方法
4、保持同步区微小
- synchronized制造了锁,带来了延迟和额外开销。
5、测试线程代码
- 将伪失败看作可能的线程问题
- 先使非线程代码可工作,不要同时追踪非线程缺陷和线程缺陷
- 编写可调整的线程代码
- 运行多于处理器数量的线程
- 在不同的平台上运行
- 编写可插拔的线程代码
- 装置试错代码
有两种装置代码的方法:
1) 硬编码:手工向代码中插入wait()、sleep()、yield()、priority()的调用
2) 自动化:可以使用Aspect-Oriented Framework、CGLIB或ASM之类的工具通过编码来装置代码