Python 3面向对象编程(txt+pdf+epub+mobi电子书下载)


发布时间:2020-08-09 10:15:41

点击下载

作者:达斯帝·菲利普斯 (Dusty Phillips)

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Python 3面向对象编程

Python 3面向对象编程试读:

前言

这本书将向你介绍面向对象范型的术语,通过一步步的例子,专注于面向对象的设计。它将带你从简单的继承开始,这在面向对象程序员工具箱里是最有用的工具之一,到最复杂之一的合作继承。你将能提高、处理、定义以及操作异常。

你将能够把Python面向对象和不是那么面向对象的方面结合起来。通过研究高级的设计模式,你将能够创建可维护的应用程序。你将学习Python复杂的字符串和文件操作,以及如何区分二进制和文本数据。将会介绍给你不止一个,而是两个非常强大的自动化测试系统。你将会理解单元测试的喜悦以及创建单元测试是多么简单。你甚至会学习像数据库连接和GUI工具包这样的高级库,以及它们是如何应用面向对象原则的。

这本书讲了什么

第1章,面向对象设计覆盖了重要的面向对象概念。它主要处理关于抽象、类、封装和继承。在建模我们的类和对象时,我们也简要地看了下UML。

第2章,Python对象讨论了类和对象,以及它们是如何在Python中使用的。我们将学习Python对象中的属性和行为,以及把类组织成包和模块。最后我们将看到如何保护我们的数据。

第3章,当对象是相似的让我们从更深层次的视角来看继承。它覆盖了多重继承以及向我们展示了如何从内置来继承。这一章还包括了多态以及鸭子类型。

第4章,异常处理讲解异常和异常处理。我们将学习如何创建自己的异常。它还涉及了把异常用于程序流程控制。

第5章,何时使用面向对象编程主要处理对象,什么时候创建和使用它们。我们将看到如何使用属性来包装数据,以及限制数据访问。这一章也会讨论DRY原则以及如何不重复代码。

第6章,Python数据结构覆盖了数据结构的面向对象特征。这一章主要处理元组、字典、列表和集合。我们也会看一看如何扩展内置对象。

第7章,Python里面向对象的快捷方式顾名思义,在Python中如何省时。我们将看到很多有用的内置函数,然后继续在列表、集合和字典中使用解析。我们将学习关于生成器、方法重载,以及默认参数的内容。我们还会看到如何把函数当成对象来用。

第8章,Python设计模式1第一次向我们介绍了Python设计模式。然后我们将会看到装饰器模式、观察者模式、策略模式、状态模式、单例模式以及模板模式。这些模式将会和在Python中实现的合适的例子和程序一起讨论。

第9章,Python设计模式2讲解上一章剩下的内容。我们将通过Python中的例子看到适配器模式、外观模式、享元模式、命令模式、抽象模式以及组合模式。

第10章,文件和字符串学习字符串和字符串格式化。也会讨论字节和字节数组。我们也将会学习文件以及如何从文件读取数据和往文件里写数据。我们将学习存储和pickle对象,最后本章会讨论序列化对象。

第11章,测试面向对象的程序以使用测试和为什么测试如此重要开头。重点是测试驱动开发。我们将看到如何使用unittest模块,以及py.test自动化测试套件。最后我们将使用coverage.py来看代码的测试覆盖率。

第12章,常用Python 3库集中介绍库以及它们在程序构建中的利用率。我们将使用SQLAlchemy来构建数据库,使用TkInter和PyQt开发用户界面。这一章会继续讨论如何构建XML文档以及我们应该如何使用ElementTree和lxml。最后我们将使用CherryPy和Jinja来创建一个Web应用。

对于这本书你需要什么

为了编译运行在本书中提到的例子,你需要下面的软件:

● Python 3.0或者更高的版本

● py.test

● coverage.py

● SQLAlchemy

● pygame

● PyQt

● CherryPy

● lxml

谁需要这本书

如果你是面向对象编程技术的新手,或者你有基本的Python技巧,并且希望深入地学习如何以及什么时候在Python中正确地应用面向对象编程,这本书适合你。

如果你是一个其他语言的面向对象编程人员,你也会发现这本书是对Python的一个有用的介绍,因为它使用了一些你已经熟悉的术语。

那些寻求迈入Python 3新世界的Python 2程序员也将会发现这本书的好处,但是其实你不需要了解Python 2。

惯例

在这本书中,你将发现一些用于区分不同种类信息的文本风格。这里有一些这些风格的例子,并且介绍了它们的意义。

文本形式的代码会按照下面显示:“我们可以通过使用import语句来访问Python的其他模块”。

一个代码块会像下面这样显示: class Friend(Contact): def __init__(self, name, email, phone): self.name = name self.email = email self.phone = phone

当我们希望你对一个代码片段里特殊的部分引起注意的时候,相应的行或者元素会设置成粗体: class Friend(Contact): def __init__(self, name, email, phone): self.name = name self.email = email self.phone = phone

任何一个命令行的输入或输出都是下面这样的形式: >>> e = EmailableContact("John Smith", "jsmith@example.net") >>> Contact.all_contacts

新的术语或者重要的词将会显示成黑体。例如你在屏幕、菜单和对话框中看到的词,会以文本的形式这样显示:“你每一次点击Roll!按钮的时候,我们会通过这个特性来给标签更新一个新的随机值”。警告或者重要的笔记将会这么显示。小窍门和诀窍会这么显示。

读者反馈

我们随时欢迎来自读者的反馈信息。请告诉我们你对本书的评价——你喜欢或者不喜欢的地方。读者的反馈对于我们是非常重要的,这会帮助我们为你创作更有价值的作品。

请将你的反馈意见通过邮件feedback@packtpub.com发送给我们,并在邮件的主题中标明相关图书的名称。

如果你需要一本书并且希望我们出版相关图书,请在www.packtpub.com用SUGGEST A TITLE表格发一个纸条给我们或通过电子邮件suggest@packtpub.com告诉我们。

