计算机是怎样跑起来的-第7章 成为会使用面向对象编程的程序员吧
在本章笔者想让诸位掌握的是有关面向对象编程的概念。理解面向对象编程有着各种各样的方法,程序员们对它的观点也会因人而异。本章会将笔者至今为止遇到过的多名程序员的观点综合起来,对面向对象编程进行介绍。哪种观点才是正确的呢?这并不重要,重要的是把各个角度的观点整合起来,而后形成适合自己的理解方法。在读完本章后,请诸位一定要和朋友或是前辈就什么是面向对象编程展开讨论。
7.1 面向对象编程
面向对象编程(OOP,Object Oriented Programming)是一种编写程序的方法,旨在提升开发大型程序的效率,使程序易于维护 A。因此在企业中,特别是管理层的领导们都青睐于在开发中使用面向对象编程。因为如果开发效率得以提高、代码易于维护,那么就意味着企业可以大幅度地削减成本(开发费用+维护费用)。甚至可以这样说,即使管理者们并不十分清楚面向对象编程到底是什么,他们也还是会相信“面向对象编程是个好东西”。
但是在实际的开发工作中,程序员们却有一种对面向对象编程敬而远之的倾向。原因在于他们不得不重新学习很多知识,还会被新学到的知识束缚自己的想法,导致无法按照习惯的思维开发。以笔者写书的经验来看,如果是讲解传统的编程方法,那么只需要写一本书就够了,而讲解面向对象编程则需要写两本书。直说的话就是面向对象编程太麻烦了。甚至还曾听到过这样的传言:若是在面向开发人员的杂志中刊登了标题中含有面向对象编程的专栏,那么仅凭这一点,杂志的销路就好不了。
虽然现状如此,但是还是让笔者讲解一下面向对象编程吧。因为在未来的开发环境中,将成为主流的不是 Java 就是 .NETA,而无论选择哪个,面向对象编程的知识都是不可或缺的。这使得在这之前还对其敬而远之的程序员们也不得不迎头赶上了,因为他们已经没有退路了。
的确,精通面向对象编程需要花费大量时间。所以请诸位先通过阅读本章,掌握一些基础知识,至少能够说出面向对象是什么。然后再为实践面向对象编程而开始踏踏实实的深层学习吧。
7.2 对 OOP 的多种理解方法
在计算机术语辞典等资料中,常常对面向对象编程做出了如下定义。面向对象编程是一种基于以下思路的程序设计方法:将关注点置于对象(Object)本身,对象的构成要素包含对象的行为及操作 B,以此为基础进行编程。这种方法使程序易于复用,软件的生产效率因而得以提升。其中所使用的主要编程技巧有继承、封装、多态三种。
这段话足以作为对术语的解释说明,但是仅凭这段话我们还是无法理解面向对象编程的概念。
“面向对象编程是什么?”如果去问十名程序员,恐怕得到的答案也会是十种。就此打个可能稍微有点特别的比方吧。有几个人去摸一只刺猬,但他们看不到刺猬的全身。有的人摸到了刺猬的后背,就会说“摸起来扎手,所以是像刷子一样的东西”;而有的人摸到了刺猬的尾巴,就会说“摸起来又细又长,所以是像绳子一样的东西”(如图 7.1所示)。同样的道理,随着程序员看问题角度的不同,对面向对象编程的理解也会是仁者见仁、智者见智。

