
2.1.3 一个接口/实现示例
我们来创建一个简单的(如果不是很实用的)读取数据库的类。我们将编写一些Java代码,这些代码会从数据库中获取记录。正如之前讨论的一样,在进行任何设计时,识别终端用户一直是最重要的问题。你可能需要做一些场景分析,与终端用户进行面谈,然后列出这个项目的需求。接下来是我们对这个数据库阅读器的需求:
·必须能打开到数据库的连接
·必须能关闭到数据库的连接
·必须能将光标指向数据库中的第一条记录
·必须能把光标指向数据库中的最后一条记录
·必须能得到数据库中的记录条数
·必须能知道当前数据库中是否仍有记录(当前指向的是否是最后一条记录)
·必须能够根据键值把光标指向特定的记录
·必须能够获取指定键值的记录
·必须能基于当前光标的位置获取下一条记录
根据以上需求,可以开始尝试设计一个读取数据库的类,为终端用户设计可能的接口。
在本例中,读取数据库的类仅提供给想使用数据库的程序员。因此接口本质上是程序员想要使用的应用程序编程接口(Application-Programming Interface,API)。这些方法其实封装了数据库系统暴露的功能。为什么要这么做?在本章的后面我们会详细讨论该问题。简短的回答是我们需要定制数据库功能。例如,我们必须处理对象从而可以将它们写入到关系型数据库中。编写这样的中间件对于设计和编码而言可能并不简单,但这是封装特性的真实示例。最重要的是,如果我们想替换数据库引擎,则无须修改大量代码。
图2.2展示了一个类图,表示了Data-BaseReader类的潜在接口。
请注意该类中的方法都是公共方法(请记住靠近方法名的加号表示该方式是一个公共接口)。而且这里只展示了接口,没有展示任何实现。请花一分钟来确定这个类图是否能大体满足上面列出的项目需求。如果你之后发现该类图没有满足所有需求也没关系。因为面向对象设计是一个迭代的过程,所以你无须一开始就保证它绝对正确。

图2.2 DataBaseReader类的UML类图
公共接口
请记住如果一个方法是公共方法,那么程序员就可以访问它,因此可以认为它是类接口的一部分。请不要混淆术语接口(interface)与Java和.NET中的关键字interface。稍后的章节会讨论关键字interface。
对于我们列出的每个需求,需要有对应的方法来提供对应的功能。现在需要考虑一些问题:
·作为一名程序员,为了更有效地使用这个类,还需要知道关于这个类的其他信息吗?
·需要知道打开数据库的内部数据库代码吗?
·需要知道数据库代码对应的一条具体记录的物理位置吗?
·需要知道内部数据库代码如何确定是否还有剩余记录吗?
回答是都不需要!你不需要知道任何信息。只需要关心能获取正确的值并且操作没有出错。事实上,程序员更喜欢对具体实现再做一层抽象。应用程序将使用你编写的类来打开数据库,进而调用相应的数据库API。
最小接口
在极限情况下,保证最小接口是刚开始不给用户提供任何公共接口。当然这样的类是无用的。然而,这强制用户主动找你说:“我需要这个功能。”然后你们可以协商。这样保证你只在需要的情况下增加接口,绝不要假设用户需要什么东西。
创建包装对象看起来有些过度杀伤,但编写这样的类有很多好处。比如,当今市场上有很多中间件产品使用了包装对象的技术。考虑把对象映射到关系型数据库的问题。面向对象的数据库从未流行起来,理论上它们应该非常适合面向对象的应用程序。然而,一个现存的问题是大多数公司有数年的遗留数据存放在关系型数据库中。如果公司既需要保留关系型数据库中的数据,又要拥抱面向对象技术,那么如何处理这个断层呢?
首先,可以把所有遗留的关系型数据转换到一个全新的面向对象的数据库中。然而,任何遭受过严重的(也是长期的)数据转换之痛的人都知道无论如何都不能这样做。这种转换往往会耗费大量的时间和精力,而到头来系统还是不能正常工作。
其次,可以使用中间件产品把应用程序代码中的对象无缝地映射到关系型模型中。只要关系型数据库依旧盛行,这种方案相比之前就要更好些。有些人可能会辩论说面向对象的数据库比关系型数据库在对象持久化方面更有效。事实上,很多开发系统都能提供这样无缝转换的服务。
对象持久化
对象持久化,意思即保存对象的状态以便稍后可以恢复和使用,因为没有被持久化的对象在其生命周期之外就会被销毁掉。例如,对象的状态可以保存在数据库中。
在当前的业务环境下,关系型数据库和对象建立映射关系是一个非常好的方案。很多公司集成了这样的中间件技术。比如一个公司拥有一个网站作为前端接口,而数据存在大型机中,这很常见。
如果创建一个完全面向对象的系统,使用面向对象的数据库是个可行的选项(也拥有更好的性能)。不过面向对象的数据库的发展经历与面向对象语言的发展经历比起来差远了。
独立应用程序
即使从头创建一个全新的面向对象的应用程序,也很难完全避免遗留数据。新创建的面向对象的应用程序也不会是独立的应用程序,因为它很可能需要获取存储在关系型数据库(或者其他的数据存储设备)中的数据。
让我们回到数据库例子中。图2.2只展示了该类的公共接口,除此之外别无其他。当完成该类后,可能会包含更多的方法,当然也会包含一些属性。不过作为使用该类的程序员无须知道任何私有方法和属性的相关信息。你肯定无须了解公共方法中的具体代码,只需简单知道如何与这些接口交互即可。
公共接口的实现代码会是什么样子呢(假设使用的是Oracle数据库)?我们来看看open()方法:

在这个例子中,如果你是程序员,会发现open方法需要String类型作为参数。Name代表了需要传入的数据库文件,但在本例中我们并不关心它如何被映射到一个具体的数据库。这就是我们需要知道的。现在,有趣的事情来了——真正使接口如此伟大的东西!
为了迷惑用户,我们来修改数据库的实现。昨晚我们把Oracle数据库中的全部数据迁移到了一个SQLAnywhere数据库中(我们忍受了巨大而漫长的痛苦)。虽然总共花费了好几个小时,但最终我们做到了。
现在代码看起来是这样:

令我们非常懊恼的是,今天早上竟然没有收到任何用户的抱怨。这是因为虽然改变了实现,但并未改变接口!用户关心的调用接口仍然是相同的。修改实现代码可能需要大量的工作(即使只改了一行代码,整个模块都需要重新编译),但使用了DataBaseReader类的应用程序代码则无须任何修改。
代码重新编译
动态加载的类是在运行时被加载的,并不是静态链接到一个可执行文件。当使用动态加载的类时(比如Java和.NET),无须重新编译使用者的类。然而在静态链接语言中(比如C++),引入新类则需要一个链接。
分离用户接口与实现,能省却大量令人头痛的事情。在图2.3中,数据库的具体实现对终端用户来说是透明的,终端用户只能看到接口。

图2.3 接口