面向对象缺陷分析

教科书上说,面向对象三大特性是封装、继承、多态。其中封装和多态并非面向对象原创,比如说USB口可以插入U盘、手机、ipod等设备,这就是多态。插入之后电脑内部怎么运转不用管,看显示器操作就行,这就是封装。在软件开发领域,面向对象出现之前,封装和多态虽然没有具体概念,但是其思想却已经广为应用。我认为,最能代表面向对象的,也是最值得喷的特性,就是继承。

将世界看成对象的集合

据说,面向对象是比较适合人的思维的,因为人在看世界的时候就是一个个对象,因此面向对象有利于程序的构建。这个观点是非常形而上(美其名曰哲学)的,其正确性先不去讨论。为了模拟对象,class关键字引入各种面向对象语言,把属性与操作(方法)封装于其中。在现实世界中对象似乎有一定层次关系,比如鸡鸭牛羊属于动物,动物属于生物,生物属于对象……以此为据,面向对象先驱们设计了继承这一思想,让class之间也形成继承关系,这样既有助于理解类的概念,又让代码有层次性,而且还提高了代码的复用性——少写了不少代码,提高生产力。自此,以class、继承为基础的面向对象思想、语言、设计方法大行其道,直至今日。

继承的问题

直觉上看,继承确实符合人认识事物的思维。但是在实践中,我发现继承有时不那么好用。比如番茄属于植物,属于食物,然而植物和食物之间的关系不是继承也不是并列,而是有交集。在食物类中,番茄同时具有水果和蔬菜的特性,即便现实世界也很难区分。这就是继承的问题所在:面向对象语言中的继承体系是比较严格的树形关系,而现实世界或者软件需求却不一定是树形,很可能关系错综复杂。

我在“设计模式的本质思想”一文中提到一个例子,现在将这个例子简化一下。有个Bird类,类内有Sing(鸣叫)和Move(移动)两个方法,具体操作什么由子类实现。先设计基类,如下

//纯面向对象语言java登场
abstract class Bird {
	abstract void Sing();
	abstract void Move();
}

Java号称纯面向对象语言,一切都是对象。看看又是abstract又是class,真有面向对象风格,基类设计的没太大问题。然后我们实现(泛化)一个麻雀类,叫声是"Jijijiji",移动方式是"Fly"(不要联想JJFly),一个鸽子类,叫声是"gugugu",移动方式是"Fly"。如下

class Sparrow extends Bird{
	@Override
	void Sing() {
		System.out.println("jiji");
	}
	@Override
	void Move() {
		System.out.println("Fly");
	}
	
}
class Dove extends Bird{
	@Override
	void Sing() {
		System.out.println("gugugu");
	}
	@Override
	void Move() {
		System.out.println("Fly");
	}
}

至此,一个简单的鸟继承体系完成。在应用层使用Bird对象的时候不必管具体子类会怎么实现,似乎很好地实现了模块化。

不过仔细一看有重复代码,Fly那一部分似乎是重复的。那我们提取一个FlyBird类,移动方式是"Fly",叫声由子类实现,麻雀和鸽子都继承自FlyBird。看起来不错。

现在我们在加入小鸡类,叫声是"jijiji"而移动方式是"walk",叫声那部分代码可能也会重复。有兴趣的同志可以试试,麻雀、鸽子、小鸡这三个类只用单继承,很难避免代码重复。出现这一问题的原因是,类之间的关系很复杂,只靠简单的树形继承无法表达这些关系。

解决方案——设计模式诞生

设计模式远远没有大家想的和他自己鼓吹的那么高大上,其出现主要还是为了解决上面这个问题。看看如何用策略模式解决这一问题

//策略模式的思想是组合代替继承
//先声明两个接口
interface Move{
	void move();
}
interface Sing{
	void sing();
}
//基类的行为不直接定义,而通过接口实现
class Bird {
	Bird(Move m, Sing s){
		this.m = m;
		this.s = s;
	}
	private Move m;
	private Sing s;
	
	public void Sing(){
		s.sing();
	}
	public void Move(){
		m.move();
	}
}
//实现飞行动作
class Fly implements Move{
	public void move(){
		System.out.println("Fly");
	}
}
//实现Jiji叫声
class Jiji implements Sing{
	public void sing() {
		System.out.println("jijiji");
	}
}
//麻雀类
class Sparrow extends Bird{
	Sparrow(){
		super(new Fly(), new Jiji());
	}
}

按照这种方式,用组合代替继承,合理的避免了重复代码,软件再扩大时也比较容易管理。确实是个不错的解决方案。

其他面向对象解决方式

