设计思想:多用组合少用继承?

2,071 阅读5分钟

老生常谈,组合优于继承,多用组合少用继承。为什么继承不建议使用了?组合解决了继承什么问题呢?组合与继承该如何作出选择?我们一起来看看这些问题。

继承犯了什么错?遭人嫌弃?

继承是面对对象的四大特性之一,表示 is-a 的类关系。继承特性解决了代码复用的问题,但是继承的深度过度,代码会变得更复杂,变得更难以维护。继承的使用,一直存在很大的争议,甚至还有人认为他是一种反模式,被建议尽量少用。

我们看看继承的问题。

假如定义一个关于虫的类。车类我们定义抽象类为 AbstractInsect,在抽象类中定义虫固有的属性和方法。

public abstract class AbstractInsect {
    //...
}

我们知道大部分的虫类都是不能飞的,但是也不排除部分的昆虫是有飞行能力,比如飞蚁、蚊子。这时候,我们怎么改造这个抽象类呢?

我们可以直接在AbstractInsect类中,定义一个新的方法fly()吗?

改造后的代码:

public abstract class AbstractInsect {
    public void fly() {
        //...
    }

    //...
}

// 毛毛虫
public class Caterpillar extends AbstractInsect {
    public void fly() {
        throw new UnSupportedMethodException("I can't fly.'");
    }
    
    //...
}

// 蚊子
public class Mosquito extends AbstractInsect {
    public void fly() {
        //...
    }
    
    //...
}

这种实现方式虽然可以解决问题,但是会带来诸多问题。一方面增加了上层业务的工作量;另一方面,违反了迪米特法则,暴露了有问题的接口给外部调用,容易误用。

你可以会提出疑问,可以针对 AbstractInsect 做进一步抽象吗?针对飞行能力,拆分成会飞的虫类AbstractFlyInsect和不会飞的虫 AbstractUnflyInsect ,这样毛毛虫 Caterpillar 继承 AbstractUnflyInsect ,蚊子 Mosquito 继承AbstractFlyInsect,问题不就解决了么?

到这里,继承关系已经变成了3层了,目前还可以接受,层次比较浅,也算是一种较良好的设计。但是随着功能的迭代,需求提出需要增加是否可以叫的能力。按照上面的方式进行扩展,我们应该对会飞的虫类AbstractFlyInsect和不会飞的虫 AbstractUnflyInsect俩个抽象类做进一步的抽象,分别为会飞会叫的虫类、会飞不会叫的虫类、不会飞会叫的虫类、不会飞不会叫的虫类。这时,继承关系已经变成4层了。

以此类推,随着能力的添加,抽象的组合就会成倍增加,类的继承层次也会更深更复杂。

这里存在俩个比较大的问题:

  • 代码的可读性很差。若要了解清楚父类方法实现,要一直往上层追溯,直到根部方法;
  • 破坏了类的封装性。不知道你有没有发现,增加能力fly时,需要修改子类的继承。一旦父类的代码修改,就会影响到子类的逻辑;

综上所述,继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性。

组合是否能解决继承的问题

其实,我们基于组合、接口、委托来解决层次过深、继承关系过于复杂的问题。

接口表示具有某种行为特性,has-a特性。上面的问题,针对会飞的能力定义接口FlyAble,会叫的能力定义接口TweetAble

public interface Insect {
}

public interface FlyAble {
    void fly();
}

public interface TweetAble {
    void tweet();
}

// 毛毛虫
public class Caterpillar implements Insect, TweetAble {
    public void tweet() {
        //..
    }
    //...
}

// 蚊子
public class Mosquito implements Insect, FlyAble, TweetAble {
    public void fly() {
        //...
    }
    
    public void tweet() {
        //...
    }
    //...
}

上面的代码很好地解决了问题。但是这里又出现了另外的问题,毛毛虫Caterpillar和蚊子Mosquito都实现了TweetAble接口,这里有可能会出现重复的逻辑实现,那么这个问题应该如何解决呢?

我们可以通过委托的方式去解决问题。

public class FlyAbility implements FlyAble {
    public void fly() {
        //..
    }
}

public class TweetAbility implements TweetAble {
    public void tweet() {
        //..
    }
}

// 毛毛虫
public class Caterpillar implements Insect, TweetAble {
    private TweetAbility tweetAbility = new TweetAbility();
    public void tweet() {
        tweetAbility.tweet();
    }
    //...
}

// 蚊子
public class Mosquito implements Insect, FlyAble, TweetAble {
    private FlyAbility flyAbility = new FlyAbility();
    private TweetAbility tweetAbility = new TweetAbility();
    
    public void fly() {
        flyAbility.fly();
    }
    
    public void tweet() {
        tweetAbility.tweet();
    }
    //...
}

继承,表示 is-a 的类关系,支持多态,解决代码复用。is-a 的类关系可以通过组合和接口的 has-a 的类关系替代;多态可以用接口代替;代码复用可以用组合和委托来实现。因此,我们完全可以使用组合、接口、委托的手段,来完全替代继承,特别是一些继承关系复杂的代码。

组合与继承该如何作出选择?

如果类之间的继承结构稳定(不会轻易改变,相对稳定),继承层次比较浅(超过2层就会变得复杂了),继承关系不复杂,我们就可以大胆地使用继承。反之,尽量使用组合、接口、委托来替代继承。

除此之外,还有一些设计模式会固定使用继承或者组合。比如,装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等都使用了组合关系,而模板模式(template pattern)使用了继承关系。

尽管有些人喊着“多用组合少用继承”口号,甚至“杜绝继承”。但是在我看来,继承并非一无是处,只要他的缺点能有效控制,他依然可以发挥它的优势。在适当的场合,合理地选择继承还是组合,要因地制宜。