一、引言
除了抽象和封装,面向对象还有另外两个特征:继承和多态。由于继承实在没有太多可说的,而且这两个特征的关系又非常密切,所以就放在一起谈了。
二、继承
继承最初是一个生物学上的概念,是指因遗传而获得,后来引申到社会学,指老子死了,儿子得到遗产之类。面向对象中的继承,自然使用的是生物学上的概念,是指子类获得父类的属性或方法。
继承在实际开发中往往被滥用:本应为包含关系的,为了使用的便利性,却被设计为继承关系。
包含体现的是HAS-A关系,继承体现的是IS-A关系。因此,在使用继承时要非常慎重,确定一定以及肯定为IS-A关系的,才使用继承。
在桥接模式的例子中,由于抽象出了窗口的实现,使用了包含关系,才得到了灵活的结构。
否则,如果都使用继承关系,把XWindow、PMWindow也设计为Window的子类,那就会导致类爆炸:出现Icon_X_Window、Icon_PM_Window、Transient_X_Window、Transient_PM_Window,局面就不可收拾了。
在GoF的23个设计模式中,装饰器模式是最为精巧的,这种设计模式既利用了继承,又利用了包含:
- 装饰器要继承被装饰的对象,这样才能向调用者提供一致的接口,让调用者感觉不到变化;
- 装饰器要包含被装饰的对象,这样才能既做装饰器自己的事情,又做被装饰对象原来的事情。
三、多态
多态原本也是一个生物学上的概念,是指同一物种有不同形式的个体。所以翻译为多样性会更容易理解,但这么直白的词,会让程序员少了耍酷的机会。
对于多态,有多种类型的说法:
- 父类的同一个方法,在不同的子类中有不同的实现;
- 同一个类中,对相同方法名的重载(overload);
- 一个接口,有多种不同的实现;
- 最奇葩:泛型中,可以使用不同的类型参数。
究其本意,第一种能真正称得上多态。基于这一认识,我们再进一步具体化,在Java和C#中,多态体现为两种形式:
- 父类为抽象类,声明了抽象方法,不同的子类有不同的实现;
- 父类中定义了虚方法,不同的子类有不同的实现,即进行重写(override)。
Java和C#对待虚方法的态度是不同的:
- Java中,最大限度地允许重写:只要没有声明不允许重写(使用 final 关键字),子类就可以重写;
- C#中,最小限度地允许重写:只有声明了可以被重写(使用abstract或 virtual 关键字),子类才可以重写。
老谭认为,应该对重写进行限制,所以拥护C#的做法。我所说的这种限制更为苛刻:尽量使得可重写的方法为非public的,也就是说,在C#中,尽量使得abstract或virtual方法为protected。
例如,定义了父类Shape,以及两个子类Circle和Rectangle:
abstract class Shape
{
public abstract void Draw();
}
class Circle : Shape
{
private float radius;
public override void Draw()
{
if (radius > 0)
{
DrawCircle();
}
}
private void DrawCircle()
{ ... }
}
class Rectangle : Shape
{
private float width;
private float height;
public override void Draw()
{
if (width > 0 && height > 0)
{
DrawRectangle();
}
}
private void DrawRectangle()
{ ... }
}
Shape定义了public抽象方法Draw,什么都不做,就交给两个子类去处理。Circle和Rectangle只有实现public Draw:Circle在确定自己的半径大于0时,才去绘制圆;Rectangle在确定高和宽均大于0时才绘制长方形。这个先判断再执行的逻辑在子类中反复出现,可以将其提取到父类中:
abstract class Shape
{
public void Draw() // 注意:public方法,既非abstract,也非virtual
{
if (IsValidShape())
{
DoDraw();
}
}
protected abstract bool IsValidShape();
protected abstract void DoDraw();
}
class Circle : Shape
{
private float radius;
protected override bool IsValidShape()
{
return radius > 0;
}
protected override void DoDraw()
{ ... }
}
class Rectangle : Shape
{
private float width;
private float height;
protected override bool IsValidShape()
{
return width > 0 && height > 0;
}
protected override void DoDraw()
{ ... }
}
将abstract和virtual尽量限制为proetcted,是基于两方面的考虑,一是封装,二是依赖倒置原则(DIP)。
首先看封装,这里指的是将public方法封装在父类中,在实现这些public方法时需要调用abstract或virtual的protected方法,也就是用模板方法这个设计模式来实现。这有两个方面的好处,其一,逻辑集中起来,不分散到各个子类中;其二,变动时影响范围小,如果需要修改public,只在父类中修改就可以了,不会波及到每个子类。
再看DIP,所谓依赖倒置,是指下层依赖上层,或者具体依赖抽象。人们谈到DIP时,常常把接口当作抽象层,DIP就是面向接口编程。其实,父类也是一层抽象:通过将public方法集中到父类,就使得调用者只需要调用父类的方法,不能调用子类的。在上面的例子中,Circle和Rectangle的属性都是private或protected,没法调用。同时也做到了具体依赖抽象:子类的实现依赖于父类,父类定义了abstract或virtual的方法,子类实现这些方法就可以了。
四、结束语
面向对象这个小系列到这里就谈完了。
但是仅仅看面向对象方面的知识,很难对其有透彻的理解。所谓功夫在诗外。想要做到透彻理解,程序猿们应该:
- 大量的开发实践;
- 掌握计模式;
- 掌握面向对象设计原则;
- 掌握重构方法。