如果你具有关于某个主题的专业知识,并且有兴趣参与图书的编写,请查看我们的作者指南,网址:www.packtpub.com/authors。

客户支持

你现在是拥有Packt书的尊贵主人,购买本书我们会从以下方面帮助到你。下载本书的示例代码

你可以通过你在http://www.packtpub.com的账号来下载你购买图书的所有示例代码文件。如果你从其他地方购买的本书,你可以访问http://www.packtpub.com/support并且注册,我们会直接通过电子邮件发给你文件。

勘误

虽然我们已尽力确保我们内容的准确性,但是错误还是会发生。如果你发现本书的一个错误——文字或者代码错误——如果你能报告给我们,我们将非常感激。通过这样做,你可以把其他读者从挫败中拯救出来并且帮助我们改善这本书的后续版本。如果你发现任何错误,请通过访问http://packtpub.com/support来报告它们,选择你的书,点击let us know链接,并且输入错误的细节。一旦你的勘误被核实,你提交的信息将会被接收并且将会把勘误上传到我们的网站,或者在添加到一个已有的勘误列表的相应主题部分。从http://www.packtpub.com/support选择我们的信息标题将看到任何一个存在的勘误。

盗版

互联网上著作权侵害是在所有媒体中持续存在的问题。在Packt,我们会非常重视对版权和许可证的保护。如果你在互联网上发现了对于我们著作的任何形式的任何非法复制,请立即告诉我们地址或者网站名字,以便我们能进行补救。

请通过copyright@packtpub.com联系我们,可附带一个可疑链接的盗版材料。

我们非常感谢你在保护我们的作者以及我们能带给你有价值内容的能力方面给予我们的帮助。

问题

如果你对这本书的任何方面有问题,你可以通过questions@packtpub.com联系我们,我们将尽力解决这个问题。第1章面向对象设计

在软件开发过程中,通常认为在编程之前要完成设计。这种说法并不准确;在实际中,分析、编程和设计趋向于重叠、结合和交织。通过本章内容,我们会学到:

● 面向对象是什么意思。

● 面向对象设计和面向对象编程的不同。

● 面向对象设计的基本原则。

● 基本统一建模语言并且当它不是一个麻烦的时候。面向对象

所有人都知道对象是什么:某种我们可以感知、触摸和操纵的有形的东西。我们接触到的最早的对象是典型的婴儿玩具。木质积木、塑料形体及过量的拼图都是常见的第一对象。婴儿可以很快地学习到,特定的对象可以做特定的事情。三角形物体能准确地放到三角形的孔里。铃可以响、按钮可以按,以及手柄可以扳。

在软件开发中,对象的定义也没有太大的差别。对象通常不是你可以捡起、感知或者触摸的有形的东西,而是这些东西的模型,这些模型可以做特定的事情并且特定的事情可以作用于它们身上。正式地讲,对象是一个数据以及相关行为的集合。

现在我们知道了什么是对象,那么什么是面向对象呢?面向简单的意思就是指向。所以面向对象简单的意思就是“功能上指向建模对象”。通过数据和行为来描述交互对象的集合,这是用于对复杂系统建模的众多技术之一。

如果你读过任何的宣传资料,那么你可能已经碰到了面向对象分析、面向对象设计、面向对象分析设计以及面向对象编程这些术语。在一般的面向对象范畴里,这些都是高度相关的概念。

事实上,分析、设计和编程是软件开发的整个过程。称它们是面向对象是简单指定了软件开发所追求的风格。

面向对象分析(OOA)是着眼于一个问题、系统或者任务的过程,一些人想要把这个过程变成一个应用,并且识别这些对象以及对象间的交互。分析阶段全是关于需要做些什么的。分析阶段的输出是一套需求。如果我们想一步就完成分析阶段,我们需要把一个任务,比如“我需要一个网站”,变成这样一套需求:

访问这个网站的人需要可以做以下事项(以楷体字代表行为,黑体代表对象):

● 回顾历史。

● 申请工作。

● 浏览、比较以及订购我们的产品。

面向对象设计(OOD)是把上面这些需求转化为实践规范的过程。设计者必须命名这些对象、定义行为并且正式地指定这些对象可以作用于其他对象的具体行为。设计阶段全是关于事情应该如何做的。设计阶段的输出是实践规范。如果我们想一步就完成设计阶段,我们需要把需求转化成一套类和接口,这些类和接口可以通过任何(理想情况)面向对象编程语言来实现。

面向对象编程(OOP)是把这个完美定义的设计转化成一个可以工作的程序的过程,这个程序恰好做了CEO最初要求做的事情。

是啊,没错!如果这个世界符合我们的想象,并且就像旧教科书告诉我们的那样,可以井井有条地一步接一步遵循这些阶段,那就太美好了。通常,现实世界要残酷得多。不管我们多努力地区分这些阶段,我们总是发现当我们准备设计的时候,事情还需要进一步分析。当我们开始编程的时候,发现在设计中,一些特性需要澄清。在这个快节奏的现代世界,大部分开发刚好是一个迭代式开发模型。在迭代式开发中,会先模块化、设计和编程实现任务中一个小的部分,然后评估这个程序并且通过扩展来改善每一个特性,包括在一系列短的周期里增加新的特性。

这本书剩下的内容是关于面向对象编程,但是在本章,我们会讲解在设计背景下一些基本的面向对象原则。这将会让我们理解这些相当简单的概念,并且无须争论软件语法或者解析器。对象和类

因此,一个对象是一个有着相应行为的数据的集合。我们如何区分两类对象呢?苹果和橘子都是对象,但是它们没法比较却是一个常识。在计算机编程中,很少会建模苹果和橘子,但是假装我们正在为一个水果农场做一个货物清单的应用,作为一个例子,我们假设苹果会放到桶里,橘子会放到篮子里。

现在我们有4种对象:苹果、橘子、篮子、桶。在面向对象建模中,用于各种对象的术语叫作类。所以从技术术语来讲,我们现在有4个对象的类。