多重继承

单一继承的树形关系不是难以模拟需求吗?我用多重继承,实现FlyBird/WalkBird,以及JijiBird/GuguBird,麻雀继承自FlyBird and JijiBird(别想歪了),鸽子继承自GuguBird and FlyBird。理论上在这个问题中,多重继承也能合理的组织类的关系,但是多重继承问题很多,最主要的,继承自两个类,两个类的属性和方法保存一份还是两份?保存谁的?如果问题再复杂点,多重继承恐怕也很难满足需求。

方法2,将所有操作,比如Fly/Walk/Jiji/Gugu,都放在基类中实现,子类只负责调用。当子类没有新属性的时候这不失为一种解决办法。看起来很扯淡,其实是有应用的,比如基类是单链表,子类封装起来调用插入删除方法可以形成栈与队列。

其他语言的解决方式

C语言

#include <stdio.h>
struct Bird{
	void *property;
};
//函数指针模拟虚函数
typedef void (*MoveFun)(Bird*);
typedef void (*SingFun)(Bird*);
void Fly(Bird*){
	printf("Fly\n");
}

void Gugu(Bird*){
	printf("Gugu\n");
}

int main(){
	//下面三行初始化一个鸽子
	Bird b;
	MoveFun doveMove = Fly;
	SingFun doveSing = Gugu;
	
	doveMove(&b);
	doveSing(&b);
}

我学C++面向对象经历了这么个过程:class不就是struct加函数么。。。原来面向对象虚函数可以实现应用实现分离,不错。。。C语言加函数指针也能实现虚函数。。。有些东西不太好设计,还好我有设计模式。。。这TM不就是struct加函数指针么!!!

在此稍微总结一下,通过策略模式和C语言的两个例子,我们发现组合通常比继承来的爽快,能够更简单更直接地解决问题。然而面向对象语言通常不把类内方法当成可以赋值的变量(函数不是first-class),而是写了之后就固定了,和类型严格绑定,这可能是造成继承不灵活的根本原因。在python语言中,类内的方法强制带self参数,可以调用类外的函数模拟对类内函数的赋值,实为面向对象史上一大进步(此前对python有误解,现已修改)。

即使应用如上的策略模式,运行时如果不允许替换方法,面向对象的缺陷还是很大,比如小鸡长成大鸡,叫声从Jijiji变成Gugugu,恐怕要重新设计了(简单的处理方式是有的,比如把两个接口对象改成public随意改动,不过这肯定会因为太不OO而被否决)。然而用C语言就不需要所谓的设计。

Lua语言

function Fly()
	print("Fly")
end
function Gugu()
	print("Gugu")
end

function GetDove()
	dove = {}
	dove.Move = Fly
	dove.Sing = Gugu
	return dove
end

dove = GetDove()
dove.Move()
dove.Sing()

lua不支持面向对象,然而用table可以完美的模拟面向对象做得到和做不到的事。table只是一个key-value的集合,不去刻意区分属性和方法,不管属性还是方法都能随意替换。同时Lua是弱类型语言,如果把dove对象的Sing改成Jiji,那么dove就变成了一个麻雀,这就是传说中的“鸭子类型”。

继承的适用范围

实践来看,如果你想通过继承实现开闭原则——新需求只需要在已有类的基础上添加而不需要改动已有类——简直是痴人说梦。如果在设计之初就考虑应对变化,那么往往也在浪费时间,你设想的变化通常不会出现,出现的都是你没想到的。继承为主的面向对象思想,不仅不适合变化快速繁多的场合,反而对需求变化不太大的模块更能胜任:GUI中的控件(同类控件除了绘制和鼠标消息基本没什么变化的需求,一个控件写好了到处通用N个版本也不会改)、基本数据结构和算法(基本没有需求变化)、编译原理的词法分析模块(至多对内部算法优化,对外接口不会有变动) 。

总结

为了弥补继承的缺陷,编程语言引入各种概念,多重继承(c++、python)、Mixin(模拟多重继承)、delegate(C#,不过delegate作为lambda比C的函数指针要强)、functor(C++)、interface(Java and others)以及各种设计模式。C语言和lua的table有更简单更直接的实现方式,不过人家不支持面向对象,请读者不要在这扯皮说C也有面向对象思想精髓之类。

在此引用宏哥一句话:OO就是把屁股朝天拉屎, 不拉在裤子上的就叫做"OO设计师"。整个OO的设计精髓就在于不要把屎拉裤子上。问题是, 为什么要朝天拉屎呢 --  为了OO。

转载于:https://my.oschina.net/xiaoguono1/blog/284220

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值