那么到底哪种理解方法才是正确的呢?其实无论是哪种方法,只要能够通过实际的编程将其付诸实践,那么这种方法就是正确的。诸位也可以用自己的理解方法去实践面向对象编程。虽然是这么说,但如果仅仅学到了片面的理解方法,也是无法看到面向对象编程的全貌的,会感到对其概念的理解是模模糊糊的。因此,下面我们就把各种各样的理解方法和观点综合起来,以此来探究面向对象编程的全貌吧。
7.3 观点 1:面向对象编程通过把组件拼装到一起构建程序
在面向对象编程中,使用了一种称为“类”的要素,通过把若干个类组装到一起构建一个完整的程序。从这一点来看,可以说类就是程序的组件(Component)。面向对象编程的关键在于能否灵活地运用类。
首先讲解一下类的概念。在第 1 章中讲解过,无论使用哪种开发方法,编写出来的程序其内容最终都会表现为数值的罗列,其中的每个数值要么表示“指令”,要么表示作为指令操作对象的“数据”。程序最终就是指令与数据的集合。
在使用古老的 C 语言或 BASIC 等语言编程时(它们不是面向对象的编程语言,即不是用于表达面向对象编程思想的语言),用“函数” 表示指令,用“变量”表示数据。对于 C 语言或是 BASIC 的程序员而言,程序就是函数和数据的集合。在代码清单 7.1 中,用 FunctionX 的形式为函数命名,用 VariableX 的形式为变量命名。
代码清单 7.1 程序是函数和变量的集合(C 语言)

在大型程序中需要用到大量的函数和变量。假设要用非面向对象的编程方法编写一个由 10000 个函数和 20000 个变量构成的程序,那么结果就很容易是代码凌乱不堪,开发效率低到令人吃惊,维护起来也十分困难。
于是一种新的编程方法就被发明出来了,即把程序中有关联的函数和变量汇集到一起编成组。这里的组就是类。在 C++、Java、C# 等面向对象编程语言中,语法上是支持类的定义的。在代码清单 7.2 中,就定义了一个以 MyClass 为名称的类。因为程序的构成要素中只有函数和变量,所以把它们分门别类组织起来的类也理所当然地成了程序的组件。通常把汇集到类中的函数和变量统称为类的“成员”(Member)。
为了使 C 语言支持面向对象编程,人们扩充了它的语法,开发出了 C++ 语言。而通过改良 C++ 又开发出了 Java 和 C#。在本章中,将会分别介绍用 C 语言、C++、Java 和 C# 编写的示例程序。诸位在阅读时只需抓住其大意即可,不必深究每个程序的具体内容。
代码清单 7.2 定义类 MyClass,将函数和变量组织到一起(C++)

7.4 观点 2:面向对象编程能够提升程序的开发效率和可维护性
在使用面向对象编程语言开发时,并不是所有的类都必须由程序员亲自编写。大部分的类都已内置于面向对象编程语言中了,这些类可以为来自各个领域的程序员所使用。通常将像这样的一组类(一组组件)称作“类库”。通过利用类库可以提升编程的效率。还有一些类原本是为开发其他程序而编写的,如果可以把这些现成的类拿过来使用,那么程序的开发效率就更高了。
所谓企业级的程序,指的是对可维护性有较高要求的程序。可维护性体现在当程序投入使用后对已有功能的修改和新功能的扩充上。如果所维护的程序是用一组类组装起来的话,那么维护工作就轻松了。之所以这样说,是因为作为维护对象的函数和变量,已经被汇聚到名为类的各个组中了。举例来说,假设我们已经编写出了一个用于员工薪资管理的程序。随着薪资计算规则的变更,程序也要进行修改,那么需要修改的函数和变量就应该已经集中在一个类中了,比如一个叫作 CalculationClass 的类(如图 7.2 所示)。也就是说,维护时没有必要去检查所有的类,只需修改类 CalculationClass 就可以了。关于可维护性,在第 12 章中还会继续介绍。
“我是创造类的人,你是使用类的人”——在实际应用面向对象编程时要带着这个感觉。开发小组中的全体成员没有必要都对程序中的方方面面有所了解,而是组中有些人只负责制作组件(类),有些人只负责使用组件。当然也会有需要同时做这两种工作的情况。另外,还可以把一部分组件的开发任务委托给合作公司,或者买来商业组件使用。

