第一章 整洁代码
"我可以列出我留意到的整洁代码的所有特点,但其中有一条是根本性的,整洁的代码总是看起来像是某位特别在意他的人写的.几乎没有改进的余地,代码作者设么都想到了,如果你企图改进它,总会回到原点,赞叹某人留给你的代码" ---Michael Feathers
"整洁的代码只做好一件事" ---Bjarne Stroustrup
第二章 有意义的命名
1 变量名要名副其实
即顾名思义,变量.函数或者类的名称应该能告诉你,它为什么会存在,他做什么事,应该怎么用,名称应该尽量不需要注释补充亦能表示其含义.
类名和对象应该是名词或者名词短语,方法名应该是动词或者动词短语.
2 避免误导
//不好的命名 public boolean stateMachine(int smStatus) { //... } public boolean doAction() { //... } //好的命名 public boolean moveStateMachineToStatus(int smStatus) { //... } public boolean doNextStepInProviderTechnique() { //... }
3 做有意义的区分
4 使用能读得出来的名称
名字中不要有冗余,定义一个List类型变量,xxxList不如xxx的复数形式简洁,List字眼就属于冗余字段.
5 命名布尔变量
使用常见的布尔含义的命名: 如done;error;found;success;
可以添加前缀is,has来转换布尔,但是isSuccess不如success可读性好
避免使用负向变量命名:如notFound,设想if(!notFound)是不是觉得别扭.负向条件比正向条件更加难于理解,应该尽量避免.
第三章 函数
1 短小
这里是指一个函数的代码量,不是指函数名称要短小.函数应该是在做一件从语义上无法再次拆解的事情.
2 一个函数只做一件事
函数应该做一件事,做好这一件事,而且只做这一件事.
写多长的函数比较合适?
约定:不超过一屏幕,长度并不是问题,关键在于函数名称和函数体之前的语义距离,函数应该很短,也可以较长.只要保证函数只做这一件事.
拆分函数的技巧?
寻找注释,如果函数中某部分代码前面有一行注释,一般可以把这段代码替换成一个函数.此外,当感觉需要注释来说明一些什么的时候,就要考虑把要说明的这些东西封装成一个独立的函数.
3 每个函数一个抽象层级
这一点需要多一些说明,要确保函数只做一件事,函数的语句要在同一个抽象层级上.
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String fileId = request.getParameter("FILEID"); if (fileId == null) { throw new ServletException("Invalid FileId"); } String partNum = request.getParameter("PART"); int part = 1; if (partNum != null) { part = Integer.valueOf(partNum).intValue(); } boolean isLast = "Y".equals(request.getParameter("LAST")); boolean getName="Y".equals(request.getParameter("GETNAME")); String fileName = MitoDownloadCache.INSTANCE.getFileName(fileId, part); if (fileName== null) { throw new ServletException("Invalid FileName"); } MitoConfig mitoCfg = new MitoConfig(); File file = new File(mitoCfg.getPrintClientDataPath()+"/"+fileName); if (!file.exists()) { throw new ServletException("File " + file.getAbsolutePath() + " not found"); } if (getName) { doDownloadFilename(request, response, file.getName()); } else { if (isLast) { MitoDownloadCache.INSTANCE.removeFileList(fileId); } doDownload(request, response, file); file.delete(); }
上面这个方法是我copy来的,不用研究这个函数的内容,只看一下结构,这个函数中既有比较高抽象层次的函数doDownloadFilename,doDownload,还有getFileName这种位于中间抽象层次的函数,更有很多像equals等较低抽象层次的概念.混杂了不同抽象层次,读起来就比较吃力,现在来改造一下:
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { File file = fileToDownLoad(request); if (downloadByName(request)) { doDownloadFilename(request, response, file.getName()); } else { removeFromCacheIfLast(request); doDownload(request, response, fileToDownLoad(request)); file.delete(); } }
将功能拆分成红能更小更细的函数,封装成私有方法,起一个见名知义的函数名,使整个方法能够位于同一个抽象层次,然后在每个函数内部都跟着下一级抽象层次的函数,这样能给阅读带来极大的便利.(上面函数中私有方法这里不再展示)
4 使用描述性名称
函数越小,功能越集中,就越容易取一个合适的名字.另外不要害怕长名称,描述性很好的长名称比含糊不清的短名称要好.不要害怕花时间给函数起名字,起一个合适的名字本身就是编程的一部分.
5 函数输入参数
最理想的函数参数是0个(无参函数),其次是一个,二个,应该尽量避免三个以上的参数.
尽量避免标示参数,即用一个boolean类型作为入参,比如下面这个函数,eat(true);这样读起来就有些摸不着头脑,可能读者需要去查看eat方法eat(boolean isHuntry)才能比较理解这个函数.
如果函数必须要三个或三个以上的参数,就说明其中一些参数需要封装成类,如果很难封装成类,就说明这个函数的设计有问题.
6 函数输出参数
面向对象语言的方法应该尽量避免使用输出参数,当然那些util类型的工具类除外.this也有输出函数的意味,如果函数必须要修改某种状态,就修改所属对象的状态吧.
7 抽离try/catch代码块
try/catch代码块比较丑陋,搞乱了代码结构,最好把try/catch代码块的主体部分抽离出来,另外形成函数.
函数应该只做一件事,错误处理就是一件事,因此,如果处理错误(带try/catch)的函数应该只处理错误,该函数只有try/catch不应该再包含其他内容.
8 使用更少的代码
//实例1:不好的代码格式 public int size() { if (root == null) { return 0; }else { return root.numSiblings(); } } //实例1:好的代码风格 public int size() { return root != null ?root.numSiblings() : 0; }
//实例2 不好的代码 List<Integer> list = Arrays.asList(1, 2, 3)); list = Collections.unmodifieableList(list); return list; //实例2 好的代码 import static java.util.Arrays.*; import static java.util.Collections.*; ... return unmodifiableList(asList(1, 2, 3))
实例2中给出的例子有些函数式编程的味道,这里只是提供一种思路,但大多数情况我们一般写出来的都是上面的格式.
第四章 注释
不写无意义的注释,什么是无意义的注释,如下:
//当我写这段代码的时候,只有老天和我自己知道我在做什么
//现在,只剩老天知道了
...
//我不对以下代码负责
//使他们逼我写的,是违背我意愿的
注释尽量做到简洁
最好不写注释
代码结合命名应该良好的描述功能,写注释说明表达意图的失败以及对代码功能的不自信,糟糕的代码是注释存在的动机之一.
什么是必须要写的注释
警告他人 版权协议 公共API
第五章 格式
1 格式的目的
代码格式很重要,代码格式关乎沟通,而沟通是开发者的头等大事.
2 垂直格式
阅读代码都是从上往下,从左往右读的.在一个类中,在封包声明,导入声明,和每个函数之前都应该使用一个空白行来隔开.这可以给阅读带来愉悦的感受.
空白行隔开了概念,每个空白行都是一条线索,标示出一个新的独立的概念,阅读代码的时候,我们的目光总是容易停留在空白行的前一行或后一行.
变量声明 :变量声明应该尽量靠近其使用位置.类变量应该声明在类的顶部,
相关函数:某函数A调用了函数B,应该尽量把他们放到一起,让A位于B的上方.
概念相关:概念相关的代码应该放到一起,相关性越强,他们之间的距离就应该越短.
避免过度缩进(代码*):
//不好的代码风格 public void startCharging() { if (customer.hasFunds()) { if (!station.isCharging()) { if (!station.currentlyBooked()) { reallyStartCharging(); return; } } } throw new UnableToStartException(); } //好的代码风格 public void startCharging() { if (!customer.hasFunds()) throw new UnableToStartException if (station.isCharging()) throw new UnableToStartException if (station.currentlyBooked()) throw newUnableToStartException reallyStartCharging(); }
3 横向格式
一行代码应该多宽?
应当遵循无需拖动滚动条到右边的原则,每行代码控制在100个字符以内是良好的风格.
第六章 对象和数据结构
数据抽象
当我们构建一个实体类时,会为变量设置为private,再为变量提供set/get方法,想一想为什么要这么做?实际上,即使变量都是私有,我们通过变量的set/get方法操作变量时,实现仍然被曝光了,那为何不直接将变量设置为public,然后直接操作变量呢?
隐藏实现并非只是在变量之间放上一个函数层那么简单,更大的意义在于抽象,类并不是简单的用set/get方法将变量推向外间,而是暴露抽象接口,使得用户无需了解数据的实现就能够操作数据本体.
德墨忒尔定律(The Law of Demeter)
得墨特定律认为,模块不应该了解它所操作的对象的内部情况.对象隐藏数据,曝光操作.
类C的方法f只应该调用以下对象的方法
- C
- 由f创建的对象
- 作为参数传递给f的对象
- 由C的实体变量持有的对象
方法不应该调用任何由任何函数返回的对象的方法,一个很经典的比喻就是:不要和陌生人说话,只和朋友说话,还有一个比喻就是,人可以命令狗走路,但人不能直接命令狗的腿去走路,而应该由狗来控制自己的腿去走路.
如果代码中充斥着"."构成的链式编程风格的代码.意味着该定律被被违反了.当然如果调用者是数据结构,没有行为只有数据,就可以忽略.