对象和类有什么不同呢?类描述了对象。它们就像是创建对象的模板。可能会有3个橘子放在你面前的桌子上。每一个橘子都是一个不同的对象,但是所有这3个对象却有着关联同一个类的属性和行为:橘子的通用类。

可以用统一建模语言(Unified M odeling L anguage)(因为通过首字母缩写非常酷,一般简称UML)类视图,来描述在我们货物清单里的4个类的对象之间的关系。下面是我们的第一个类视图:

这个视图简单地展示了一个橘子以某种方式和一个篮子有关系,一个苹果以某种方式和一个桶有关系。关联是把两个类联系起来最基本的方式。

UML这个工具在管理者之间非常流行,但偶尔会被编程人员蔑视。一个UML视图的语法通常是相当明显的;你通常(大多数情况)不需要通过去阅读一个指南来理解你看到的UML的意思是什么。画UML也是非常简单和直观的。毕竟,很多人在描述类以及它们之间关系的时候,会自然而然地画一些用线连起来的方框。对于这些直观的视图有一个标准,会让程序员和设计者、老板,以及其他程序员之间交流起来非常简单。

但是,一些程序员认为UML是浪费时间。以迭代式开发为例,他们会争辩说,在实施之前,这些正式的规范以花哨的UML视图来实现是多余的,并且维护这些正式的视图只会浪费时间,不会有任何好处。

在一些组织机构里这是正确的,在一些其他公司的文化里这就是猪食。然而,每一个编程团队由不止一个人组成,偶尔需要坐下来并且讨论解决他们正在工作的系统的部分细节。在这些头脑风暴会议中,为了快速和容易沟通,UML是非常有用的。即使那些嘲笑这种正式的类图的组织机构,在他们的设计会议或者团队讨论里,也倾向于使用一些非正式颁布的UML。

此外,你需要交流的最重要的人是你自己。我们都认为可以记住我们做出的设计决策,但是总是会有“为什么我要这么做?”的时刻,隐藏在我们的将来。当我们开始设计的时候,如果我们保存了写有我们初始设计图表的纸片,我们最终会发现它们是一个有用的参考。

然而,本章并不是想成为一个UML的教程。在互联网上有很多材料,以及众多相关话题的书籍可用。UML覆盖的远比类和对象图多;它也有语法、部署、状态变化以及活动的用例。在这个面向对象的设计讨论中,我们会处理一些常见的类视图的语法。你会发现,你可以通过例子获取结构,并且在你自己的团队或者个人的设计会议中,你会下意识地选择UML风格的语法。

我们最初的图虽然正确,但是并没有提醒我们苹果可以放到桶里或者一个苹果可以放到多少个桶里。类之间的关系通常是明显并且不需要进一步解释的,但是进一步澄清的选项总是存在的。UML的美妙之处在于大部分的东西是可选的。我们只需要在一个图里指出尽可能多的信息,来使它在当前情况下是有意义的。在一个快速的白板会议上,我们可能只是快速地画一些条条框框。我们可能会在正式文档里涉及更多的细节,需要6个月才有意义。在这个苹果和桶的例子里,我们可以相当确信的关系是,“很多苹果可以放到一个桶里”,但是为了确保没有人将其与“一个苹果占据一个桶”相混淆,我们可以通过下面的图强调这一点:

这个图告诉我们橘子放进篮子里,通过一个小箭头显示什么放进什么。它也告诉我们双方关系的多样性(可以关联在一起的对象的数量)。一个篮子可以放很多(通过一个*表示)橘子对象。任何一个橘子可以放到一个篮子里。

很容易混淆多样性是哪一边的关系。一个类可以通过关系中其他端的任何一个对象关联在一起,这个类的对象数量就表示多样性。对于苹果放进桶这个关联,从左读到右,很多个苹果类的实例(很多苹果实例)可以放进任何一个桶里。从右往左读,就是一个桶可以与任何一个苹果关联。指定属性和行为

现在我们已经掌握了一些基本的面向对象术语。对象是类的实例,可以彼此相互关联。一个对象实例是一个带有它自己数据集合和行为的特定对象;一个特定的橘子放到我们面前的桌子上,说明它是一般的橘子类的一个实例。这非常简单,但是,什么是与每一个对象关联的数据和行为呢?数据描述对象

让我们先从数据开始。数据通常代表了一个给定对象的个体特征。一个类的对象可以定义这个类的所有实例所共享的一些特定特征。对于给定的这些特征每一个实例可以给它们不同的值。例如,放在桌子上(如果我们还没有吃掉它们)的3个橘子可能各有不同的重量。橘子类可能会有一个重量的属性。橘子类的所有实例都有一个重量属性,但是每一个橘子的重量可能会不同。属性不必是唯一的,任何两个橘子可能重量是相同的。作为一个更现实的例子,代表不同客户的两个对象可能第一个名字属性的值相同。[1]

属性通常称为特征 。一些作者认为这两个词具有不同的含义,通常属性是可变的,特征是只读的。在Python中,“只读”的概念并没有真正使用,所以在本书中我们将会看到两个术语交叉使用。此外,在第5章中,对于一类特殊的属性,特征关键词在Python中有特殊的意义。

在我们的水果清单的应用程序里,果农可能想知道橘子是从哪个果园来的,什么时候采摘以及它多重。他们可能也想追踪每一个篮子都存放到哪里。苹果可能会有一个颜色的属性,同时桶可能会有不同的大小。其中一些属性也可能属于多个类(我们也可能想知道苹果是什么时候采摘的),但是对于第一个例子,让我们只是给类图添加一些不同的属性:

