代码整洁之道_06对象和数据结构

第六章 对象和数据结构

  • 对象和数据结构的区别是啥?
  • 什么时候用对象,什么时候用数据结构?
  • 面向对象还是面向过程?

1. 数据抽象

隐藏实现并非只是在变量之间放上一个函数层那么简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//代码1:具象点
public class Point{
public double x;
public double y;
}

//代码2: 抽象点
public interface Point{
double getX();
double getY();
void setCartesian(double x,double y);
double getR();
double getTheta();
void setPolar(double r, double theta);
}

代码2的漂亮之处在于,你不知道该实现会是在矩形坐标系中还是在极坐标系中。可能两个都不是,然而,该接口还是明白无误地呈现了一种数据结构。

代码1暴露了实现,并且,即便变量都是私有的,通过取值器和赋值器使用变量,实现也被暴露了。

注意隐藏实现并非只是在变量之间放上一个函数层那么简单。隐藏实现关乎抽象!类并不简单地用取值器和赋值器将其变量推向外界,而是暴露抽象接口,以便用户无须了解数据的实现就能操作数据本体(essence)。

2. 数据结构、对象的对立

  • 对象把数据隐藏于抽象之后,暴露数据的函数;
  • 数据结构暴露其数据,没有提供有意义的函数;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 代码3:过程式 形状代码
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();
}
}

以上代码3特征:

  • Geometry类添加函数,不会影响形状类;
  • 若要增加形状,就得修改Geometry中的所有函数;

以下代码4特征:

  • 添加新形状,不影响现有函数;
  • 添加新函数,所有形状都得做修改;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 代码4:多态式 形状代码
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;
}
}

结论:

  1. 过程是代码:便于在不改动既有数据结构的前提下添加新函数;
  2. 面向对象代码:便于在不改动既有函数的前提下添加新类;

一切都是对象的说法只是一个传说——老练的程序员

3. The Law of Demeter

  • 模块不应了解他所操作对象的内部情形。
  • 对象不应通过存取器暴露其内部结构,因为这样更像是暴露而非隐藏其内部结构。
  • 方法不应调用由任何函数返回的对象的方法。

3.1 火车失事

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

模块知道ctxt对象包含多个选项,每个选项中都有一个临时目录,每个临时目录都有一个绝对路径。对于一个函数,这些知识够丰富了。调用函数需要懂得在如何在一大堆不同对象间浏览。

如果 ctxt、Options和ScratchDir只是数据结构,没有任何行为,则它们自然会暴露其内部结构,Demeter Law 也就不适用了。

如果数据结构只简单地拥有公共变量,没有函数,而对象则拥有私有变量和公共函数,那么这个问题就没那么复杂了。

3.2 混杂

混杂:一半是对象,另一半是数据结构;

这种结构拥有执行操作的函数,也有公共变量或公共访问器及改值器,增加了添加新函数的难度,也增加了添加新数据结构的难度,两头不讨好。

3.3 隐藏结构

假使ctxt、Options和ScratchDir是拥有真实行为的对象又怎样呢?由于对象应隐藏其内部结构,我们就不该看到内部结构。这样一来,如何才能取得临时目录的绝对路径呢?

如果ctxt是一个对象,就应该要求它做点什么,而不该要求它给出内部情形。因此,我们看它获取绝对路径是要干啥:

1
2
3
String outFile = outputDir + "/" + className.replace('.', '/') + ".class";
FileOutputStream fout = new FileOutputStream(outFile);
BufferedOutputStream bos = new BufferedOutputStream(fout);

破案了:创建指定名称的临时文件!

那么,直接让ctxt对象来做这事如何?

1
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);

4. 数据传送对象 DTO

最为精炼的数据结构,是一个只有公共变量、没有函数的类——数据传送对象(Data Transfer Objects,DTO)。

DTO是非常有用的结构,尤其是在与数据库通信或解析套接字传递的信息之类的场景中,在应用程序代码里一系列将原始数据转换为数据库的翻译过程中,它们往往是排头兵。

5. 小结

在任何系统中,希望能灵活地添加新数据类型,则使用对象;希望能灵活地添加新行为,则使用数据类型和过程。优秀的软件开发者不带成见地了解这种情形,并依据手边工作的性质选择其中一种适合的手段。