对于创造类的程序员,他们考虑的是程序的开发效率和可维护性,并决定应该将什么抽象为类。如果一个类的修改导致其他的类就也要跟着修改,这样的设计是不行的。必须把组件设计成即使是坏了(有缺陷了)也能轻松地替换,就像在汽车或家电等工业制品中所使用的组件那样。
在功能升级后,旧组件能够被新组件所替换的设计也是必不可少的。因此,创造者和使用者之间就需要事先商定类的使用规范。请诸位记住,对于类的使用者而言“类看起来是什么样子的”这种关于规范的描述通常被称为“接口”(Interface)。例如只要把接口告诉合作公司,就可以要求他们编写类,编写出的类也就自然能够与程序中的其他部分严丝合缝地拼装起来。在面向对象语言中,也提供了用于定义接口的语法。
7.5 观点 3:面向对象编程是适用于大型程序的开发方法
通过之前的介绍,诸位应该也理解了为什么说面向对象编程适用于编写大型程序。假设一个程序需要 10000 个函数和 20000 个变量,如果把这个程序用 100 个类组织起来,那么平均一个类里就只有 100个函数和 200 个变量了。程序的复杂度也就降到了原来的 1%。而如果
使用了稍后将会讲解的封装这种编程技巧(即将函数和变量放入黑盒,使其对外界不可见),还可以更进一步降低复杂度。
在讲解面向对象编程的书籍和杂志文章中,由于受到篇幅的限制,往往无法刊登大篇幅的示例程序。而通过短小的程序恐怕又无法把面向对象编程的优点传达出来。当然本书也不例外。所以就要请诸位读者一边假想着自己在开发一个大型程序,一边阅读本书的解说。
为了拉近计算机和人的距离,使计算机成为更容易使用的机器,围绕着计算机的各种技术都在不断发展。在人的直觉中,大件物品都是由组件组装起来的。因此可以说面向对象编程方法把同样的直觉带给了计算机,创造了一种顺应人类思维习惯的先进的开发方法。
7.6 观点 4:面向对象编程就是在为现实世界建模
程序可以在计算机上实现现实世界中的业务和娱乐活动。计算机本身并没有特定的用途,而是程序赋予了计算机各种各样的用途。在面向对象编程中,可以通过“这个是由什么样的对象构成的呢?”这样的观点来分析即将转换成程序的现实世界。这种分析过程叫作“建模”。可以说建模对于开发者而言,反映的是他们的世界观,也就是在他们的眼中现实世界看起来是什么样子的。
在实际建模的过程中,要进行“组件化”和“省略化”这两步。所谓组件化,就是将可看作是由若干种对象构成的集合的现实世界分割成组件。因为并不需要把现实世界 100% 地搬入到程序中,所以就可以
忽略掉其中的一部分事物。举例来说,假设要为巨型喷射式客机建模,那么就可以从飞机上抽象归类出机身、主翼、尾翼、引擎、轮子和座席等组件(如图 7.3 所示)。而像是卫生间这样的组件,不需要的话就可以省略。“建模”这个词也可以理解为是制作塑料模型。虽然巨型喷射式客机的塑料模型有很多零件,但是其中应该会省略掉卫生间吧,因为这对于塑料模型来说不是必需的。

7.7 观点 5:面向对象编程可以借助 UML 设计程序
可以说建模就是在为面向对象编程做设计。为了把对现实世界建模的结果以图形的形式表示出来,还经常使用被称作 UML(Unified Modeling Language,统一建模语言)的表记方法。UML 是通过统一历史上曾经出现的各种各样的表记方法而发明出来的,事实上 UML 已经成为了建模表记方法中的世界标准。
在 UML 中,规定了九种图(见表 7.1)。之所以有这么多种,是为了从各种各样的角度表示对现实世界建模的结果。例如用例图是从用户的角度,即用户使用程序的方式出发表示建模结果的一种图。而类图等出发的角度则是程序。
表 7.1 UML 中规定的九种图

