面向对象闲话(二)——面向对象设计
惭愧,一个系列第二篇能跟第一篇隔两年之久,我还真是……
对象与类
上篇文章谈到了"什么是对象"问题。而事实上,我们所见过和学习的大多数面向对象语言,迎面而来的一个概念是:类。
遗憾的是,大部分程序语言的书籍,都是直接开始讲解类的概念,并没有着重强调类和对象的关系。所以,面向"对象"的语言,为何引入了这样一个"类"的概念呢?最简单的回答是,你不能够一个一个地去描述对象,那样太愚蠢了。
类对于一般的人类而言,同样是一个朴素的概念,在比对象认知稍晚些时候,人类开始具有抽象能力:小孩子不再说“我要那个”,而是开始表达“我要苹果”。
再更大一些时候(也许五六岁?),小孩子开始能够把苹果、梨子、香蕉等概念抽象成“水果”。这个时候,类层次关系开始出现在人的认识中。
面向对象编程的最重要意义在于它提供了一种接近人类思维的表达方式,只要人类的思维模式不发生根本性改变,面向对象绝对不会过时——它的表达形式可能多种多样,但是任何从根本上否认面向对象的所谓“反思”,皆不可相信。
分类与归类
对于类层次关系,还有个有趣的问题:分类与归类。
一种建立类层次关系的方法是,分类。就是,把所有对象放在一起,称作一个类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的对象图和时序图都是非常强有力的工具。