注:正文中的引用是直接引用作者Bob大叔的话,两条横线中间的段落的是我自己的观点,其他大约都可以算是笔记了。
本文中的函数
和方法
是一个概念
本文读起来可能比较晦涩,其实通篇只是讲了一件事情:在面向对象的环境里有两种方法去定义一个类,面向对象(本文中一直谈到的对象
)和面向过程(本文中谈到的数据结构
),它们各有优劣,在开发的时候要合适地做出选择。
由于「Clean Code」整本书都有很浓厚的Java的色彩,所以大部分代码和概念都是Java中比较常见的,不过在面向对象的语言中大致都能找到相应的东西
数据抽象
我们在设计对象
的结构时,应该尽可能地使用数据抽象。如代码5-1中所示的两种对于笛卡尔平面中的一个点的数据结构定义,从这里可以看到,使用抽象的类定义,这个类就不仅仅是一个数据结构
了。通过暴露出来的方法强制了对于数据的设置必须x轴和y轴同时设置,它可以代表一个平面坐标系中的一个点,也可以代表极坐标系中的一个点。
//代码5-1
//具象类定义
public class Point {
public double x;
public 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);
}
面向对象概念中的「隐藏实现」的真实意义不应该只是在变量中增加了一层函数,而是「数据抽象」。数据抽象也不仅仅是使用一些interface
和getter
、setter
就可以的,它需要你认真仔细去思考「如何才能更好地表示一个对象所包含的数据」。
「数据结构」和「对象」的反对称性
Bob大叔在这一小节讲得很玄乎,阐述了一个道理:过程式编程和面向对象编程是互补的两个概念。「一切皆对象」只是一个神话,我们有时候不可避免的要用到过程式的代码(也就是本文一直提到的数据结构)来补充。
数据结构
和对象
是两个相反的概念,对象
隐藏了数据并暴露了对数据操作的函数,而数据结构
暴露了它的数据但并没有有意义的函数。这两个概念互为相反,但又相辅相成的。
过程式代码(使用
数据结构
的代码)的优势在于「当你想要添加新的函数时,不需要修改已存在的数据结构」,而面向对象的代码的优势在于「当你添加新的类时,不需要修改已存在的函数」。
下面通过一个例子解释它们的反对称性。
//代码5-2
//过程式的Shape类
public class Square {
public Point topLeft;
public double side;
}
public class Rectangle {
public Point topLeft;
public double height;
public double width;
}
public class Circle {
public Point center;
public double radius;
}
public class Geometry {
public final double PI = 3.141592653589793;
public double area(Object shape) throws NoSuchShapeException {
if (shape instanceof Square) {
Square s = (Square)shape;
return s.side * s.side;
}
else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle)shape;
return r.height * r.width;
}
else if (shape instanceof Circle) {
Circle c = (Circle)shape;
return PI * c.radius * c.radius;
}
throw new NoSuchShapeException();
}
}
//多态版本的Shape类
public class Square implements Shape {
private Point topLeft;
private double side;
public double area() {
return side*side;
}
}
public class Rectangle implements Shape {
private Point topLeft;
private double height;
private double width;
public double area() {
return height * width;
}
}
public class Circle implements Shape {
private Point center;
private double radius;
public final double PI = 3.141592653589793;
public double area() {
return PI * radius * radius;
}
}
代码5-2中展示了两种Shape类的实现方法:
第一种是传统的过程式的编程方法(也就是
数据结构
)。如果要在Geometry类中添加一个计算周长的方法,是不需要修改具体实现类(Square、Rectangle、Circle)中的任何代码,但是如果你要添加一个新的形状的实现类,那么就要在Geometry类中的每一个方法都作出修改。第二种是面向对象的多态方式的编程方法(也就是
对象
)。与第一种刚好相反(反对称性),如果要在Shape类中添加一个计算周长的方法,那么每一个实现类中的代码都需要修改,但是如果你要添加一个新的形状的实现类,那么现有的其他代码都不需要修改。
迪米特法则
迪米特法则可以概括为「不要和陌生人说话」。
精确的讲,迪米特法则规定一个类C
中的方法f
应该只调用以下几种方法:
-
C
中的方法 -
f
中生成的对象的方法 -
f
的参数对象的方法 -
C
持有其引用的对象中的方法
1. 火车事故
如代码5-3中代码段1这样的链式的耦合性极强的调用方法,我们通常称之为火车事故「train wrecks」,这类链式的调用应该禁止,应该将其重构为代码2这种形式。
Build模式和JQuery中的链式调用和并不适用这条规则,因为他们从头到尾所有方法的调用者都是同一个对象。
代码5-3中ctxt
这个对象操作了多个层级的函数,违反了迪米特原则。但是,如果ctxt
,Options
和ScratchDir
这三个类都是简单的数据结构(如代码5-2中过程式的类定义),并不包含任何的行为的话,这样的调用并不违反迪米特法则。
我们平常使用的很多编程框架中要求所有的「实体类」都要添加getter和setter,这使得判断一个调用是否违反迪米特法则变得很困惑。作者认为作为简单的数据结构,比如实体类Pojo,就应该只包含public属性。
//代码5-3
//代码段1 这种强耦合的链式调用应该被禁止使用
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
//代码段2
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
2. 数据结构和对象的杂交
有时候我们会创建这样的杂交类,其中有包含复杂逻辑的方法,同时又包含public属性或者public的getter和setter,应该避免创建这样的类。
3. 隐藏结构
对象应该隐藏自己的内部结构。
具体到代码5-3中所说的例子,作者认为从ctxt这个对象获取一个文件的绝对路径的本身就是不对的,绝对路径可能是ctxt的内部结构,我们可能是想使用这个绝对路径来构造一个对象,那么如果代码是这样子的:
String outFile = outputDir + "/" + className.replace('.', '/') + ".class";
FileOutputStream fout = new FileOutputStream(outFile);
BufferedOutputStream bos = new BufferedOutputStream(fout);
我们可以把它封装成函数,这样来调用:
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
传输数据的对象(Data Transfer Objects)
DTO是指那些只包含公共变量且没有函数的类,这是一类很有用的数据结构。但是现在更广为使用的则是Bean
(类的变量为私有,但是含有公共的getter和setter),它起到的作用其实和DTO相同。
Active Record
这是DTO的一种特殊形式,在上边DTO讨论的基础之上,还包括一些save或find这样一些浏览方法。
它和DTO一样都属于数据结构而非对象,所以不要在其中添加复杂的逻辑。
结论
如果未来很有可能需要不断地往一个类中添加新的对象,那么就选择对象
结构;如果未来可能需要不断的改变类的行为,就选择数据结构
。