首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

在计算时代的早期,程序员基于语句思考编程问题。到了20世纪七八十年代,程序员开始基于子程序去思考编程。进入21世纪,程序员以类为基础思考编程问题。

类是由一组数据和子程序构成的集合,这些数据和子程序共同拥有一组内聚的、明确定义的职责。类也可以只是由一组子程序构成的集合,这些子程序提供一组内聚的服务,哪怕其中并未涉及共用的数据。成为高效程序员的一个关键就在于,当你开发程序任一部分的代码时,都能安全地忽视程序中尽可能多的其余部分。而类就是实现这一目标的首要工具。

本章将就如何创建高质量的类提供一些精辟的建议。如果你是刚刚开始接触面向对象的概念,那会觉得本章的内容比较难懂。所以请一定先阅读第5章“软件构建中的设计”,然后再阅读第6.1节“类的基础:抽象数据类型(ADT)”。之后,你就应该可以比较轻松地阅读剩余各节了。如果你已经对类的基础知识比较熟悉,那么可以略读第6.1节后深入阅读第6.2节关于类接口的论述。另外,在本章最后一节“更多资源”中还包含对其他一些初级读物、高级读物以及与特定编程语言相关的资料介绍。

Class Foundations: Abstract Data Types (ADTs)

类的基础:抽象数据类型

抽象数据类型(ADT, abstract data type)是指一些数据以及对这些数据所进行的操作的集合。这些操作既向程序的其余部分描述了这些数据是怎么样的,也允许程序的其余部分改变这些数据。“抽象数据类型”概念中“数据”一词的用法有些随意。一个ADT可能是一个图形窗体以及所有能影响该窗体的操作;也可以是一个文件以及对这个文件进行的操作;或者是一张保险费率表以及相关操作等等。

要想理解面向对象编程,首先要理解ADT。不懂ADT的程序员开发出来的类只是名义上的“类”而已——实际上这种“类”只不过就是把一些稍有点儿关系的数据和子程序堆在一起。然而在理解ADT之后,程序员就能写出在一开始很容易实现、日后也易于修改的类来。

传统的编程教科书在讲到抽象数据类型时,总会用一些数学中的事情打岔。这些书往往会像这么写:“你可以把抽象数据类型想成一个定义有一组操作的数学模型。”这种书会给人一种感觉,好像你从不会真正用到抽象数据类型似的——除非拿它来催眠。

把抽象数据类型解释得这么空洞是完全丢了重点。抽象数据类型可以让你像在现实世界中一样操作实体,而不必在低层的实现上摆弄实体,这多令人兴奋啊。你不用再向链表中插入一个节点了,而是可以在电子表格中添加一个数据单元格,或向一组窗体类型中添加一个新类型,或给火车模型加挂一节车厢。深入挖掘能在问题领域工作(而非在底层实现领域工作)的能量吧!

Example of the Need for an ADT

需要用到ADT的例子

为了展开讨论,这里先举一个例子,看看ADT在什么情况下会非常有用。有了例子之后我们将继续深入细节探讨。

假设你正在写一个程序,它能用不同的字体、字号和文字属性(如粗体、斜体等)来控制显示在屏幕上的文本。程序的一部分功能是控制文本的字体。如果你用一个ADT,你就能有捆绑在相关数据上的一组操作字体的子程序——有关的数据包括字体名称、字号和文字属性等。这些子程序和数据集合为一体,就是一个ADT

如果不使用ADT,你就只能用一种拼凑的方法来操纵字体了。举例来说,如果你要把字体大小改为12point),即高度碰巧为16个像素(pixel),你就要写类似这样的代码:

currentFont.size = 16

如果你已经开发了一套子程序库,那么代码可能会稍微好看一些:

currentFont.size = PointsToPixels(12)

或者你还可以给该属性起一个更特定的名字,比如说:

currentFont.sizeOnPixels = PointsToPixels(12)

但你是不能同时使用currentFont.sizeInPixelscurrentFont.sizeInPoints,因为如果同时使用这两项数据成员,currentFont就无从判断到底该用哪一个了。而且,如果你在程序的很多地方都需要修改字体的大小,那么这类语句就会散布在整个程序之中。

如果你需要把字体设为粗体,你或许会写出下面的语句,这里用到了一个逻辑or运算符和一个16进制常量0x02

currentFont.attribute = CurrentFont.attribute or 0x02

如果你够幸运的话,也可能代码会比这样还要干净些。但使用拼凑方法的话,你能得到的最好结果也就是写成这样:

