谈谈面向对象 之 继承和多态

一、引言

除了抽象和封装,面向对象还有另外两个特征:继承和多态。由于继承实在没有太多可说的,而且这两个特征的关系又非常密切,所以就放在一起谈了。

二、继承

继承最初是一个生物学上的概念,是指因遗传而获得,后来引申到社会学,指老子死了,儿子得到遗产之类。面向对象中的继承,自然使用的是生物学上的概念,是指子类获得父类的属性或方法。

继承在实际开发中往往被滥用:本应为包含关系的,为了使用的便利性,却被设计为继承关系。

包含体现的是HAS-A关系,继承体现的是IS-A关系。因此,在使用继承时要非常慎重,确定一定以及肯定为IS-A关系的,才使用继承。

在桥接模式的例子中,由于抽象出了窗口的实现,使用了包含关系,才得到了灵活的结构。

否则,如果都使用继承关系,把XWindow、PMWindow也设计为Window的子类,那就会导致类爆炸:出现Icon_X_Window、Icon_PM_Window、Transient_X_Window、Transient_PM_Window,局面就不可收拾了。

在GoF的23个设计模式中,装饰器模式是最为精巧的,这种设计模式既利用了继承,又利用了包含:

  • 装饰器要继承被装饰的对象,这样才能向调用者提供一致的接口,让调用者感觉不到变化;
  • 装饰器要包含被装饰的对象,这样才能既做装饰器自己的事情,又做被装饰对象原来的事情。

三、多态

多态原本也是一个生物学上的概念,是指同一物种有不同形式的个体。所以翻译为多样性会更容易理解,但这么直白的词,会让程序员少了耍酷的机会。

对于多态,有多种类型的说法:

  1. 父类的同一个方法,在不同的子类中有不同的实现;
  2. 同一个类中,对相同方法名的重载(overload);
  3. 一个接口,有多种不同的实现;
  4. 最奇葩:泛型中,可以使用不同的类型参数。

究其本意,第一种能真正称得上多态。基于这一认识,我们再进一步具体化,在Java和C#中,多态体现为两种形式:

  1. 父类为抽象类,声明了抽象方法,不同的子类有不同的实现;
  2. 父类中定义了虚方法,不同的子类有不同的实现,即进行重写(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的方法,子类实现这些方法就可以了。

四、结束语

面向对象这个小系列到这里就谈完了。

但是仅仅看面向对象方面的知识,很难对其有透彻的理解。所谓功夫在诗外。想要做到透彻理解,程序猿们应该:

  1. 大量的开发实践;
  2. 掌握计模式;
  3. 掌握面向对象设计原则;
  4. 掌握重构方法。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值