UML 仅仅规定了建模的表记方法,并不专门用于面向对象编程。因此公司的组织架构图和业务流程图等也可以使用 UML 表记。
“这儿可有九种图呢,记忆起来很吃力啊”——也许会有人这么想吧。但是可以换一种积极的想法来看待它。既然 UML 被广泛地应用于绘制面向对象编程的设计图,那么只要了解了 UML 中仅有的这九种图的作用,就可以从宏观的角度把握并理解面向对象编程思想了。怎么样,如果这样想的话,就应该会对学习 UML 跃跃欲试了吧。
图 7.4 中有一个 UML 类图的示例。图中所画的类表示的正是前面代码清单 7.2 中的类 MyClass。将一个矩形分为上中下三栏,在上面的一栏中写入类名,中间的一栏中列出变量(在 UML 中称为“属性”),在下面的一栏中列出函数(在 UML 中称为“行为”或是“操作”)。
在进行面向对象编程的设计时,要在一开始就把所需要的类确定下来,然后再在每个类中列举出该类应该具有的函数和变量,而不要到了后面才把零散的函数和变量组织到类中。也就是说,要一边观察作为程序参照物的现实世界,一边思考待解决的问题是由哪些事物(类)构成的。正因为在设计时要去关注对象,这种编程方法才被称为面向对象编程(Object Oriented Programming,其中的 Oriented 就是关注的意思)。而在那些传统的开发方法中,进行设计则是要先考虑程序应该由什么样的功能和数据来构成,然后立即确定与之相应的函数和变量。与此相对在面向对象编程的设计中,因为一上来就要确定有哪些类,从而构成程序的函数和变量就必然会被组织到类中。

7.8 观点 6:面向对象编程通过在对象间传递消息驱动程序
假设要编写这样一个程序,玩家 A 和玩家 B 玩剪刀石头布,由裁判判定输赢。如果使用作为非面向对象编程语言的 C 语言编写,程序就会像代码清单 7.3 中那样;如果使用作为面向对象编程语言的 C++ 编写,程序就会像代码清单 7.4 中那样。诸位能看出其中的差异吗?
代码清单 7.3 未使用面向对象编程语言的情况(C 语言)

在 C 语言的代码中,仅仅使用了 GetHand() 和 GetWinner() 这种独立存在的函数。与此相对在 C++ 的代码中,因为函数是隶属于某个类的,所以要使用 PlayerA.GetHand() 这样的语法,表示属于类 PlayerA的函数 GetHand()。
也就是说用 C++ 等面向对象编程语言编写程序的话,程序可以通过由一个对象去调用另一个对象所拥有的函数这种方式运行起来。这种调用方式被称为对象间的“消息传递”。在面向对象语言中所说的消息传递指的就是调用某个对象所拥有的函数。即便是在现实世界中,我们也是通过对象间的消息传递来开展业务或度过余暇的。在面向对象编程中还可以对对象间的消息传递建立模型。
如果未使用面向对象编程语言,那么可以用流程图表示程序的运行过程。流程图表示的是处理过程的流程,因此通常把非面向对象语言称为“过程型语言”。而且可以把面向对象编程语言和面向过程型语言,面向对象编程和面向过程编程分别作为一对反义词来使用。
如果使用的是面向对象编程语言,那么可以使用 UML 中的“时序图” 和“协作图”表示程序的运行过程。在图 7.5 中对比了流程图和时序图。
关于流程图已经没有必要再进行介绍了吧。在时序图中,把用矩形表示的对象横向排列,从上往下表示时间的流逝,用箭头表示对象间的消息传递(即程序上的函数调用)。诸位在这里只需要抓住图中的大意即可。