根据我们设计所需的具体程度,我们也可以给每一个属性指定类型。属性类型通常是多数编程语言的标准说法,如整型、浮点型、字符串字节或者布尔型。然而,它们也可以代表一些数据结构,像列表、树或者图,或者最值得注意的是其他的类。这也是一个设计阶段和编程阶段可以重叠的领域。在一种编程语言里可用的对象或者基本类型可能和其他编程语言里可用的对象或基本对象有些许不同。通常在设计阶段我们不需要关心这些,因为在编程阶段会选择具体实现的细节。使用通用的名字就可以了。如果我们的设计需要一个列表容器类型,当实现的时候,Java程序员可以选择使用LinkedList或者ArrayList,[2]而Python程序员(就是我们!)可以选择内置的list或者tuple。

在我们的水果种植例子中,到目前为止,我们的属性都是一些基本的原始类型。但是也有我们可以明确的隐式属性:关联。对于一个给定的橘子,我们可能有一个属性,就是哪个篮子里放着这个橘子。另外,一个篮子里可能会放很多个橘子。下面这张图,就像对我们目前这些特征进行的类型描述一样,会把这些属性添加进去:[3]行为是动作

现在我们知道了什么是数据,但是什么是行为呢?行为是可以发生在对象身上的动作。一个特定的类的对象里可以执行的行为被称为方法。在编程层面,方法就像结构化编程里的函数一样,但是它们可以神奇地访问和这个对象关联的所有数据。方法可以像函数那样接收参数并返回值。

方法的参数是一个对象的列表,需要传递给被调用的方法。方法会使用这些对象来执行任何它想要做的行为或者任务。返回的值是这个任务的结果。这里有一个具体的例子;如果我们的对象是数字,数字类可能会有一个add方法,这个方法接收第二个数作为参数。当第二个数传入的时候,第一个数字对象的add方法会返回这两个数的和。给定一个对象和它的方法名,调用对象的时候也可以调用这个目标对象的方法。在编程层面上,调用一个方法就是通过传给它所必需的参数来使这个方法执行的过程。

在这个基本的清单应用里“比较苹果和橘子”,我们会遇到困难。让我们进一步来扩展它并且看看是否能突破。一个可以和橘子关联在一起的动作就是摘。如果你想一下实现,摘就是通过更新橘子的篮子属性把橘子放进篮子,并且把橘子添加到篮子里的橘子列表。所以摘这个动作要知道它要处理哪一个篮子。因为果农也要销售果汁,我们可以给橘子添加一个挤压方法。当挤压的时候,挤压方法会返回获取的橘汁的量,同时也会把这个橘子从篮子里清除掉。

篮子可以有一个出售的行为。当一个篮子卖出去以后,我们的清单系统可能需要更新一些数据,这些数据是尚未指明的用于会计和利润计算的对象。或者,我们这篮子里的橘子可能会在卖出去之前坏掉,所以我们需要添加一个丢弃方法。让我们把这些方法添加到我们的图里:

给单个对象添加模块或者方法允许我们创建一个交互对象的系统。系统里的每一个对象都是特定类的成员。这些类指明了这个对象可以容纳什么样的数据类型以及什么方法可以调用它。每一个对象的数据相比于同一类的其他对象可以有不同的状态,并且因为这个不同,每一个对象对于方法调用的反应也是不同的。

面向对象分析和设计就是找出这些对象是什么以及它们该如何交互。下一节描述了一些原则,这些原则可以用于使这些交互尽可能简单和直观。隐藏细节并且创建公共接口

在面向对象设计中模块化一个对象的主要目的是决定该对象的公共接口。接口是其他对象可以和该对象交互的属性和方法的集合。它们不需要且通常也不允许去访问对象的内部工作。一个常见的真实世界的例子就是电视。我们电视的接口是遥控器。遥控器上的每一个开关代表了一个可以由电视对象调用的方法。当我们作为调用对象去访问这些方法的时候,我们不知道也不关心这个电视是从天线、电缆还是卫星天线获取信号的。我们不关心发送什么电子信号去调节音量,或者声音是输出到扬声器还是耳机。如果我们打开电视访问它的内部工作,例如将输出信号分为去往外部扬声器以及耳机,我们的保修就失效了。

隐藏一个对象实现或者功能细节的过程,通常称为信息隐藏。它有时也被称为封装,但是实际上封装是一个更加包罗万象的术语。封装数据并不一定是隐藏。封装,夸张点说,是创建一个胶囊,所以想一下创建一个时间胶囊。如果你把一堆信息放到时间胶囊里,锁住并埋了它,它被封装并且信息被隐藏了。另一方面,如果没有把时间胶囊埋了并且没有上锁,或者胶囊是用透明塑料制成的,里面的东西同样被封装了,但是信息没有被隐藏。

封装和信息隐藏的区别不是那么重要,特别是在设计层面。很多实际中的引用使用的术语是可替换的。作为Python程序员,我们不需要或者不必要真正的信息隐藏(我们在第2章里会讨论为什么),所以对封装更多的包装定义是合适的。

然而,公共接口是非常重要的。因为在将来很难改变它,所以需要精心设计。改变接口会导致中断任意客户对象对它的调用。我们可以随意改变内部机制,例如,让它效率更高,或者就像访问本地一样访问网络上的数据,客户对象无须改变就可以使用公共接口与之交互。另一方面,如果我们通过改变公共访问的属性名字,或者改变一个方法可以接收的参数名字或类型来改变接口,那么所有客户对象也将必须修改。

记住,程序对象代表了真正的对象,但是它们不是真正的对象。它们是模型。模型化最好的天赋就是可以忽视一切不相关的细节。一个模型汽车外表可能看起来像一个真正的1956雷鸟,但是它不能跑并且传动轴也不转,因为这些细节过于复杂并且与年轻人组装模型毫无关系。模型是一个真实概念的抽象。

抽象是另外一个和封装以及信息隐藏相关的面向对象词汇。简而言之,抽象意味着要对一个给定任务以最合适的水平来处理细节。它是一个把公共接口从内部细节里抽取的过程。一辆车的司机需要与方向盘、油门和刹车交互。而电动机、传动系统以及制动子系统的工作原理对于司机来讲是无关紧要的。另一方面,对于机械师,优化引擎以及刹车就会有不同水平的抽象。下面是对一辆汽车在两个水平上的抽象:

