文章仅限加深印象学习
1.1对象的概念
1.1.1 面向功能的软件设计的局限性
面向过程的程序设计语言主要使用顺序、选择、循环三种基本结构来编写程序。
- 顺序:按照时间轴顺序完成每个处理 - 选择:根据条件的成立与否执行不同的条件分支 - 循环:根据一定的条件反复执行同样的代码
面向对象问世之前,程序设计被看作为一个个功能系统的集合。
程序员--》根据设计文档--》实现各个函数--》完成目标软件。 最关心的:如何从需求中提取出要实现的功能,决定数据格式,并将其组合在一起。
【举例子-实现一个用于管理和采购办公用品的OA系统】:
在这个OA系统中,我们假设某部门提出采购需求后,首先要检查该部门预算够不够用。通常做法:设计申请购买商品的登陆页面和申请的流程,决定使用的数据格式,定义检查数据正确的函数,定义预算数据的格式和访问他的接口。
简而言之:整个系统设计的过程,就是将需求分解成一个个小的功能,同时定义每个功能所需的数据格式。
但是 需求是无时不刻不再变更的,例如:假设需求从纸质文件的购买申请变更为通过网页来申请,或者需要新增能根据部门/物品/类别来查看历史记录的功能,这时就需要对基于传统做法生成的软件进行大幅度的修改。
软件的核心是功能,而需求又容易变更,所以围绕功能设计软件,会比较难于应对需求的变更,维护成本比较高.
1.1.2 面向对象的模块化
从20世纪80年代后半期开始,面向对象的编程方法渐渐引起了人们的关注。
首先,让我们超出计算机的范畴广义地考虑,对象指的就是人能够识别的东西,从你手里拿的书,随身的笔记本,到桌子、手表、摩托车、 、收据、工资单、图书馆和区政府等,这些都是对象。而且,对象有属性,并且能够接受消息来进行相应的处理。属性指的是对象的性质和所具有的信息,例如汽车能装多少个人,是什么颜色的,现在行驶的速度是多少等。属性也可以称为状态。消息是指对象和对象之间的信息传递,在信息共享、查询、命令请求时使用。
面向对象的编程是指,以对象作为程序的基本模块来进行软件需求的分析、设计和开发的一种思考方法。
再让我们来看看刚才提到的那个办公用品的管理系统。
某公司的办公用品管理系统对于圆珠笔和复印纸这类消耗品都会有一部分预留库存。每个部门想要这些东西的时候,如果库存够用,就会直接从库存中划拨,并提交结算单给财务部门。如果库存不足,则会从商店购买,并把部门提交的申请单放入暂存箱中。商品送来之后,把商品交给需要的部门,并同时把结算单交给财务 部门。
只读文字可能不太好懂,参照图1-1来看一下会更容易理解。图里面的椭圆形就是对象,箭头代表的是消息的传递。图中虽没有标名对象的属性,但是例如库存中肯定会有圆珠笔和复印纸的数量,暂存箱中,肯定会有还没有处理的订单信息,这些分别是仓库和暂存箱的属性。另外,订单本省也可以用一张纸质订单这样的对象来表示。通常人们在整理多个概念之间的关系时,会画类似于图1-1这样的图来理顺关系。因此把这个图中的每个对象作为一个模块来实现一个软件会是一个不错的选择。
例如,总务部告诉供应部:“我想要10支圆珠笔。”供应部会调用仓库的属性,即圆珠笔的库存情况。如果库存不足的话,供应部会向商店发出购买圆珠笔的请求。
这张图并没有变现出功能性的细节,所以就算是申请办公用品的方式基于网页,或需要添加查看购买的历史记录,上图中描绘的各个对象之间的关系也不会有太大的变化。基于面向对象的软件开发,比较接近于人的思考方式,更善于应对需求的变更。
1.1.3 消息传递
消息是对象之间通信的唯一手段。请求、查询、应答和一场通知等,所有的通信和控制都是通过消息完成的。对象收到消息后,会对消息进行解析、完成相应的处理并返回结果。具体的处理方法和这个对象内部实现相关,这里叫做方法。
方法中写明了程序的各种操作的实现和规则。
消息传递的时候可以使用对象或者基本数值作为参数。另外,消息处理的结果也可以返回一个对象或基本数值。
送信的对象称为发送者,收信的对象称为接受者。
通过消息协调各个对象之间的消息发送,使其作为一个整体运行,这就是面向对象的软件的运行模式。
1.1.4 模块的抽象化
综上说述,具备以下特征的东西可以称为对象。
- 可人为分辨出这是一个对象
- 拥有属性
- 能够向其它对象发送消息
- 能够接受消息,并作出相应的处理
- 消息的处理是通过对象的方法完成的
像这种使用对象的概念对问题进行抽象化的方法叫做面向对象。使用对象的概念来分析如何做一个软件叫做面向对象的分析(OOA)。以对象为基础来设计软件叫做面向对象的设计(OOD)。编程过程中使用面向对象的概念叫作面向对象的程序设计(OOP)。另外,以消息通信构成的鼠标、键盘或用户界面的按钮等同程序之间的接口叫做面向对象的接口。
通过使用面向对象的语言,可在面向对象的分析和设计的基础上来完成编程。它不像传统的以功能为核心的软件开发,需要明确指明每个函数所所对应的功能。面向对象的软件开发,从需求分析、设计到编程都使用统一的模型,所以更善于应对需求的变更。
把一个事物作为对象考虑时,并不需要把真实世界中这个事物的所有属性和构成全部放到对象中,只需要考虑和要实现的模型有关的属性和动作即可。例如现实中供应部的工作肯定不会像我们举例那么简单,但在设计的时候也不需要考虑要定的纸是不是A4的,商店是不是早九晚五上班等过于琐碎的细节。
抽象化指的是尽可能地不考虑相关细节,只关注对象的核心和本质。对于现实世界中的事物,你越观察、分析就会发现越来越多的细节。通过抽象可以用简单概念的集合来描述一个复杂的对象,这是非常重要的。
如何抽出一个对象,是根据要实现的模型和软件的性质、功能来决定的。其实我们人类对于事物的认识,在不知不觉中也是一个抽象化的过程。理解模型中的对象,就和理解人类日常生活中的对象是非常相似的。
目前为止,好像给大家留下了一个“万物皆对象”的感觉,到底有没有什么不适合作为对象的东西呢?适合作为对象和不适合作为对象有什么区别呢?
实际上并没有明显区别,只要构建出来的模型是没有问题的,万物都可以被当作对象来处理。
真实世界中某个独立存在的事物,一个整体的某个组成部分、有重要作用的人和部门等,都比较适合作为对象。特别是如果某个东西能够拟人化,也比较适合作为对象。另外,多个素数、行列这些和数学有关的操作也可以被看作对象。
相反,时间、空间、知识、伦理、感动等等这些过于抽象,不适合用数字来定性的东西,不适合作为对象。另外难于被归类成“收到消息,进行处理”这个过程的,也不太适合当作对象。
那么,“数学”到底该不该被看作对象呢?对此有正反两方面的意见。整数或实数这种单纯的数字类型,可被看作对象,也可以看作单纯的数值。让我们来看看在编程语言中是如何做的。Smalltalk(面向对象编程语言的始祖)中所有的东西都是对象,所以数字也不例外。而C++中数字并不是对象。Objective-C和C++一样,并不是把数字看作对象,后面的章节中将会有具体的说明。
专栏:面向对象的方方面面
“面向对象” 是一个经常被提及的词,但很多以“面向对象”,开头的词并没有一个被广泛认可的定义。例如,到底什么样的编程语言才算是一个面向对象的语音,就有各种不同的定义。同样,语言中很多基本的定义、概念和惯用名在不同的编程语言中存在不同的解释,有点混乱。更麻烦的是,把资料翻译成中文的时候,有新“发明”了很多说法。
Objective-C来源于面向对象语言的始祖,Smalltalk,所以Objective-C中和面向对象相关的各种表达的本质和Smalltalk是基本一致的,但和其他语言可能存在不同。阅读本书时侯,要注意这点。
1.1.5 对象的属性
让我们从属性的角度来重新认识一下对象这个概念。图1-3是一个包含了属性和方法的对象。
对象拥有属性(也可以说是状态),但属性是怎么被定义的呢?对象的属性一般被定义为指向其他对象的指针,这个指针叫做 实例变量,或变量。变量可能指向一个空的对象(null),另外变量也不一定必须是一个指针,也可以是数值类型。让我们来看一下图1-4,这是一个邮箱(或水箱)保温系统的例子。左上角的对象是整个系统的总控部分,它有三个属性,分别是系统开关、温度调节器和当前状态(停止中、低温保温中、高温保温中等)。开关和温度调节器这两个属性分别指向其他两个类的对象。
温度调节器本身也拥有若干个属性,例如用于获取水温的温度传感器、用于给水加热的加热器、用于显示当前温度的LED显示设备,这些属性都是其他对象的实例。温度调节器还有一个属性是报警器---用于水温过高或过低时报警,目前还没有指向某个具体的对象。另外水温的上限值和下限值都已经设置好了。
对象和对象之间一般时通过一个对象的某个属性是另一个对象的变量来建立关系的。没有引用关系的两个对象之间无法发送消息。Objective-C中把连接对象的变量称为输出口(outlet)。GUI的设计工具Interface Builder(Mac OS X 平台下用于设计和测试GUI的应用程序)中也使用了outlet这种说法。各个对象之间就好像插口一样,互相连接在一起。
1.1.6 类
图1-4展示了管理邮箱温度用的多个对象之间的关系。但对于一个大型设备来说,可能有多个需求要管理邮箱。这时如果为每个邮箱单独设计的话,就可能非常麻烦。针对这种情况,我们可以把具备相同变量和方法的对象提炼出来,做成“模板”。这样以后就可以使用“模板”来创建各个具体的对象。这种“模版”就是 类(class)。
类包含了一组特定对象的共有特性。例如就汽车这个对象来说,虽然世界上有各种各样的汽车,但汽车的基本结构和驾驶方法都是相同的。于是我们就可以不考虑汽车构造和驾驶方面的各种细节,抽象出“汽车”这个概念。虽然每个具体型号的汽车有着不一样的细节,但整体上都符合我们抽象出来的汽车的概念。类就是舍弃了每个具体对象的各种细节,把所有对象都具备的共同部分抽象出来。
所有类的对象都可以共享该类中定义的变量和方法。不同对象之间的差异就在于变量值的不同。拿汽车来举例的话,变量就包含颜色、外观、引擎和轮胎等,根据这些条件就可以确定一台汽车了。
用类创建对象的过程叫作实例化,生成的对象实例对象,或简称为实例。
不同实例对象的变量各不相同,或指向不同的对象,或有不同的数值。实例所拥有的变量称为实例变量。类决定了需要定义几个、定义什么类型的实例变量。不同实例对象的实例变量可以各不相同。
方法是在类中统一定义的,同一个类不同实例对象的方法都是相同的。
在图1-5 管理水温的对象和管理油温的对象是同一个类的不同实例。管理水温和油温的方法是一致的,但各自用到的上限值和下限值各不相同,水温和油温用的感应器和加热器也各不相同。很多面向对象语言中,对象都只能由类的定义来实例化。
既然提到了类,就不得不提到一个非常重要的概念--继承。但一次讲太多的话比较容易引起混乱,关于继承的详细内容留到第3章详细说明。接下来我们从软件构成的角度来继续说明一下对象。
1.2 模块和对象
上一节中我们简单说明了基于面向对象的软件设计和开发。本书在说明时使用了比较简单的概要图,而真正的基于面向对象的分析和设计则会使用UML(统一建模语言)这种图形化的表示方法。至于如何基于对象来建立模型,如何用图形或文档来秒数定义好的模型等,都会被作为各种各样的软件开发方法来详细讨论。
接下来我们来讲讲如何基于面向对象来设计软件。封装是软件设计中一个非常重要的概念,不仅仅是Objective-C,所有的面向对象语言都将使用到封装这一概念。
1.2.1 软件模块
无论是面向对象还是面向过程的软件开发,都需要把要完成的系统分解成若干个小模块,先独立开发每个模块,然后在组装成整个软件。那么到底什么是模块呢?通常,模块是工业制品中的概念,指的是整体中的某一部分,它具备独立的功能,更换时不会影响到其他部分,例如电源模块。软件开发中的模块也是一个功能单位,构成一个软件的各个相互独立的部分叫作模块。一个模块由变量、方法甚至其他模块构成。所以模块具备层次性。
我们这里提到的模块所提供的功能包括:过程和函数的调用、变量的重新赋值等。
1.2.2 高独立性的模块
长久以来,人们一直在寻找软件模块划分的最佳方案,并为此提出了各种各样的概念和方法。但简而言之,独立性高(低耦合、高內聚)的模块划分是最佳的划分方式。模块的独立性,指的是每个模块之间的交集应该尽可能地小。这样,模块的内部无论如何变化,对其他模块的影响低都能减少到最小。如果一个模块中实现的内容要参照其他模块的内容才能理解,或一个模块发生了变动之后其他模块都需要相应的改变,这样的模块就是独立性比较差的模块。
我们可以从What和How这两个角度来观察一个模块。What指的是这个模块提供了什么功能。How指的是这个模块如何实现这些功能。一个独立性高的模块会把What和How清楚的分开。相反,独立性低的模块往往无法良好地区分What和How,说不清它所提供的功能。
就独立性高的模块而言,我们只需要知道它所提供的功能就能够良好的使用,不需要了解其内部是如何实现的。以独立性高的模块为基础,就算是大型软件也可以很容易的完成。独立性高的模块的声明和实现是分开的,只要声明保持一致,具体实现可以随时被g更换成性能更高的实现。独立性高的模块一旦发生问题,只需要更改这个模块即可,这大大地提高了软件的可维护性。
相反,如果一个模块封装的不好,其他模块直接使用了这个模块内部的一些实现,那么未来无论是想更新模块内部的某个方法的实现还是想替换这个模块都会很麻烦。
1.2.3 模块的信息隐蔽
模块独立性的划分原则是只对外提供最小限度的接口信息,内部实现不对外公开。也就是把模块做成一个黑盒。这个原则叫作信息隐蔽或封装。
如果提供方提供的模块是一个黑盒,那么调用方就只能使用该模块所提供的功能来开发软件。提供方也只需要按照事先的约定实现功能即可。这样的话,一旦提供方发现了更快的算法或更有效率的内存管理方式,就可以很容易地对内部实现进行。替换,这也符合之前说明的对于高独立性模块的要求。另外模块的调用方也不需要了解模块的内部实现细节,只需按照事先约定好的接口来调用函数即可,这样更容易制作出可重用的软件。
传统的面向过程语言的封装功能有限,很难制作出高独立性的模块。在这些语言中,数据和对数据的操作是分离的,从语言层面上无法提供模块化的解决方案,也不能对外隐蔽模块的内部实现。
从1980年开始,人们开始思考如何从语言层面上来提供模块化的支持,例如Pascal为基础开发出了Modula-2模块、Ada的包等。
C语言的本身是以文件为模块的,难以实现多层次的模块。通过使用static函数或特别构造头文件等方法也能够做出高独立性的模块,但这对程序员的要求比较高,一不小心就可能弄出低独立性、不好理解的程序。
1.2.4 类的定义和接口
在绝大多数基于面向对象语言开发的程序中,对象是由类的定义来描述的,实例对象是在程序运行的时候动态生成的。也就是说,类的定义和实现文件就是构成程序的模块。类是由实例变量和方法构成的,所以类的定义中包含了实例变量和方法的定义。对象的使用者只关心到底如何使用这个对象,而不关心它的内部实现。
类公开给外部的、关于如何使用这个类的信息叫作接口。接口中定义了这个类所包含的实例变量和可接收的消息。
从类的外部也就是类的使用者的角度来看,只能看到接口中定义的信息,无法看到类的内部实现和数据定义等。图1-6 用一个电视的例子说明了接口的概念,用户只需要知道电视的使用方法(接口)就可以了,不需要了解电视内部的各种实现的细节。
如上面例子所示,类把对外公开的方法记录在接口中,除此之外各种详细的实现对外均不可见。一个设计良好的类只会把必须公开的信息记录在接口中,这样才能够加强独立性。
就类的定义而言,有把接口和实现分开写的语言,也有写在一起的语言,Objective-C属于前者。
1.2.5消息发送的实现
一个程序通常是由多个对象构成,而至于每个对象到底该如何完成预设的功能,则有各种各样的实现方法。其中一种实现的方式就是多个对象同时执行,对象之间的消息通信也并进行(异步)。就好像很多人在参加一个宴会一样,每个人都可以和其他人说话。另一种方式通常是一个对象发送消息触发其他对象的消息处理的函数,整个过程好比一个顺序发言的研讨会。
现在在很多面向对象的语言中,消息发送就好像函数调用一样,可以将某个对象发送消息看作调用这个对象的一个函数。虽然在消息的发送方法、响应方法的确定等方面和函数调用还有很多不一样的地方,但在发送方发送消息侯需要等到接受方处理完成才能返回这点上和函数调用是一样的。
另一方面,进程间通信、不同主机间的网络通信也可以抽象地看成对象间的消息通信。把这些情况都考虑在内的话,就会出现很多仅仅使用函数调用这种形式无法解决的情况。例如为了高效率地实现进程间的通信,Objective-C特别定义了一种接受方无需返回应答的消息。