沉浸在面向过程编程中的程序员们通常都习惯于用流程图思考程序的运行过程。可是为了实践面向对象编程,就有必要改用时序图来考虑程序的运行过程。
7.9 观点 7:在面向对象编程中使用继承、封装和多态
“继承”( Inheritance)、“封装”( Encapsulation)和“多态”
(Polymorphism,也称为多样性或多义性)被称为面向对象编程的三个基本特性。在作为面向对象编程语言的 C++、Java、C# 等语言中,都已具备了能够用程序实现以上三个特性的语法结构。
继承指的是通过继承已存在的类所拥有的成员而生成新的类。封装指的是在类所拥有的成员中,隐藏掉那些没有必要展现给该类调用者的成员。多态指的是针对同一种消息,不同的对象可以进行不同的操作。
其实仅仅介绍如何在程序中使用这三个基本特性,就已经需要一本书了。因而有很多人就会被所学到的语法结构和编程技术中涉及的大量知识所束缚,以致不能按照自己的想法编写程序。其实只要沉静下来,不拘泥于语法和技术,转而去关注使用这三个特性所带来的好处,就能顺应着自己的需求恰当地使用面向对象编程语言了。
只要去继承已存在的类,就能高效地生成新的类。如果一个类被多个类所继承,那么只要修正了这个类,就相当于把继承了这个类的所有类都修正了。只要通过封装把外界不关心的成员隐藏起来,类就可以被当作是黑盒,变成了易于使用且便于维护的组件了。而且由于隐藏起来的成员不能被外界所访问,所以也就可以放心地随意修改这些成员。只要利用了多态,生成对同一个消息可以执行多种操作的一组类,使用这组类的程序员所需要记忆的东西就减少了。总之,无论是哪一点,都是面向对象编程所带来的好处,都可以实现开发效率和可维护性的提升。
稍后将会介绍如何在实际的编程中使用继承。为了对类进行封装,需要在类成员的定义前指定关键词 public(表示该成员对外可见)或是private(表示该成员对外不可见)。之前的代码清单 7.2 中省略了这些关键词。实现多态可以有多种方法,感兴趣的读者可以去翻阅面向对象语言的教材等相关资料。
7.10 类和对象的区别
前面介绍了有关面向对象的几种观点。诸位读者应该已经了解面向对象编程是怎么一回事了吧。但是请允许笔者再补充一些面向对象编程中必不可少的知识。
首先,要说明一下类和对象的区别。在面向对象编程中,类和对象被看作是不同的概念而予以区别对待。类是对象的定义,而对象是类的实例(Instance)。经常有教材这样说明二者之间的关系:类是做饼干的模具,而用这个模具做出来的饼干就是对象。虽然这是个有趣的比喻,但是如果这样类比的话,就有可能无法看清二者在实际编程中的关系(如图 7.6 所示)。
在之前的代码清单 7.2 所示的程序中,定义了一个类 MyClass。但是我们还无法直接使用类 MyClass 所持有的成员,要想使用就必须在内存上生成该类的副本,这个副本就是对象(如代码清单 7.5 所示)。

