面向对象闲话(一)——什么是对象
如果你要把一只猫卖给一个计算机专家,千万不要说它有多聪明多可爱,而要说,这是一只面向对象的猫。这是程序员对程序员的揶揄。
面向对象渗透到软件的各个领域,既然找不到银弹,这颗铜子弹成了我们对抗人狼的最佳武器。
在这样的世界里,你有没有想过,什么是面向对象呢?
如果你习惯性地说:继承、封装和多态,那么请你继续读完这篇随笔吧,它会带你认识一个真实的OO
我想提及面向对象之前,应该先搞明白什么是对象。《面向对象分析与设计》(Grandy
Booch)中提到,对象是一个朴素的概念,约一岁以后,人会培养出一种对象概念的能力。从人类的认知角度来说,对象应是以下之一
- 可触摸或可见的东西
- 可被理性理解的东西
- 可进行想像或施加动作的东西
在英文中Object是一个应用广泛的词,它的出现早于程序语言出现之前,我想用中文中一个最适合的词来翻译,应该是"东西"而非"对象",当然有点太口语化了。Grandy
Booch接下来还提到,现实生活中的对象不是软件开发中唯一感兴趣的对象,另一些重要的对象是设计过程的产物。最后的总结是:对象具有状态、行为、和标识。
对象具有状态、行为、和标识这三点是非常重要的,这是面向对象大师Grandy
Booch对朴素对象概念的非常精确的概括。
《面向对象编程导论》(Ian Graham)中对面向对象有这样描述:一些作者已经强调了面向对象程序设计和面向数值程序设计的区别。MacLenna(1982)指出,值(例如数字17)是应用式的和只读的,他们总是抽象的。对象(例如实例)在一定范围内存在,可以被创造、撤销、共享和更新。数值是引用透明的,引用它的一切事情将使用同样的数值。
从这一段我们可以看出,与"对象"相对的并非"过程",而是"值"。值和对象最显著的区别正是Booch总结的3点,状态、行为、和标识。举一个例子,3是一个值,苹果是一个对象。苹果具有状态,它可能已经烂了,或者还没熟;苹果也有行为,它可以被削皮,可以被咬一口,也可以变烂;标识保证了,苹果是唯一的,你无法在世界上找到任何一个其它苹果,使得它跟这个苹果是“同一个苹果”。而对值来说并非如此,3就是3,是没有状态的;3也是不可改变的,你只能由3得到一个新的值4,却没法把任何一个3变成4;值是不需要标识的,世上所有3都是相同的,没有任何区别。
在这种朴素地认识下,我想可以分析一下C++对象模型的设计:用内存中连续的一段存储一个对象的状态,这样可以将它们的地址作为标识,将对象分类并将他们的行为以成员函数的形式体现
面向对象闲话(二)——面向对象设计
对象与类
上篇文章谈到了"什么是对象"问题。而事实上,我们所见过和学习的大多数面向对象语言,迎面而来的一个概念是:类。
遗憾的是,大部分程序语言的书籍,都是直接开始讲解类的概念,并没有着重强调类和对象的关系。所以,面向"对象"的语言,为何引入了这样一个"类"的概念呢?最简单的回答是,你不能够一个一个地去描述对象,那样太愚蠢了。
类对于一般的人类而言,同样是一个朴素的概念,在比对象认知稍晚些时候,人类开始具有抽象能力:小孩子不再说“我要那个”,而是开始表达“我要苹果”。
再更大一些时候(也许五六岁?),小孩子开始能够把苹果、梨子、香蕉等概念抽象成“水果”。这个时候,类层次关系开始出现在人的认识中。
面向对象编程的最重要意义在于它提供了一种接近人类思维的表达方式,只要人类的思维模式不发生根本性改变,面向对象绝对不会过时——它的表达形式可能多种多样,但是任何从根本上否认面向对象的所谓“反思”,皆不可相信。
分类与归类
对于类层次关系,还有个有趣的问题:分类与归类。
一种建立类层次关系的方法是,分类。就是,把所有对象放在一起,称作一个类Object,接下来,根据特征看看Object能分成哪些类,这些类又能分成哪些子类……以次类推。这样的方法得出的结果,类与类之间是不存在交集的,并且所有类最终都会继承一个基类Object。使用这样的逻辑的语言典型代表有Java和C#。
另一种方法是,归类。就是对于每一个对象,根据它的特征,看看它属于哪些类。这时,一个类可能有多个父类,这种方法,就会产生所谓的“多继承”关系。使用这样的逻辑的语言典型代表就是C++。
所以,实际上流传甚广的说法“C#和Java不能多继承类,只能提供多继承接口”是不恰当的(不能说不对),如果你使用C#或者Java这样的语言,从设计开始,就完全不可能出现需要多继承的情况。只有理解了语言背后采用的哲学,才能够正确使用语言。
面向对象设计
《C++程序设计语言》一书中,讲到了一系列设计的步骤:
- 发现类
- 描述操作
- 描述依赖性
- 描述接口
发现类最简单和行之有效的方法就是从需求描述中寻找。一些名词往往对应着一个类。而TC++PL中还提到了几种情况:
- 动词可能意味着对象上的操作、全局函数或者类
- “重复”、“将……作用于”往往意味着迭代器对象
- 形容词“可存储的”、“并行的”、“注册的”、“约束的”可能成为类(winter注:C#或者Java中,它们更可能作为接口或者attribute)
有趣的是,自然语言非常自由,所以程序未必应该完全对应于需求描述,比如著名的“狗咬人”问题:
A dog bites a person. A person is bited
by a dog.
写成程序,就是:
Person person = new Person;
Dog dog = new Dog;
dog.bite(person);
person.isBitedBy(dog);
两种设计哪个更好呢?
我们在写程序的时候,不可能受到需求文档使用句型的影响,这个时候,我们必须回到对象的本质上面:标识、状态、行为。而对象的行为,必定是改变对象自身状态或者对外输出对象状态的。
这样,在这个场景里面,答案就是显而易见的了:
人的状态改变,所以人应该有hurt方法。
狗的状态未改变,但是咬这个动作必须根据它的内部状态输出伤害,所以dog应该有biteDamage方法。
最后,一个良好的设计是:
person.hurt(dog.biteDamage());
所以,不要陷入“面向对象语言描述要尽量跟需求描述一致”,正确的抽象才是根本。
设计实践
一个常见的错误是把类与模块相混淆。比如以下类就很可疑:
Login类
DBHelper类
BusinessLogic类
模块是一个相对独立的功能单元,一般来说,模块可能包含很多类。如何避免这样的错误呢?
我自己喜欢使用先对象、后类的设计方法。也就是说,先完全不考虑类的问题,将系统中的具体对象识别出来。我在OOD阶段做的第一件事,就是在设计图中间画一条线,线上面的对象是可见的,线下面是不可见的逻辑对象。
下图是我编写的一个黑白棋游戏(shaofei.name/othelloAI/othello.html)时画的对象图:
这种方法可以帮助我们发现一些坏味道,比如,每一条跨过分割线的依赖线条,都应该是同一个方向的。
应用了MVC模式以后,Controller位于线的中间,它阻止了UI对象直接控制业务逻辑:
因为是one man project我使用的图形比较简单,在正式的项目中,UML的对象图和时序图都是非常强有力的工具。 |