现在我们有了一些指向相似概念的新术语。所有这些术语凝结成一句话就是,抽象就是通过把公共和私有接口分开而封装信息的过程。私有接口可以用于信息隐藏。

所有这些定义带来的最重要的事情就是使得与我们模型交互的对象能够理解我们的模型。这就意味着要注意小细节。确保方法和属性有合适的名字。当分析一个系统的时候,在原始问题里对象通常代表名词,而方法通常代表动词。属性通常可以作为形容词,尽管如果属性指向了属于当前对象一部分的另一个对象,它仍然可能是一个名词。相应地命名类、属性和方法。不要试图给将来可能有用的对象或者行为建模。准确地建模那些系统需要执行并且设计的任务,建模自然而然会走向一个合适的抽象层次。这并不是说我们不应该考虑未来可能的设计修改。我们的设计应该以开放式结尾,这样可以满足未来的需求。然而,当抽象接口的时候,试着准确地建模那些需要建模的而不要管其他。

在设计接口的时候,试着把自己放到对象里并且想象这个对象对隐私有很强的偏好。不要让其他对象可以访问你的数据,除非你认为这样做对你有最大的好处。不要通过给它们一个接口来强制你自己去执行一个特定的任务,除非你确定你希望它们可以这么做。

对于确保你的社交账户隐私,这也是一个很好的实践。组合和继承

到目前为止,我们已经学会了把系统作为一组交互式对象来设计,每一个交互都是在适当的抽象层次上查看对象。但是我们尚不知道如何创建这些抽象层次。做这个有很多方法;我们会在第8章和第9章讨论一些高级的设计模式。但是大多数设计模式都会依赖两个基本的原则,称之为组合和继承。

组合是把一些对象收集在一起组成一个新对象。当一个对象是另一个对象的一部分的时候,组合是一个好的选择。在机械师的例子里,我们已经看到了第一个组合的暗示。一辆汽车是由引擎、传动、启动器、车灯和挡风玻璃等很多其他部件组成的。接下来,引擎是由活塞、曲轴以及阀门组成的。在这个例子里,组合是提供抽象层次非常好的方式。汽车对象可以提供给司机所需的接口,同时提供了访问它的组成部件的能力,这个能力为机械师提供了一个更深的抽象层次。当然,如果机械师需要更详细的信息来诊断问题或者优化引擎,这些组成部分可以进一步分解。

这是关于组合的第一个常见例子,但是当它用于设计计算机系统的时候,不是一个好的例子。物理对象很容易分解成组件对象。至少从古希腊人最初假定原子是物质的最小单位开始,人们已经这样做了(当然,他们没有使用过粒子加速器)。计算机系统通常没有物理对象那么复杂,然而在这样的系统里识别部件对象也不会很自然地发生。在一个面向对象的系统里,对象偶尔代表物理对象,比如人、书或者电话。然而更常见的是,它们会代表抽象的概念。人有名字,书有标题,电话是用来拨打的。电话、标题、账号、名字、预约以及支付,通常不认为是物理世界中的对象,但是在计算机系统里它们经常被建模为组件。

让我们通过建模一个更面向计算机的例子来看看实际中的组合。我们将看一下一个计算机象棋游戏的设计。这是一个19世纪80年代和90年代学术界非常流行的消遣游戏。人们预测有一天计算机能够击败人类的象棋大师。当这发生在1997年时(IBM的深蓝击败了象棋冠军加里·卡斯帕罗夫),尽管仍有计算机和人类象棋手之间的竞赛,并且程序没有达到能够在所有时间内都打败人类象棋大师,但是在这个问题上人类兴趣索然。

作为一个基本的、高层次的分析:象棋游戏是两个选手之间玩的,在一个8×8的网格里包含64个位置的棋盘上使用一副棋子。棋盘有可以移动的两套16个棋子,两个选手交替以不同的方式移动。每一个棋子可以吃掉其他的棋子。每一次移动,棋盘都要在计算机屏幕上画出它自己。

在描述中,我已经通过使用楷体字确定了一些可能的对象,以及使用黑体字确定了一些关键方法。在一个把面向对象分析变成设计的过程中,这是常见的第一步。在这一点上,为了强调组合,我们将关注于棋盘,而不会关心太多有关玩家或者不同类型棋子的问题。

让我们从可能的最高层抽象开始。我们有两个玩家,通过轮流移动一副棋子来做交互。

那是什么东西?它看起来不像我们之前的类图。这是因为它根本不是一个类图!这是一个对象视图,也称为一个实例图。它描述了一个特定状态、特定时间的系统,描述的是对象的特定实例,而不是类之间的交互。记住,这两名玩家是同一个类的成员,所以类图看起来会有点不同。

这个图确切地显示了两个玩家可以和一套棋子做交互。它也表明,任何玩家一次只能玩一套棋子。

但是我们要讨论组合而不是UML,所以让我们想一下象棋是由什么组成的。当前我们不关心玩家的组成。我们可以假设在玩家的其他器官中有心脏和大脑,但是这和我们的模型无关。事实上,没有人禁止该玩家是深蓝自己,深蓝没有心脏也没有大脑。

然后,象棋是由一个棋盘和32个棋子组成的。棋盘进一步由64个位置组成。你可能会争论说,棋子不是象棋的一部分,因为你可以在象棋里用其他一副棋子把棋子替换掉。虽然在一个计算机版本的象棋里,这是不可能的,但是它给我们引入了聚合。聚合几乎和组合一样。不同的地方是,聚合对象可以独立存在。把一个位置和不同的象棋棋盘相关联是不可能的,所以我们说,象棋棋盘是由位置组成的。但是对于棋子,它可以独立于象棋而存在,而被认为和一套象棋是一种聚合关系。

另外一种区分聚合和组合的方法是考虑对象的寿命。当(内部)创建和销毁相应对象的时候,如果组合(外部)对象控制着它们,那么组合就是最合适的。如果可以独立于组合对象去创建相关对象,或者可以对比对象,一个聚合关系可能会有意义;聚合是组合的一个更一般的形式。任何组合关系同时也是聚合关系,但也不尽然。

