前言
让软件能工作和让软件保持整洁,是两种截然不同的工作。我们中的大多数人脑力有限,只能更多的把精力放在让代码能工作上,这也没啥问题。问题是,大多数人在程序能工作时就以为万事大吉了。
将本书与《重构…》《设计模式…》 一起看,相互印证,会有豁然开朗的感觉。比如要重构一段代码,重构、使其符合某个设计模式、使代码更整洁,这些都是统一的。
《clean code》 主要为我们分享:
- 什么是整洁代码
- 在函数、类、系统、并发等层面上,如何编写整洁代码
代码层面的整洁
- 函数只做该函数名下同一抽象层上的步骤,函数中的语句都要在同一抽象层级上。
-
给变量、函数、类起一个有意义的名字,名称应与抽象层级相符。这个知道的多,做到的少。
- 比如测试类的测试方法写一个test就完事
- 最理想的参数数量是0,其次是一,再次是二,应尽量避免三。为何?参数与函数名通常处在不同的抽象层级,它要求你了解目前并不特别重要的细节。从测试角度看,参数更让人为难。
- 应当避免布尔参数(或枚举等类似选择行为的参数),它明显表示函数做了不止一件事
-
输出参数更让人难以理解。
- 在面向对象编程之前的岁月里,有时的确需要输出参数。然而,面向对象语言中对输出参数的大部分需求已经消失了,因为this也有输出函数的意味在内。
- writeField(outputStream,name) 可以把writeField写成outputStream的成员之一;把outputStream 写成当前类的成员变量,从而无需传递它;还可以分离出FieldWriter类,在其构造方法中采用outputStream,并且包含一个write方法。
- 函数要么做什么事,要么回答什么事儿,这二者不可兼得。不可能兼得的原因不是写起来有问题,而是用起来经常会带来可读性损失。
- 重复可能是软件中一切邪恶的根源,面向对象的继承、aop、面向组件编程多少都是消除重复的策略
- 写代码和写别的东西很像,在写论文或文章时,你先想写什么就写什么,然后再打磨它。初稿也许粗陋无序,你就斟酌推敲,直至达到你心目中的样子。关键是,你别写完代码就写别的代码去了
- 如果你做好了上述细节,那么基本不需要注释,注释是一种失败,原因很简单:程序猿不能坚持维护注释。
- 代码格式关乎沟通,而沟通是代码开发者的头等大事,而不是“让代码能工作”。
- 如果一个函数很长,那么通常需要拆分。一个变量列表很长,通常也需要拆分。
-
错误处理
- 使用异常而非返回码
-
测试代码和生产代码一样重要,它需要被思考、设计和照料,没有了测试,你就很难做改动。
- 测试应该有布尔值输出,你不应查看日志文件来确认测试是否通过
- 正常的业务代码中,或许应为方便测试留有一席之地。比如,一个对象虽然理论上只需提供get方法,但为了方便测试,可以提供set方法以直接注入demo数据。
这些具体细节与《重构——改善既有代码的设计》是相辅相成的
- 前者突出编写代码(代码还未完成),后者突出重构。
- 以方法为例,前者要求“方法参数越少越好,不要超过三个”(“坏味道”描述的比较明确),后者提出重构方法的各种技巧。
- 知道重构的技巧并不是难点,这又回到了老问题:知易行难,还是知难行易。
类
内聚比较直观的定义:类应该只有少量实体变量,类中的每个方法都应该操作一个或多个这种变量。通常而言,方法操作的变量越多,就越黏聚到类上。
保持函数和参数列表短小的策略,有时会导致为一组子集方法所用的实体变量数量增加,出现这种情况时,往往意味着至少要有一个类要从大类中挣扎出来。
假设一个有许多变量的大函数function,你想把该函数中某一部分拆解成单独的函数subFunction。不过,subFunction使用了function中的4个变量,这4个变量都作为参数传递到subFunction么?只需要将这4个变量提升为实体变量,并将4个实体变量和subFunction一起抽出一个类即可。所以,拆函数往往跟拆类是一起的。保持内聚性,就会得到许多短小的类。
对象和数据结构
对比以下两个定义:
public class Point{
public double x;
public double y;
}
public class Point{
private double x;
private double y;
public void setX(double x){
this.x = x;
}
public double getX(){
return x;
}
public void setY(double y){
this.y = y;
}
public double getY(){
return y;
}
}
public interface Point{
double getX();
double getY();
void setCartesian(double x,double y);
double getR();
double getTheta();
void setPolar(double r,double theta);
}
第一段和第二段代码本质是一样的,第三段代码漂亮之处在于:
- 你不知道该实现会是在矩形坐标系中,还是在极坐标系中,可能两个都不是!然而,该接口还是明白无误的呈现了一种数据结构。
- 固定了一套存取策略,你可以单独读取某个坐标,但必须通过一次原子操作设定所有坐标。
无论出于怎样的初衷,setter/getter都把私有变量公开化,诱导外部函数以过程式程序使用数据结构的方式使用这些变量。
隐藏实现并非只是在变量之间放上一个函数层那么简单,隐藏实现关乎抽象!第一/二段代码更像数据结构,暴露其数据,没有提供有意义的函数。第三段代码,对象,把数据隐藏于抽象之后,暴露操作数据的函数(暴露行为)。暴露什么,不暴露什么,直接暴露,还是间接操作,都关于抽象。
过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数,面向对象代码便于在不改动既有函数的前提下添加新类。老练的程序猿知道,一切都是对象只是一个传说,合适的才是最好的。比如dto,本身就是为了传输数据存在的,全是setter/getter 方法。
行为 | 数据 | |
---|---|---|
对象 | 暴露 | 隐藏 |
数据结构 | 没有 | 暴露 |
隐藏什么,就容易更改什么。暴露什么,就难以新增什么。
系统层面的整洁
-
构造和使用分开
- 一栋写字楼在建设时,有建筑工人和起重机,而在使用时只有白领和玻璃幕墙。启动代码、类构造代码很特殊,不要和使用逻辑混杂在一起。
- 这或许是spring 在整洁代码这块的意义
-
系统新增功能不可避免,但是否可以不和原有代码写在一起呢?
- 原有的代码,类尽量内聚。模块边界尽量清晰。
- 通过aop等办法,将新业务逻辑和原来的业务代码织入到一起。这要求,合理的切分关注面,模块化系统性关注面
- 良好的抽象,通过提供新的实现类的方式新增功能
并发代码的整洁
- 对象是过程的抽象,线程是调度的抽象
- 并发是一种是一种解耦策略,它帮助我们把做什么(目的)和何时(时机)分开。
- 分离线程相关代码和线程无关代码
并发编程的难点
public class x{
private int lastIdUserd;
public int getNextId(){
return ++lastIdUserd;
}
}
所谓线程相互影响,是因为线程在执行getNextId时有许多路径可行,需要理解Just-in-time编译器如何对待生成的字节码,还要理解java内存模型认为什么东西具有原子性。
就生成的字节码而言,对于在getNextId方法中执行的两个线程,有12870种不同的可能执行路径,如果lastIdUsed 的类型从int变为long,则可能路径的数量则增至2704156种。当然,多数路径都得到正确的结果,问题是其中一些不能得到正确结果。
这一段,让笔者想到了数据库事务,它满足ACID特性。
- 对于java,开发人员直接写的是代码,会被翻译成字节码。字节码分为指令和数据两个部分。无特殊指令,os不保证指令的原子性,数据的可见性;jvm 提供的字节码指令类似。
- 对于数据库,开发人员直接写的是sql,会被解释为执行计划。数据库保证事务(sql序列)的ACID
它们的一个共同点是:虽然可以实现,但jvm不会直接保证一行java代码是原子的,数据库不会直接保证一个sql执行是原子的。尤其是代码,原子性和可见性(反映在sql中类似隔离性)的保证需要开发人员介入。
并发模型
大部分并发问题,都是这三个问题的变种
- 生产者消费者模型
- 读者作者模型,共享资源主要为读者提供信息源,偶尔被作者线程更新。
- 宴席哲学家。前两者多少带点协作性质,哲学家模型则纯粹是对资源的分时使用
小结
在重构过程中,可以应用有关优秀软件设计的一切知识:提升内聚性,降低耦合度,切分关注面,模块化系统性关注面,缩小函数和类的尺寸,选用更好的名称。以达到:可测试、消除重复,保证表达力,尽可能减少类和方法的数量,优先级从高到低。
controller ==> service ==》 dao 后遗症
- 过程式代码,基本无需抽象也能完成工作
- object bean 概念的引入,简化了j2ee开发,但也将“抽象” 阉割了。bean只有setter和getter,大量类创建和转换代码出现在service、工具类的方法中。比如将A转换为B,一般写成
B AUtils.transfer(A a)
,AUtils获取A的属性使用get方法等,transfer代码远没有写在A中清爽。
我们说,面向对象的一个基本特性是抽象,本文为抽象提供了更丰富的内涵
- 函数只做该函数名下同一抽象层上的步骤,函数中的语句都要在同一抽象层级上。
- 方法名和变量名应与抽象层级相符