currentFont.attribute = CurrentFont.attribute or BOLD

或者是这样:

currentFont.bold = True

就修改字体大小而言,这些做法都存在一个限制,即要求调用方代码直接控制数据成员,这无疑限制了currentFont的使用。

如果你这么编写程序的话,程序中的很多地方就会到处充斥着类似的代码。

Benefits of Using ADTs

使用ADT的益处

问题并不在于拼凑法是种不好的编程习惯。而是说你可以采用一种更好的编程方法来替代这种方法,从而获得下面这些好处:

可以隐藏实现细节  把关于字体数据类型的信息隐藏起来,意味着如果数据类型发生改变,你只需在一处修改而不会影响到整个程序。例如,除非你把实现细节隐藏在一个ADT中,否则当你需要把字体类型从粗体的第一种表示变成第二种表示时,就不可避免地要更改程序中所有设置粗体字体的语句,而不能仅在一处进行修改。把信息隐藏起来能保护程序的其余部分不受影响。即使你想把在内存里存储的数据改为在外存里存储,或者你想把所有操作字体的子程序用另一种语言重写,也都不会影响程序的其余部分。
 

改动不会影响到整个程序  如果想让字体更丰富,而且能支持更多操作(例如变成小型大写字母、变成上标、添加删除线等)时,你只需在程序的一处进行修改即可。这一改动也不会影响到程序的其余部分。

让接口能提供更多信息  currentFont.size = 16这样的语句是不够明确的,因为此处16的单位既可能是像素也可能是磅。语句所处的上下文环境并不能告诉你到底是哪一种单位。把所有相似的操作都集中到一个ADT里,就可以让你基于磅数或像素数来定义整个接口,或者把二者明确地区分开,从而有助于避免混淆

更容易提高性能  如果你想提高操作字体时的性能,就可以重新编写出一些更好的子程序,而不用来回修改整个程序。

让程序的正确性更显而易见  验证像currentFont.attribute = current-
Font.attribute or 0x02
这样的语句是否正确是很枯燥的,你可以替换成像currentFont.SetBoldOn()这样的语句,验证它是否正确就会更容易一些。对于前者,你可能会写错结构体或数据项的名字,或者用错运算符(用了and而不是or),也可能会写错数值(写成了0x20而不是0x02)。但对于后者,在调用current-
Font.SetBoldOn()
时,唯一可能出错的地方就是写错方法(成员函数)名字,因此识别它是否正确就更容易一些。

程序更具自我说明性  你可以改进像currentFont.attribute or 0x02这样的语句——把0x02换成BOLD或“0x02所代表的具体含义”,但无论怎样修改,其可读性都不如currentFont.SetBoldOn()这条语句。

WoodfieldDunsmoreShen曾做过这样一项研究,他们让一些计算机科学专业的研究生和高年级本科生回答关于两个程序的问题:第一个程序按功能分解为8个子程序,而第二个程序分解为抽象数据类型中的8个子程序(1981)。结果,按那些使用抽象数据类型程序的学生的得分比使用按功能划分的程序的学生高出超过30%

无须在程序内到处传递数据  在刚才那个例子里,你必须直接修改current-
Font
的值,或把它传给每一个要操作字体的子程序。如果你使用了抽象数据类型,那么就不用再在程序里到处传递currentFont了,也无须把它变成全局数据。ADT中可以用一个结构体来保存currentFont的数据,而只有ADT里的子程序才能直接访问这些数据。ADT之外的子程序则不必再关心这些数据。

你可以像在现实世界中那样操作实体,而不用在底层实现上操作它  你可以定义一些针对字体的操作,这样,程序的绝大部分就能完全以“真实世界中的字体”这个概念来操作,而不再用数组访问、结构体定义、TrueFalse等这些底层的实现概念了。

这样一来,为了定义一个抽象数据类型,你只需定义一些用来控制字体的子程序——多半就像这样:

currentFont.SetSizeInPoints(sizeInPoints)

currentFont.SetSizeInPixels(sizeInPixels)

currentFont.SetGBoldOn()

currentFont.SetBoldOff()

currentFont.SetItalicOn()

currentFont.SetItalicOff()

currentFont.SetTypeFace(faceName)

这些子程序里的代码可能很短——很可能就像你此前看到的那个用拼凑法控制字体时所写的代码。这里的区别在于,你已经把对字体的操作都隔离到一组子程序里了。这样就为需要操作字体的其他部分程序提供了更好的抽象层,同时它也可以在针对字体的操作发生变化时提供一层保护。

 

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论