让我们描述一下我们目前的象棋组合,并且给对象添加一些属性来保持组合关系:

在UML图里,一个实心菱形代表了组合关系。空心菱形代表了聚合关系。你会注意到棋盘和棋子作为象棋的一部分来存储,它们的引用也以同样的方式作为一个属性存储在象棋里。在实践中,再次表明一旦迈过设计阶段,聚合和组合的不同通常是无关紧要的。当实现的时候,它们几乎也表现得一样。然而,当你的团队在讨论不同对象如何交互时,它可以帮助你们区分它们的不同。通常你可以把它们当成同样的东西对待,但是,当你需要区分它们的时候,也要知道它们的不同(这是在工作中的抽象)。继承

我们已经讨论了对象之间3种类型的关系:关联、组合和聚合。但是我们没有完全定义我们的象棋,并且这些工具似乎并没有给我们所需要的能力。我们讨论过玩家有可能是人或者具有人工智能的软件的可能性。所以说一个玩家和一个人关联,或者人工智能是玩家对象的一部分,看起来都不正确。我们真正需要做到的是能够说“深蓝是一位玩家”或者“加里·卡斯帕罗夫是一位玩家”。

这是一种由继承形成的关系。继承是众所周知最著名的用于面向对象编程的一种关系。继承有点像家庭树。我爷爷的姓是菲利普,我父亲继承了这个姓,我又从父亲那里继承过来(同时还有蓝色的眼睛以及写作的嗜好)。在面向对象编程中,一个人除了有继承特性和行为,一个类也可以从其他类继承属性和方法。

例如,在我们的象棋中有32个棋子,但是只有6种不同的类型(兵、车、象、马、王和后),每种类型当它们移动的时候会有不同的行为。所有这些棋子都有如颜色、属于哪副棋的属性,但是它们在棋盘上也有独特的形状、不同的移动。看一下这6种棋子如何从一个Piece类继承:

当然,空心箭头指明从Piece类继承来的单个棋子类。所有子类都会自动从基类继承一个chess_set和color属性。每一个棋子提供了一个不同的形状属性(当渲染棋盘时会在屏幕上画出这个形状),以及一个不同的move方法,每一轮在棋盘上移动这个棋子到一个新的位置。

我们实际上知道所有Piece的子类都需要一个move方法,否则当棋盘试图移动棋子的时候会感到困惑。有可能我们想要创建一个新版本的象棋游戏,它有一个额外的棋子(向导)。目前我们的设计允许我们不需要给它一个move方法就能设计这个棋子。当棋盘要求棋子移动它自己的时候,棋盘就死掉了。

我们可以在Piece类里创建一个虚拟的移动方法来实现。子类可以用更具体的实现来重写这个方法。例如,默认的实现可能会弹出一个错误消息,提示这个棋子不能移动。在子类里重写方法允许实现强大的面向对象的系统。例如,如果要实现一个带有人工智能的玩家类,我们可能需要提供一个叫calculate_move的方法,这个方法接收一个棋盘对象,然后决定哪个棋子要移动到哪里去。一个非常基础的类可能会随机地选择一个棋子和方向,然后移动。然后我们可以在子类里重写这个方法,来实现深蓝。第一个类可能比较适合跟初学选手比赛,其后的可以挑战一位大师。重要的是,这个类的其他方法,比如通知棋盘已选择的移动,并不需要改变;这些实现可以在两个类之间分享。

考虑到象棋棋子,给它提供一个默认的移动方法其实没有什么意义。我们只需要在任意子类里指定它需要的移动方法。这可以通过使Piece成为一个抽象类,并且声明一个抽象的移动方法实现。抽象方法基本会说“我们在子类里需要这个方法,但是我们拒绝在这个类里指明一个实现方法”。

事实上,创建一个没有实现任何方法的类是可能的。这样一个类会简单地告诉我们这个类该做什么,但是绝对没有提供任何建议告诉你如何去做。在面向对象的世界里,这样的类称之为接口。

继承提供了抽象

现在是时候讨论另外一个长流行词了。多态是依据一个类的不同子类的实现而区别对待这个类的能力。我们已经在我们描述的象棋系统里实际看到过它了。如果我们想让设计走得远一点,我们可能会看到棋盘对象可以从玩家那里接收一个移动请求,并且调用这个棋子的move函数。棋盘不需要知道与之交互的棋子是什么类型的。它所需要做的只是调用move方法,然后适当的子类会去考虑是要按照车还是兵移动它。

多态非常酷,但是它是一个在Python编程中很少使用的字眼。Python更进了一步,它允许像父类那样对待一个对象的子类。在Python中一个棋盘的实现可能会让任何一个对象具有移动的函数,不管它是象、车或者鸭子。当调用move的时候,象会在棋盘上斜着走,车会开到一个地方,而鸭子根据自己的心情可以游泳或者飞。

在Python中这种多态性通常称为鸭子类型:“如果它像鸭子一样走路,像鸭子一样游泳,那么它就是一只鸭子”。我们不关心它是否真的是一只鸭子(继承),只要它可以游泳或者走路。鹅和天鹅很容易能够提供我们正在寻找的像鸭子那样的行为。这允许未来的设计师可以创建新种类的鸟,而不需要真正指定水生鸟类的继承层次结构。这也允许他们创建完全不同的插入式行为,这些行为传统的设计师从没有计划过。例如,未来的设计师可能可以创建一个能走路游泳的企鹅,它具有和鸭子相同的接口而不需要任何建议。

多重继承

当我们在自己的家庭树上考虑继承的时候,我们可以看到我们不仅仅从一个父亲那里继承特性。当陌生人告诉一位骄傲的母亲,她的儿子“有他父亲那样的眼睛”时,她通常将回应说,“是的,但是他有我的鼻子”。

