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

本文涉及的产品
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
简介: 正如第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


相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
6月前
|
安全 Java 数据安全/隐私保护
|
6月前
|
C++
C++ 接口的实现,及作用通俗理解方式
C++中的接口,一般就是指抽象类,是一种用来描述类对外提供的操作、方法或功能的集合——注意,一般只是描述(声明),而不对这些方法或功能进行定义实现,通常在
66 2
|
6月前
|
存储 编译器 C语言
C与C++之间相互调用的基本方法
C与C++之间相互调用的基本方法
122 1
|
程序员 数据安全/隐私保护 C++
C++面向对象封装特性的实例分析与应用扩展(一)
生活中充满复杂性,处理复杂性的方法之一就是简化和抽象。在计算中,为了根据信息与用户之间的接口来表示它,抽象是至关重要的。将问题的本质特征抽象出来,并根据特征来描述解决方案。抽象往往是用户定义类型的捷径,在C++中用户定义类型指的就是实现抽象接口的类设计。
127 1
C++面向对象封装特性的实例分析与应用扩展(一)
Java实现多线程开发的四种方式,详解它们之间异同
Java实现多线程开发的四种方式,详解它们之间异同
Java实现多线程开发的四种方式,详解它们之间异同
|
安全 Java
java面向对象三大特性,封装篇
1.封装的概念 在面向对象程式设计方法中,封装是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。 封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。
103 0
|
编译器 C语言 C++
C++面向对象封装特性的实例分析与应用扩展(二)
封装意义一: 在设计类的时候,属性和行为写在一起,表现事物 语法: class 类名{ 访问权限: 属性 / 行为 }; 示例1:设计一个圆类,求圆的周长
148 0
C++面向对象封装特性的实例分析与应用扩展(二)
|
Java 测试技术 API
接口和抽象有什么区别?
接口和抽象有什么区别?
149 0
接口和抽象有什么区别?
下一篇
无影云桌面