《代码简洁之道》

阅读本书有两种原因:第一,你是个程序员;第二,你想成为更好的程序员。

第一章、前言

艺术书并不保证你读过之后能成为艺术家,只能告诉你其他艺术家用过的工具、技术和思维过程。本书能做的,只是展示好程序员的思维过程,还有他们使用的技巧、技术和工具。

第二章、有意义的命名

软件中随处可见命名。我们给变量、函数、参数、类和封包命名。我们给源代码及源码所在的目录命名。我们给jar文件、war文件和ear文件命名。我们命名、命名、不断命名。既然有这么多命名要做,不妨做好它。

  • 命名必须有意义且规范,否则会难以理解和阅读
1
2
3
4
5
6
7
8
//反面教材
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : list1) {
list1.add(x);
}
return list1;
}
1
2
3
4
5
6
7
8
9
10
//正面教材 开发一款扫雷游戏,我们发现,盘面是名为list1的单元格列表,状态值4表示“已标记”
public List<int[]> getFlaggedCells(){
List<int[]> flaggedCells = new ArrayList<int[]>();
for(int[] cell : gameBoard){
if(cell[STATUS_VALUE] == FLAGGED){
flaggedCells.add(cell);
}
return flaggedCells;
}
}
  • 避免使用List等关键字,因为它在代码中有特殊含义,可以替换为group等
  • O o 0 L l 1 傻傻分不清 慎用!!!
  • 避免自己造词,会导致可读性大幅下降
  • 使用可以搜索到的名称 短字母e等会大量出现,导致查找困难
1
2
3
4
5
6
7
8
9
10
11
12
13
//对比以下代码
for (int j = 0; j < 34; j++) {
s += (t[j] * 4) / 5;
}
//对比以下代码
int realDaysPerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j = 0; j < NUM_OF_TASKS; j++) {
int realTaskDays = taskEstimates[j] * realDaysPerIdealDay;
int realTaskWeeks = realTaskDays / WORK_DAYS_PER_WEEK;
sum += realTaskWeeks;
}
  • 在正常的循环计数器中通常使用i、j、k命名,没有问题,其他地方则需要谨慎使用

第三章、函数

  • 函数的第一规则是要短小。第二规则是还要更短小
  • 函数应该做一件事。做好这件事。只做这一件事。
  • 避免使用三个或以上的函数参数,如果需要使用时应该封装为类
  • 避免向函数中传入Boolean值,别无选择时,应当把函数一分为二
  • 适当使用动词和关键字,例如 assertEqual 改为 assertExpectedEqualsActual(expected, actual) 能更方便阅读和记忆
  • if语句中的指令和查询分开写,避免理解错误
1
2
3
4
5
6
7
//如果有一个这样的函数
if(set("username", "unclebob"))....
//会分不清时候 这个函数是判断时候成功设置成unclebob了 还是时候设置成unclebob成功了
//为了防止混淆
if(attributeExists("username")){
setAttribute("username", "unclebob");
}
  • 使用try/catch异常捕获来代替返回错误码
  • 把try/catch中的try和catch代码块分别抽离出来形成函数避免搞乱结构

第四章、注释

什么也比不上放置良好的注释来得有用。什么也不会比乱七八糟的注释更有本事搞乱一个模块。什么也不会比陈旧、提供错误信息的注释更有破坏性。

  • 注释不能美化糟糕的代码
  • 注释应当简洁明了的描述代码内容
  • 能用函数和变量或变量的时候就别用注释
  • 用源代码控制系统代替作者信息的注释
  • 不用的代码直接删除!切勿注释

第五章、格式

  • 代码格式应向报纸学习,由许多篇文章组成,多数短小精悍
  • 每一块代码之间的空行大幅增加阅读性
  • 紧密相关的代码应该互相靠近而不是注释分隔开
  • 实体类应当统一放置 java为顶部
  • 如果一个函数调用了另一个,就应该把它们放到一起
  • 被调用的函数应当放在调用函数的下方

第六章、对象和数据结构

  • 下方代码漂亮之处在于,你不知道该实现会是在矩形坐标系还是在极坐标系中,可能两个都不是!然而,该接口还是明白无误的呈现了一种数据结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//具象点
public class Point(){
private double x;
private double y;
}
//抽象点
public interface Point(){
double getX();
double getY();
void setCartesian(double x, double y);
double getR();
double getTheta();
void setPolar(double r, double theta);
}
  • 得墨忒耳定律,方法不应调用由任何函数返回的对象的方法,以下为错误示例

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

  • 上面的代码常被称作火车失事,应当做切分处理
  • 但是如果仅仅访问其内部的数据结构,那么不适用于得墨忒耳定律,以下为正确示例

final String outputDir = ctxt.options.scratchDir.absolutePath;

第七章、错误处理

  • 使用异常处理来代替使用返回码
  • 不要用try/catch语句控制流程
  • 避免返回null值,可以用Collections.emptyList()进行代替
  • 避免使用null值进行传递

第十章、类

  • 类应当短小,避免使用没有用到的方法
  • 单一职权原则,每个类负责一个小功能
  • 类应该只有少量实体变量,类中的每个方法都应该操作一个或多个这种变量

第十一章、系统

  • 将系统的构造和使用分离开
    • 通过使用工厂设计模式
    • 通过依赖注入
    • java代理

第十三章、并发编程

对象是过程的抽象。线程是调度的抽象

  • 并发防御原则,并发代码问题解决方案
    • 采用单一职权原则,建议分离并发相关代码和其他代码
      • 并发相关代码有自己的开发、修改和调优生命周期
      • 开发相关代码有自己要对付的挑战,和非并发相关代码不同
    • 限制数据使用的作用域,方法之一是采用synchronization关键字保护,谨记数据封装,严格限制对可能被共享的数据的访问
      • 可能会忘记保护一个或多个临界区
      • 需要花费更多力气保证一切都受到保护
      • 很难找到错误源,也很难判断错误源
    • 使用数据副本,一开始就避免共享数据
    • 每个线程尽量独立运行,不与其他线程共享数据
  • 了解并发编程中的几种执行模型
    • 生产者-消费者模型
      • 一个或多个生产者线程创建某些工作,并放置于缓存队列中。一个或多个消费者线程从队列中获取资源并完成这些工作。生产者和消费者之间的队列是一种限定资源
    • 读者-作者模型
      • 当存在一个主要为读者线程提供信息,但是只偶尔被作者线程更新的共享资源,吞吐量就会是个问题。这是一个辛苦的平衡工作
    • 宴席哲学家
      • 一群哲学家坐在圆桌旁,每个人只有一个叉子,面前有一大碗意大利面。哲学家们思索良久,直至肚子饿了。每个人都要拿起叉子吃饭,但是除非手上有两把叉子,否则就没法进食。需要等别人吃完,放回叉子。

?后续没有再看了,中文的翻译真挺差的说实话