最近发现一个面向对象比较有趣的讨论:谈谈继承的局限性(http://www.cnblogs.com/xrunning/archive/2011/10/17/2214487.html).
以四边形,矩形,正方形为例子讨论继承的问题。矩形是两对边平衡的四边形,而正方形是内角为90度的矩形。
一种设计方式是:四边形做基类,矩形继承自四边形,正方形继承自矩形。理由是:矩形是四边形,正方形是矩形。矛盾在于,面向对象的类继承隐喻了这样一件事:子类是父类的超集(包含父类),但是正方形并不需要四条边长的属性,而只需要一条边长(因为4条都一样),也就是正方形反而比四边形要小。
评论指出:这个设计的病根在于设计逻辑,继承应该基于概念的外延,也就是类似松树是树的扩展,所有松树必然包含树的内容。而正方形只是对四边形进行限制,也就是限制了四条边相等,四个内角相等。
这种说法貌似挺有道理,也是我们设计中可以借鉴的。但是认真思考一下,反而觉得不是那么明确。
我想要解决这个问题,应该从根本上对继承进行深入的解剖。继承隐喻了子类是父类的超集这一技术特征,因此省去编码的麻烦,他等价于把父类作为子类成员。
如:
class 正方形
{
四边形 Base;
}
我们很容易发现问题是我们完全不需要四边形来实现正方形,我们只需要一个int成员就能够实现正方形。继承的问题在于继承强制性的决定了子类只包含父类,而这种假定的目的仅仅是为了形成继承关系,也就是多态(通用访问界面)。这其实只需要通过接口便可以完美的解决。
继承和接口的分别就是接口不限制实现,你可以用任何方式实现子类。继承本身不但包含了接口,还包含了数据定义等属于实现细节的内容。
我认为,继承是不完全的面向对象技术,因为面向对象最重要的内容就是隐藏实现,隔离外部和内部,实现多态访问,但是继承会让同一个继承族的类之间无法做到这一点。
如果让我设计面向对象语言,我会让类自身无法访问,而只能通过接口访问。类负责实现细节,接口负责访问界面。
如果按照接口的角度去设计这三个对象,我们只会得到一个接口,那就是“可计算面积的多边形”,然后包含一个统计面积的函数。用户的最终目的是面积,而什么边长、角度之类的都只是细节罢了。
可见,接口才是为使用者服务的,而继承只是一种不成功的设计模型,他是为细节实现服务的一种自以为高明的范式。继承会让你认为只要不断地扩展子类,就能设计出一个完美的系统,它基于一种似是而非的推理逻辑:子类是父类的超集,这种“是”关系很常见,而现实是这种模型并不常见。人们说的“是”,大多数情况都不是一种子集和超集的关系,反而是一种更加反叛的,对父类否定和修改的模型。
进阶话题:不同模型的设计方式
我上面谈到模型的问题,现实问题中会出现哪些模型,下面归类一下。
子集超集关系:继承就是基于这种关系的设计范式。但是组合是更好的方式,因为组合可以替换成员的实际类型,继承却不可以。用“子类是父类”这种语言大多数情况是无法产生正确的设计,需要用“子类有成员”这样的语言去推理。
原型和特例的关系:这是比较常见的情况。大多数对象都有一个共同的原型,但是大多数都需要经过一些修正才能符合需求。
无关系:没有共同点的对象。
注意!这里说的是实现细节的问题,而接口不参与实现细节,所以是属于不同范畴的话题。
进阶话题二:参数化类型
和面向对象相对的另一个设计哲学就是泛型,泛型是参数化的类型。事实上,参数化是一个很通用的技巧,比如函数的参数化,一个函数依赖参数,而不是依赖具体数据,这样便可以得到通用的算法。
我把方法进行了一些归类:
过程:一个代码片段
函数:一个算法
泛型函数:一个通用算法
可见,参数化的目的是得到更加通用的算法。
参数化类型的目的也是类似的,为了得到一个更加通用的类型。对设计对象而言,成员就是其参数。如果不对成员进行参数化,那么这个对象就是死板的。笔者有一个不太成熟的想法,通过构造函数传递对象的参数,这个参数实际就是初始化成员,而成员由方法使用,这就得到了一个通用的对象。
至于参数化成员的多少与相对重要性等问题,和方法的使用情况有关,如果该成员构成对象的本质属性,他就应该参数化。比如汽车的引擎就应该参数化,至于颜色之类的关系不大的可以无需参数化。
笔者认为,一个成功的面向对象设计,应该正确处理界面和实现的关系(通过接口而不通过类访问对象),应该避免使用继承(通过使用组合和接口替代),应该更加的通用(参数化关键成员)。
有些时候,成员没有使用其他类型,而方法里面却使用了其他类型,这更加糟糕,因为方法硬编码调用了其他类型,会产生更加紧密的耦合,改进的方法是把它作为成员,或者把他做为方法参数。至于什么时候应该作为成员,什么时候应该作为方法参数,笔者暂时还不能提供建议。
对象方法分析细节:
1.过程,无参数
2.过程,无参数,调用成员
3.过程,无参数,调用成员,创建外部对象
4.函数,有参数
5.函数,有参数,调用成员
6.函数,有参数,调用成员,创建外部对象
对于对象设计来说,如果不调用成员,那么大可以作为类成员,而不是对象成员来实现。如果一个过程,不调用外部成员,那么他就是一个固定的没有变化的函数。如果它是一个函数,它就是一个纯粹的工具算法,和类型无关,不如把它移出去并进行通用化。
一个普通的方法,应该是那些调用成员的方法。一个劣质的方法,是那些创建对象的方法,因为这会增加方法对外部类型的依赖。