先要创建一个个的对象然后才能使用类中定义的成员,对于面向对象语言的初学者而言,他们会认为这样做很麻烦。但是也只能这样做,因为面向对象语言就是这样规定的。可是为什么要确立这样的规则呢?原因是即便是在现实世界中,也有类(定义)和对象(实体)的
区别。举例来说,假设我们定义了一个表示企业中雇员的类 Employee。如果仅仅是定义完就可以立刻使用类 Employee 中的成员,那么程序中实际上就只能存在一名雇员。而如果规定了要先创建类 Employee 的对象才能使用,那么就可以需要多少就创建多少雇员了(通过在内存上创建出类 Employee 的副本)。在这一点上,稍后将要介绍的具有两个文本框的 Windows 应用程序也是如此,也就是说这个程序创建了两个文本框类的对象。
这样的话,就更能理解“类是做饼干的模具,用模具做出来的饼干是对象”这句话的含义了吧。有了一个做饼干的模具(类),那么需要多少就能做出多少饼干(对象)。
7.11 类有三种使用方法
前面已经介绍过了,在面向对象编程中程序员可以分工,有的人负责创建类,有的人负责使用类。创建类的程序员需要考虑类的复用性、可维护性、如何对现实世界建模以及易用性等,而且还要把相关的函数和变量汇集到类中。这样的工作称为“定义类”。
而使用类的程序员可以通过三种方法使用类,关于这一点诸位要有所了解。这三种方法分别是:1. 仅调用类所持有的个别成员(函数和变量);2. 在类的定义中包含其他的类(这种方法被称作组合);3. 通过继承已存在的类定义出新的类。应该使用哪种方法是由目标类的性质以及程序员的目的决定的。
在诸位平时所见的程序背后,程序员们也是按照上述三种方法使用类的。代码清单 7.6 中列出了一段用 C# 编写的 Windows 应用程序。当用户点击按钮,就会弹出一个消息框,里面显示的是输入到两个文本框中的数字进行加法运算后的结果(如图 7.7 所示)。
诸位在这里不需要深究程序代码的含义,而是要把注意力集中到类的三种使用方法上。在这个程序中,表示整体界面的是以 Form1 为类名的类。类 Form1 继承了类库中的类 System.Windows.Forms.Form。在 C# 中用冒号“:”表示继承。在窗体上,有两个文本框和一个按钮,用程序来表示的话,就是类 Form1 的成员变量分别是以类 System. Windows.Forms.TextBox(文本框类)为数据类型的 textBox1、textBox2,和以类 System.Windows.Forms.Button(按钮类)为数据类型的 button1。像这样类中就包含了其他的类,也可以说是类中引用了其他的类。而代码中的 Int32.Parse 和 MessageBox.Show,只不过是个别调用了类中的函数。
代码清单 7.6 进行加法运算的 Windows 应用程序(用 C# 编写)

7.12 在 Java 和 .NET 中有关 OOP 的知识不能少
在本章的最后,笔者来解释一下为什么说程序员已经到了无法逃避面向对象编程的地步了。在未来的开发环境中,将成为主流的不是Java 就是 .NET。Java 和 .NET 其实是位于操作系统(Windows 或 Linux等)之上,旨在通过隐藏操作系统的复杂性从而提升开发效率的程序集,这样的程序集也被称作“框架”(Framework)。框架由两部分构成,一部分是负责安全执行程序的“执行引擎”,另一部分是作为程序组件集合的“类库”(如图 7.8 所示)。

无论是使用 Java 还是 .NET,都需要依赖类库进行面向对象编程。在 Java 中,使用的是与框架同名的 Java 语言。而在 .NET 中,使用的是 .NET 框 架 支 持 的 C#、Visual Basic.NET、Visual C++、NetCOBOL等语言进行开发。上述的每种语言都是面向对象语言。其中 Visual Basic.NET 和 NetCOBOL 是在古老的 Visual Basic 和 COBOL 语言中增加了面向对象的特性(类的定义、继承、封装和多态等)而诞生的新语言。至今还对面向对象编程敬而远之的程序员们,你们已经不得不迎头赶上了。不要再觉得麻烦什么的了,以享受技术进步的心情开始学习面向对象编程吧!
通过综合整理面向对象的各种理解方法,相信诸位已经能看到面向对象的全貌了。但这里还有一点希望诸位注意,那就是请不要把面向对象当成是一门学问。程序员是工程师,工程是一种亲身参与的活动而不是一门学问。请诸位把面向对象编程作为一种能提升编程效率、写出易于维护的代码的编程方法,在适当的场合实践面向对象编程,而不要被它各种各样的概念以及所谓的编程技巧所束缚。
面向对象编程就是通过把组件拼装到一起进行编程的方法——笔者曾经明确下过这样的结论,也是以此为理念进行实践的。但是也许有人会摆出学者的那一套理论:“你还没有明白面向对象编程的理念,你这个是面向组件编程!”如果真有人这样说,笔者就会反问他:“这么说你正在实践面向对象编程吗?”
在接下来的第 8 章,笔者将一改编程的话题,开始讲解数据库。敬请期待!

1099

被折叠的 条评论
为什么被折叠?



