封装与信息的隐藏
封装与信息的隐藏
显示了肆虐一时的SARS病毒结构图。可以看出,病毒可以分成三个层次:一层坚硬的壳层,壳层外呈触角状的蛋白体,以及壳层内部的遗传物质。显然这种构造使得SARS病毒得以隐藏和保护自身内部结构,这是病毒经过漫长时间的演化形成的有效生存机制。
无独有偶,面向对象的软件设计中,对象也可以分成三层,一层由各个方法构成的壳层,暴露在壳层外的各个接口,以及受到保护的内部属性,参见图4.2。
图4.1、SARS病毒模型。(www.who.org) 图4.2、对象由属性和方法组成,对象向外界提供接口以供操作对象内部属性和触发对象的行为。
对象的这种结构,使得封装(Encapsulation)与信息隐藏(Information Hiding)变得可能。封装和信息隐藏常常被混为一谈,但它们并不是一回事。
4.1 封装
到底什么是封装呢?让我们还是从四十年前谈起吧。
如本书在第1章中介绍的,世界上第一个具备面向对象特征的语言是Simula。是Simlua第一次引入了真正意义上的封装。
在六十年代,模块化编程是基本的设计手段,模块化也是Simula的基本设计思想,只是Simula的模块化非常有特色:模块的设计不是建立在过程之上的,而是建立在真实世界中的物体基础之上的。
每一个真实的物体都有一些需要模型化的行为,每一个真实的物体都有一些关于其状态的信息。这些物体相互作用,形成了Simula需要描述的模拟系统。Simula的设计师决定以最为接近真实物体的办法进行模拟系统的设计,这样一来,按照真实物体的组织来组织模块,就是顺理成章的了。
将数据和作用于数据之上的操作绑在一起形成模块,这就是封装。
一场对象技术的革命就是这样开始的。这场革命开始于封装。
一个例子:棋盘上的点
对象是对真实世界中的物体的抽象化,但对象不是照搬真实世界中物体。对象技术中的建模不是把真实世界照搬到软件系统中,对象技术首先要将真实世界理想化、简单化、抽象化,然后把这个改变了的影像搬进软件系统中。
读者一定建国围棋棋子和棋盘吧,本节就考察一下如何为围棋棋盘上的点建模。
图4.1、围棋的一间跳、点三三。 图4.2、Point类的类图
下面的Point类描述在棋盘上的一个点,它带有两条数据,分别代表横纵坐标。这个类可以用UML描述,参见图4.2。
代码清单4.1、Point类描述棋盘上的点
public class Point {
public single x;
public single y;
}
与点相联系的操作有下面这些:
移动(move):将这个点对象移动到另一个位置,就好比把一个围棋子从棋盘上的一个点移动到另一个点。
计算距离(distance):与这个点与另一个点的距离。围棋手需要计算的比这个要复杂得多。
要实现这些方法,需要一个新的工具类,不妨称作PointUtil如下 :
代码清单4.2、操作点的工具类
public class PointUtil {
public double distance(Point p1, Point p2){
return Math.sqrt( (p1.x - p2.x) *(p1.x - p2.x) + (p1.y - p2.y)*(p1.y - p2.y));
}
public void move(Point p, double x, double y){
p.x = x;
p.y = y;
}
}
如果要移动一个点,就把这个点传入PointUtil对象的move方法,同时需要传入的还有新的坐标。这个PointUtil对象会把Point对象移动到新的坐标。
如果要计算一个点到另一个点的距离,就需要把两个点对象传入到PointUtil对象的distance方法中,这个方法会返还你想知道的距离。譬如下面的代码就设定了两个点,并计算出他们的距离:
代码清单4.3、如何计算两个点之间的距离。
public static void main(String[] args){
Point p1 = new Point();
Point p2 = new Point();
p1.x = 10.0F;
p1.y = 20.0F;
p2.x = 100.0F;
p2.y = 200.0F;
PointUtil putil = new PointUtil();
System.out.println(putil.distance(p1, p2));
}
有两种设计师往往会给出类似于上面的这种设计。
熟悉过程性设计概念,初学对象技术的设计者常常会给出类似上面的设计。这种对责任的划分方法明显地带有过程式设计的痕迹,因为虽然设计中也使用了类,给人的感觉是如果Java允许把方法放到类外边去,设计者可能会更高兴。Java语言强制设计师使用类,但是使用了类,并不能保证一个设计就真正体现了对象思想的。
另一类设计者试图原封不动地在软件系统中重现真实世界,他们会认为这是一个好的设计:这难道不是对现实世界的真实描述吗?在现实世界中,围棋子会自己走路吗?难道不是外力移动棋子?棋子难道知道计算距离吗?难道不是一个计算器什么的负责计算距离吗?
不错,在现实世界中是这样的。但是这个现实世界不是一个理想化的,而是一个蹩脚的现实世界,你不想把这样的现实世界原封不动地搬进软件模型中。
在一个理想化的世界中,围棋子难道不应当自己会移动吗?外界只需要传送一个消息给棋子,棋子就自动到位。这虽然是想象中的事情,但是并非距离现实很远,只需要借助于一定的电子科技,完全可以在改进的棋子和棋盘上实现这一点。难道你不想把这个聪明的棋子和棋盘当作原型加以引进吗?
如何划分责任是面向对象的设计与过程性设计的基本区别。如果一个对象包含了某项操作所需的全部数据的话,那么这项操作就应当属于这个对象,而不是属于另一个不能提供所需数据的对象。上面所讨论的move方法需要改变的是Point对象的两个属性:x和y,而这两个属性就在Point对象内,为什么不把move方法交给Point对象呢?
基于同样的道理,计算一个点与另一个点的距离,需要两个点的坐标,而坐标本就是Point对象的属性,为什么不把这项计算任务交给Point对象呢?当然,这就意味着PointUtil类根本就没有用处,可以取消。