《面向对象的思考过程(原书第4版)》一2.1 清楚接口和实现之间的区别-阿里云开发者社区

开发者社区> 华章出版社> 正文

《面向对象的思考过程(原书第4版)》一2.1 清楚接口和实现之间的区别

简介: 正如第1章所示,构建健壮的面向对象设计的关键之一是理解接口和实现之间的不同。因此,当设计类时,应该向用户暴露什么、隐藏什么是非常重要的。而封装与生俱来的数据隐藏机制可以对用户隐藏不必要的数据。 小心不要混淆接口与图形化用户接口(graphical user interface,GUI)这两个概念。

本节书摘来自华章出版社《面向对象的思考过程(原书第4版)》一书中的第2章,第2.1节,[美] 马特·魏斯费尔德(Matt Weisfeld) 著黄博文 译更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.1 清楚接口和实现之间的区别

正如第1章所示,构建健壮的面向对象设计的关键之一是理解接口和实现之间的不同。因此,当设计类时,应该向用户暴露什么、隐藏什么是非常重要的。而封装与生俱来的数据隐藏机制可以对用户隐藏不必要的数据。
小心不要混淆接口与图形化用户接口(graphical user interface,GUI)这两个概念。虽然GUI名称本身包含了接口这个单词,但是我们所说的接口是一种更通用的术语,它并不局限于图形化接口。
还记得第1章中的烤面包机例子吗?烤面包机(或相似功能的设备)需要插入一个接口,即电源插座,如图2-1所示。所有需要电的设备都需要符合正确的接口,即电源插座。烤面包机不需要知道插座的任何实现,或者电是如何产生的。对所有烤面包机而言,它不关心电是燃煤电厂还是核工厂生产的,只关心具体接口可以正确、安全的工作就行。


cfee047df91cdff195e82cb842fb8b0da59ab062



还有一个汽车的例子。司机和汽车之间具有很多接口,比如方向盘、油门踏板、刹车和点火开关。先抛开美观问题,大多数人开车时的主要关注点是启动、加速、停止、转向等。大部分司机中极少关心那些眼睛看不到的组件(实现)。事实上,大多数人根本就无法识别出某些组件,比如催化器和垫圈。然而,任何司机必须清楚如何使用油门踏板,因为这是一个通用接口。制造厂商为车安装一个标准的油门踏板,确保市场上的目标客户能够使用这个系统。
然而,如果制造厂商决定安装一个操纵杆来代替油门踏板,大多数司机会不习惯这点,那么这个车型销量不会很广(只能博得一些喜欢打破常规的人的喜爱)。而如果制造厂商替换了汽车的引擎(改变了部分实现),只要没有改变性能和外观,大多数司机都不会注意到这点。
只要接口不变,可替换的引擎必须严格遵守接口。把四缸发动机替换为八缸发动机可能会改变接口规则,导致需要使用该引擎接口的其他组件不能正常工作。而在发电厂例子中从交流电(AC)改成直流电(DC)也会影响接口规则。
引擎属于实现,方向盘属于接口。改变实现不会对司机造成影响,而改变接口则会。司机会注意到方向盘的外观变化,即使改变可能很微小。必须强调的是,对引擎的改变不应让司机注意到,否则就会破坏接口。例如,改变引擎可能会丧失动力,这会引起驾驶者的注意,事实上是改变了接口。
用户能看到什么
接口与类直接相关。终端用户通常看不到任何类,只会看到GUI或者命令行。然而,程序员会接触类接口。重用类的前提是有人已经写了一个类。因此,程序员必须知道如何正确使用这个类。程序员需要将很多类组合成一个系统,所以需要理解类的接口。因此,本章中讨论用户时,我们主要指设计人员和开发人员,没必要引入终端用户。因此,当我们在此上下文中讨论接口时,我们在讨论类接口,而不是GUI。
正确地设计类时要注意两部分,即接口和实现。

2.1.1 接口

呈现给终端用户的服务暴露了接口。最佳实践中,只呈现给用户需要的服务。当然,不同的人对用户需要什么服务可能持有不同看法。如果你把10个人放到一个屋子让他们每个人进行独立设计,你可能会得到10份完全不同的设计,而且这些设计都没什么错。然而,作为一个通用的规则,一个类的接口应该只包含需要用户知道的东西。在烤面包机例子中,用户只需要知道烤面包机必须插到接口上(这个例子中接口就是电源插座)以及如何操作烤面包机本身。
识别用户
当设计类时最重要的考虑就是识别类的读者(或用户)。

2.1.2 实现

实现细节对于用户是隐藏的。我们必须时刻牢记关于实现的一个目标,那就是修改实现不需要变动用户代码。看起来可能有些困惑,但该目标是设计问题的核心所在。如果对接口的设计是恰当的,那么即使调整了实现,用户调用代码也无需任何改变。请记住,接口包含了调用方法及返回值的语法。如果没有改变接口,用户无需关心是否修改了实现。程序员只关心使用相同的语法能够获得相同的值即可。
我们可以拿手机来举例。打电话的接口很简单,我们只需拨一个号码或者从地址簿中选取一个联系人。如果供应商更新了软件,它不会改变你打电话的方式。无论如何修改实现,打电话的接口始终保持不变。然而,如果我的电话区号变了,供应商也有可能会修改接口。基础接口变了(比如电话区号变了),需要用户改变行为。商家希望保持这样的修改最小化,因为有些用户不喜欢这种改变,或者不想这样的麻烦。
再说烤面包机的例子。只要接口一直是电源插座,具体实现就可能会从一个燃煤电厂切换为核电站,但这不会影响烤面包机。这里有一个非常重要的规则,即煤电厂和核电厂都必须要遵循接口规格。如果煤电厂提供交流电(AC),而核电厂提供直流电(DC),就会出问题。用户和实现都必须要遵循接口规格。

2.1.3 一个接口/实现示例

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


c1b8964964646af52a47b8cc729026737d4de66d



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

0166c1a35cf8c7e05bf52b8b07e923ca83c7c452


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

华章出版社

官方博客
官网链接