面向对象设计同样可以有类似多重继承的特性,这允许一个子类从多个父类那里继承功能。在实践中,多重继承是比较棘手的事情,并且有些编程语言(最明显的就是Java)严格禁止它。但是多重继承也有它的用处。多数情况下,它可以用来创建一个具有两组不同行为的对象。例如,一个对象被设计用于连接扫描仪,并且把扫描文件发送传真出去,这种情况下,对象可以从两个单独的scanner和faxer传真机对象那里继承过来。

只要两个类有不同的接口,对于一个从这两个类继承过来的子类通常来讲是无害的。但是如果我们从两个提供重叠接口的类继承就会变得一团糟。例如,如果我们有一个摩托车类,它有一个move方法,并且还有一个船类,它也有一个move方法,然后我们想把它们聚合成一个两栖车辆,当我们调用move方法的时候,这个合成类如何知道该怎么做?在设计层面,这需要解释,在实现层面,每种编程语言都有各自不同的方式来决定该调用哪一个父类的方法,或者以什么样的顺序去调用。

通常,处理这个的最好方法就是避免它。如果你有一个像这样的设计,你可能是做错了。退一步,再分析一下系统,看看是否可以把多重继承给删掉,而采用其他类似关联或者组合设计。

为了扩展行为,继承是一个非常强大的工具。它也是早些时候面向对象设计最激动人心的进步之一。因此,它通常是面向对象程序员拿到的第一个工具。然而,认识到拥有一把锤子并不能把螺丝变成钉[4]子是很重要的。对于明显的“是一个 ”这样的关系来说,继承是完美的方案,但是它可以被滥用。程序员通常在两种对象间使用继承来分享代码,而这两个对象没有直接关系,看到的不是“是一个”关系。这未必是一个糟糕的设计,而是一个非常好的机会,问一下他们为什么这么设计,以及是否不同的关系或者设计模式更合适。案例学习

让我们通过几个现实世界中迭代的面向对象设计例子来把我们新的面向对象知识联系到一起。我们将要建模的系统是一个图书馆目录。图书馆通过使用卡片目录来追踪它们的藏书已经几个世纪了,然而最近电子藏书更流行。现代图书馆有一个基于Web的目录,我们可以在自己家里查询藏书。

让我们开始分析。当地图书馆要求我们写一个新的卡片目录程序,因为他们古老的基于DOS的程序很难用并且过时了。这并没有给我们太多细节,但是在我们开始要求更多信息之前,让我们想一下我们已经知道的关于图书馆目录的知识。

目录包含书的列表。人们搜索这个目录来寻找特定主题、特定标题,或者特定作者的书。书可以通过一个唯一的国际标准书号(ISBN)区分。每一本书都有一个杜威十进制数(DDS)来帮助我们在特定书架上找到它。

这个简单的分析告诉了我们在这个系统里的一些明显的对象。我们快速地识别出书是最重要的对象,它有一些前面已经提到的属性,比如作者、标题、主题、ISBN、DDS号及目录,作为书的一种管理方式。

我们也注意到有一些其他对象可能需要也可能不需要在系统里建模。出于编写目录的目的,所有我们需要的就是通过一本书里的author_name属性来搜索一本书。但是作者也是对象,并且我们可能需要存一些关于作者的其他数据。当我们考虑这个问题的时候,我们可能还记得有些书的作者不止一个。突然间,对象只有一个author_name属性的想法看起来似乎有点蠢。与每本书关联一个作者列表显然是一个更好的主意。作者和书的关系很显然是关联,因为你绝对不会说“书是一个作者”(它不是继承关系),并且说“书有一位作者”,虽然语法是对的,但是这并不意味着作者是书的一部分(这不是聚合关系)。事实上,任何一位作者都有可能跟多本书相关联。

我们应该注意名词(名词总是好的候选对象)书架。在一个目录系统里,书架是一个需要建模的对象吗?我们如何识别一个书架。如果有一本书存放在一个书架末尾,然后因为另一本书插入到这个书架上而导致之前那本书移动到下一个书架的开头,这会发生什么?

DDS旨在帮助我们在图书馆里定位实体书架。这样,给书存储一个DDS属性就应该足够找到这本书,而不管它放到了哪个书架上。所以至少目前可以把书架从我们的竞争对象列表里删除了。

在这个系统里另外一个可疑对象是用户。我们需要知道关于一个特定用户的任何信息吗?他们的名字、地址,或者过期的图书列表?到目前为止,只有图书管理员告诉我们他们想要一个目录;关于追踪订阅或者逾期通知他们并没有说什么。在我们的脑海里,我们也注意到作者和用户都是特定类型的人;将来这里可能会有一种有用的继承关系。

至于目录的目的,目前我们决定我们不需要识别用户。我们可以假设一个用户将会搜索目录,但是除了提供一个允许他们搜索的接口外,我们不必要在系统里给他们建模。

我们已经确定了几个关于书的属性,但是目录会有什么属性?任何一个图书馆都会有多个目录吗?我们需要唯一地识别区分它们吗?显然,目录有一个它所包含的书的列表,但是在某种程度上,这个列表可能不是公共接口的一部分。

行为呢?很显然目录需要一个搜索方法,可能会区分作者、标题及主题。书有什么行为吗?它需要有一个预览的方法吗?或者预览可以通过第一页属性来表示,而不是一个方法?

前面讨论的问题都是面向对象分析阶段的一部分。但是混杂着这些问题,我们已经确定了设计的部分关键对象。事实上,你刚才看到的是分析和设计之间的微观迭代。有可能这些迭代会发生在刚开始的图书管理员会议上。然而在这个会议之前,我们已经为我们具体识别的对象勾勒出了一个最基本的设计:

