代码整洁之道-对象和数据结构
代码整洁之道
第 6 章 对象和数据结构
6.2 数据、对象的反对称性
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();
}
}
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;
}
}
对象把数据隐藏于抽象之后,暴露操作数据的函数。
数据结构暴露其数据,没有提供有意义的函数。
添加一个perimeter()
函数会怎样?
添加一个新形状呢?
对象和数据结构之间的二分原理:
过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类。
反过来讲也说得通:
过程式代码难以添加新的数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类。
在任何系统中,我们有时会希望能够灵活的添加新数据类型,所以更喜欢在这部分使用对象。另外一些时候,我们希望能灵活的添加新行为,这时我们更喜欢使用数据结构和过程。
6.4 数据传送对象
最为精炼的数据结构,是一个只有公共变量、没有函数的类。这种数据结构有时被称为数据传送对象,或DTO(Data Transfer Objects)
。DTO
是非常有用的结构,尤其是在与数据库通信、或解析套接字传递的消息之类场景中。
更常见的是“豆”(bean)结构。豆结构拥有由赋值器和取值器操作的私有变量。
对豆结构的半封装会让某些OO
纯化论者感觉舒服些,不过通常没有其他好处。
public class Address {
private String street;
private String streetExtra;
private String city;
private String state;
private String zip;
public Address(String street, String streetExtra,
String city, String state, String zip) {
this.street = street;
this.streetExtra = streetExtra;
this.city = city;
this.state = state;
this.zip = zip;
}
public String getStreet() {
return street;
}
public String getStreetExtra() {
return streetExtra;
}
public String getCity() {
return city;
}
public String getState() {
return state;
}
public String getZip() {
return zip;
}
}
Java的访问级别修饰符
作用域 | 当前类 | 同一package | 子孙类 | 其他package |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
C#访问级别修饰符
访问修饰符 | 访问级别 |
---|---|
public | 任何地方都可以访问 |
protected internal | 同时具备protected与internal的访问权限 |
internal | 当前程序集内部可以访问 |
protected | 当前类内部及其子类都可以访问 |
private | 只能在当前类内部访问 |
将变量设置为私有(private)
不想其他人依赖这些变量,随时修改其类型或实现。
6.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);
}
而第一段代码很明确的是在矩形坐标系中实现。我们可以单个操作那些坐标,暴露了实现;
我们不知道第二段代码的实现会是在矩形坐标系还是极坐标系中。它呈现的不止是一个数据结构,还固定了一套存取策略,我们可以单独读取某个坐标,但必须通过一次原子操作设定所有坐标。
即便通过变量取值器和赋值器使用私有变量,其实现仍然暴露了。
隐藏实现并非只是在变量之间放上一个函数层那么简单。隐藏实现关乎抽象。类并不简单的用取值器和赋值器将其变量推向外间,而是暴露抽象接口,以便用户无需了解数据的实现就能操作数据本体。
public interface Vehicle {
double getFuelTankCapacityInGallons();
double getGallonsOfGasoline();
}
public interface Vehicle {
double getPercentFuelRemaining();
}
前者使用具象手段与机动车的燃料层通信,后者采用百分比抽象。
你能确定前者里面都是些变量存取器,却无法得知后者中的数据形态。
不应暴露数据细节,应以抽象形态表述数据。这并不只是用接口或者赋值器、取值器就万事大吉。要做严肃的思考,以最好的方式呈现某个对象包含的数据。
6.3 得墨忒耳律(迪米特法则)(The Law of Demeter)
模块不应了解它所操作对象的内部情形。
得墨忒耳率认为,类 C
的方法 f
只应该调用以下对象的方法:
- C
- 由 f 创建的对象
- 作为参数传递给 f 的对象
- 由 C 的实体变量持有的对象
方法不应调用由任何函数返回的对象的方法。换言之,只跟朋友谈话,不与陌生人谈话。
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
这类代码常被称作火车失事,因为它看起来就像是一列火车。不要为了省一两个变量采用这种方式,起一个有意义的变量名,不仅利于阅读代码,还便于调试。最好做类似如下的切分:
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
如果ctxt
是个对象,就应该要求它做点什么,而不是要求它给出内部情形。
那我们为何还要得到临时目录的绝对路径呢?
来看看同一模块(许多行之后)的这段代码:
String outFile = outputDir + "/" + className.replace('.','/') + ".class";
FileOutputStream fout = new FileOutputStream(outFile);
BufferedOutputStream bos = new BufferedOutputStream(fout);
我们发现,取得临时目录绝对路径的初衷是为了创建指定名称的临时文件。
所以,直接让ctxt
对象来做这事如何?
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
ctxt
隐藏了其内部结构,防止当前函数因浏览它不该知道的对象而违反得墨忒耳律。
人可以命令一条狗行走,但是不应该直接指挥狗的腿行走,应该由狗去指挥控制它的腿如何行走。
一个对象应该对其他对象有最少的了解,所以又叫做最少知识原则(Least Knowledge Principle, LKP
)。
降低类之间的耦合。由于每个对象尽量减少对其他对象的了解,因此,很容易使系统的功能模块独立,相互之间不存在(或很少存在)依赖关系。
- 只和直接的朋友交流(从依赖者的角度来说,只依赖应该依赖的对象)
- 减少对朋友的了解(从被依赖者的角度来说:只暴露应该暴露的方法或者属性,即在编写相关的类的时候确定方法/属性的权限)
关闭计算机
当我们按下计算机的关机按钮的时候,计算机会执行一系列的动作:保存未完成的任务、关闭服务、关闭显示器、关闭电源等等。
public class System {
public void saveTask() {
}
public void closeService() {
}
public void closeScreen() {
}
public void closePower() {
}
public void close() {
saveTask();
closeService();
closeScreen();
closePower();
}
}
public class Container {
private System system;
public System getSystem() {
return system;
}
}
public class Person {
private Container container;
public void clickCloseButton() {
System system = container.getSystem();
system.closePower();
}
}
public class System {
private void saveTask() {
}
private void closeService() {
}
private void closeScreen() {
}
private void closePower() {
}
public void close() {
saveTask();
closeService();
closeScreen();
closePower();
}
}
public class Container {
private System system;
public void sendCloseCommand() {
system.close();
}
}
public class Person {
private Container container;
public void clickCloseButton() {
container.sendCloseCommand();
}
}
迪米特法则的核心观念就是类间解耦,弱耦合。只有弱耦合了之后,类的复用才可以提高,类变更的风险才可以减低。但解耦是有限度的,不存在没有耦合的系统。所以在实际项目中,需要适度地参考这个原则,避免过犹不及。