10 March 2014

偷个懒,这篇博文直接取自《代码整洁之道》第六章的标题,事实上,本文也是对这一章做了一个总结,或者算是篇笔记吧。

这一章讲的东西虽然不多,但会让人晕头转向。事实上,只要你注意看本章的开篇段落,就会发现后面讲了半天都在讲什么。比如我们命名变量时,将其限定为private,这种做法的意义在于使得其他人不能访问这些变量。我们将这些变量隐藏起来,作为这个类的“私有财产”,有时(应该是多数时候),我们甚至不希望外界知道这些变量。

于是,作者提出了这样的问题,为什么还是有那么多程序员给对象自动添加赋值器和取值器?所谓赋值器和取值器,类似类中的方法,比如我们有个私有变量info,我们又写了一些方法如setInfo(赋值)或getInfo(取值)。

好吧,你可能觉得这没什么,也许你经常还会这么做。但事实上,这样的方法在不知不觉中透露了类中抽象数据(变量),这与我们的本意是不符的。来看两个例子:

示例1:

public interface Vehicle {
	double getFuelTankCapcityInGallons();
	double getGallonsOfGasoline();
}

示例2:

public interface Vehicle {
	double getPercentFuelRemainning();
}

初看只是两个接口而已,但通过这些接口的命名,我们发现第一个例子能够清楚知道(至少是猜测)出这些都是变量的存取器(甚至是这个类都拥有哪些变量),而后者只是一个百分比的抽象,我们很难得知其中的数据形态。显然,后者更为合适,它隐藏了数据的细节。

实际上,这里要回到我们的标题,对象和数据。我们希望对象不要暴露其中的数据,而数据结构(相对于对象)本身一定会暴露其中数据,且没有提供什么有意义的函数(方法)。有点绕,要记得对象和数据结构是不同的两类事物。下面两个例子很好展示了这一点:

过程式形状代码(请注意数据结构):

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 Shape) {
			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 r = (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;
	}
}

第一个例子更像C语言代码,几个形状的数据结构(没错,我们所说的数据结构就是类似这里的代码,变量都是public,且没有其他方法)类似C中的结构体。而在Geometry类中,更是过程式编程思想。而在第二个例子中,显然是面向对象的思想,每个类中都定义了私有变量和area方法。显然更适合多态实现。

对于第一个例子,优点在于,如果我们想要添加一个求周长的方法,只需要添加这个方法,而不需要修改形状类(即数据结构),但是当我们添加一个新的形状时,我们不得不对每一个方法都进行修改。

对于第二个例子,优点在于,如果我们想要添加一个新的形状,直接添加即可,不需要关心处理多态那部分的代码。但是,如果我们要添加一个求周长的方法,就需要对所有的类都进行修改。

两种做法各有利弊,如果我们要使用对象的方法(第二例),就要防止暴露其中的数据。即本文开篇所说的问题。

也许你听说过得墨忒耳律(The Law of Demeter)—— 模块不应了解它所操作对象的内部情形。作者举了一个成为“火车失事”的例子:

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

因为它违反了得墨忒耳律,调用了getOptions返回值的getScrachDir函数,又调用getScrachDir返回值getAbsolutePath方法。(有没有发现Android里经常是这样的做的。。。)

最好做如下的切分:

Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();

即使这样做,也并不能保证不违反得墨忒耳律,如果ctxt,Options和ScratchDir是对象还是数据结构(注意上面过程式代码和多态式代码的例子),如果是数据结构且没有其他行为,那就不违反(数据结构的数据都是暴露的)。如果是对象,显然这些方法暴露了太多信息。不相信,那我们来看看这段代码都会暴露什么信息:Options是类(对象),则getScratchDir取得了其中的某个叫做ScratchDir的类(File型),而这个类最终取得AbsolutePath(通过getAbsolutePath方法)。它至少透露了Options类中包含一个返回File对象的方法,和它的一个私有变量(String型的代表绝对路径的私有变量)。

所以,对于这些包含对象的编程,要始终注意隐藏数据。那么到底为什么要隐藏这些数据?因为暴露了抽象接口,用户便无需了解数据的实现就能操作数据本体。这使得本来私有的数据被公之于众,使得我们的本意不能够实现。

最后介绍一种精炼的数据结构,只有公共变量,没有函数的类。其被称为数据传送对象,或DTO(Data Transfer Objects):

public class Address {
	private String street;
	private String city;

	public Address (String street, String city) {
		this.street = street;
		this.city = city;
	}

	public String getStreet() {
		return street;
	}

	public String getCity() {
		return city;
	}
}


blog comments powered by Disqus