我们和图书管理员见面,带着这张基本图以及一个铅笔来交互式地改善它。他们告诉我们这是一个好的开始,但是图书馆并不只摆书,他们也有DVD、杂志及CD,这些都没有ISBN或者DDS号。然而所有[5]这些类型的东西只能通过一个UPC号来唯一标识。我们提醒管理员,他们必须在书架上找到这些东西,并且这些东西可能没有按照UPC组织分类。图书管理员解释说,每种类型的组织方式不同。CD大多数是有声读物,只有几打库存,所以它们按照作者的姓来组织分类。DVD分为不同类型并且进一步通过标题来组织分类。杂志通过标题以及精确的卷和发行号组织分类。书,好像我们已经猜到了,是通过DDS号组织分类的。

由于之前没有面向对象设计的经验,我们可能会考虑给DVD、CD、杂志和书在我们的目录里添加单独的列表,并且轮流搜索每一项。问题是,除了特定的扩展属性以及标识这些东西的物理位置以外,这些东西的行为方式都是一样的。这就是一个继承工作!我们快速地更新下我们的UML图:

图书管理员需要理解我们画的图的要点,但是对于定位功能有一些疑惑。我们通过一个特定的用例来解释,有一位用户想要搜索“小兔子”这个词。用户首先给检索目录发送了一个搜索请求。目录查询其内部的项目列表,并且找到在标题里带有“小兔子”的一本书和一个DVD。这时,目录并不关心它找到的是DVD、书、CD或者杂志;对目录而言,所有的东西都是一样的。但是用户想知道如何找到物理的东西,如果目录只返回一个标题列表就有些疏忽了。所以它对找到的两个东西调用了定位方法。书的定位方法返回了一个DDS号,这个号可以用于找到放着这本书的书架。DVD通过返回类型和标题来定位。然后用户就可以访问DVD部分,找到包含这个类型的部分,并且通过标题找到具体的DVD。

就像我们解释的,我们勾勒出了一个UML序列图来解释不同对象是如何通信的:

类图描述类之间的关系,序列图描述了对象之间的特定的消息传递。从每个对象出来的虚线是生命线,它描述了对象的生命周期。每个生命线上的宽方格代表了这个对象上的有效处理(没有方块的对象基本是闲置的,在等待事件发生)。生命线之间的水平箭头显示特定信息。实箭头表示被调用的方法,虚线箭头代表方法返回的值。半箭头表示发生或者从一个对象返回的异步消息。一个异步消息通常意味着第一个对象调用了第二个对象的方法并立即返回。经过一些处理,第二个对象调用第一个对象的一个方法来给它一个值。这和正常的方法调用相反,正常的方法调用会在方法内部做处理,然后立即返回一个值。

像所有的UML图一样,序列图最好在需要的时候使用。为了画一个图而画一个UML图是毫无意义的。但是当你需要在两个对象间交互通信的时候,序列图是一个非常好的工具。

不幸的是,目前我们的类图仍然是一个混乱的设计。我们注意到DVD里的演员、CD里的艺术家都是各种类型的人,但是却和书的作者区别对待。图书管理员也提醒我们,他们大多数CD是音频书籍,它们有的是作者而不是艺术家。

我们如何处理贡献一个标题的不同类型的人?一个明显的实现就是创建一个Person类,带有人的名字以及其他相关细节,然后为艺术家、作者及演员创建子类。但是这里真的需要继承吗?为了搜索和检索目录的目的,我们不在乎表演和写作是两个完全不同的活动。如果我们在做一个经济模拟,分别给演员和作者单独一个类以及不同的calculate_income和perform_job方法是有意义的,但是为了检索目录的目的,知道这个人贡献了这个东西可能就足够了。我们认识到,所有的东西都有一个或者多个贡献者对象,所以我们把作者关系从书中移到了它的父类里:

Contributor/LibraryItem关系是多对多的多样性关系,就像在每一个关系的结尾通过*表示一样。任何一个图书馆项目可能不止一个贡献者(例如,一个DVD视频中有多名演员和一名导演)。并且很多作者写了很多书,所以他们会被附到多个图书馆项目里。

这个小改变,虽然看起来更加简洁和简单,但是丢失了一些重要信息。我们仍然可以说出那些贡献一个特定图书馆项目的人,但是我们不知道他们是如何贡献的。他们是导演还是演员?他们写了音频书,还是用声音叙述了那本书?

如果我们可以给Contributor类添加一个Contributor_type属性就好了,但是当处理那些既写书又导演了电影的多才艺的人时,这将会崩溃。

有一个选择是给我们每一个LibraryItem子类添加属性,它会保存像书的作者、CD的艺术家这种我们需要的信息,然后让这些属性都指向Contributor类。这样做的问题是,我们失去了优雅的多态。如果我们要列出一个项目的贡献者,我们需要在这个项目里查找特定的属性,就像作者或者艺术家。我们可以通过给LibraryItem类添加一个GetContributors方法来实现,LibranyIten类的子类可以重写这个方法。然后目录永远不会知道对象正在查找什么属性;我们抽象了的公共接口:

看着这个类图,感觉我们好像做错了什么。这很笨重和脆弱。它可能会做我们需要的一切,但是感觉很难维护或者扩展。这里有太多的关系,修改任何一个类,太多的类会受到影响。它看起来像意大利面条和肉丸子。

既然我们已经讨论了继承作为一个选项,并且发现它想要成为这个选项,我们可能要回顾下我们之前的基于组合的图表,那里的Contributor直接附在LibraryItem上。再想一想,我们可以看到我们其实需要给全新的类再添加一个关系来区分贡献者的类型。在面向对象设计中这是一个很重要的步骤。现在我们给我们的设计添加一个类,旨在支持其他的对象,而不是给初始需求的任何部分建模。我们正在重构设计,以便于帮助系统中的对象而不是现实中的对象。重构是维护或设计一个程序的重要过程。重构的目的是通过移动代码,删除重复代码或者复杂关系来改善设计,有利于形成一个更简单、更优雅的设计。

这个新类由一个Contributor和额外的识别给定LibraryItem贡献者的属性组成。对于一个特定的LibraryItem可以有多个贡献者,一个贡献者可以以同样的方式贡献不同的项目。这个图传达设计得非常好:

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载