05 为何说要多用组合少用继承?如何决定该用组合还是继承?

高级工程师为何说要多用组合少用继承?在工作中,假如你工作了,或者你参加过代码review,那你应该听到过这句话。我以前只是听说过,但是没有使用过,哈哈哈哈哈,今天学习下,以后少被骂。

看过前面的文章,到这里用我通俗的感觉就是代码设计演进的一个过程,抽象类,接口比继承的优点。这一章节是组合使用对于继承的优点。组合优于继承,多用组合少用继承。为什么不推荐使用继承?组合相比继承有哪些优势?如何判断该用组合还是继承?今天,我们就围绕着这三个问题,来详细讲解一下这条设计原则。

1. 为什么不推荐使用继承?

前面在讲继承和抽象类的以及接口的区别时,已经讲过继承的缺点,继承主要是表示is-a关系,主要是解决代码复用问题,但是假如继承层级过深,那么就会造成代码复杂且冗余,很难维护。

目前的继承关系还比较简单,层次比较浅,也算是一种可以接受的设计思路。我们再继续加点难度。目前场景中,我们只关注"鸟会不会飞",但如果我们还关注"鸟会不会叫",那这个时候,我们又该如何设计类之间的继承关系呢?

这种情况再来几个,那真的是头疼,假如你要修改一个功能,你需要先去改不会飞的鸟类,再去修改会飞的鸟类,照这样下去,你会因为这种低级且重复的工作而烦累,技术上也止步不前。

2. 什么是组合?

面向对象编程中,组合(Composition)是一种关系,表示一个类包含另一个类的对象,而这种关系通常表达了 "有一个" 的关联。这是一种对象关联的形式,与继承相比,组合更注重 "包含" 关系而不是 "是一个" 关系。

js 复制代码
// 定义引擎类
class Engine {
    void start() {
        System.out.println("Engine starting...");
    }
}

// 定义汽车类,通过组合引擎实现
class Car {
    private Engine engine;  // 使用组合关系,Car 包含一个 Engine 对象

    // 构造函数,通过组合关系初始化 Engine 对象
    Car() {
        this.engine = new Engine();
    }

    // 启动汽车,委托引擎启动
    void start() {
        engine.start();
        System.out.println("Car starting...");
    }
}

在上述示例中,Car 类通过组合关系包含了一个 Engine 对象。这意味着 Car 不是继承自 Engine,而是在其内部包含了一个 Engine 对象。通过这种方式,Car 类可以重用 Engine 类的功能,同时保持了更松散的耦合。

组合的优势

  • 灵活性: 可以轻松更换或升级组成对象,而不影响包含它们的类。
  • 复用: 通过组合可以重用其他类的功能,而不必继承整个类的实现。
  • 简化: 降低了系统的复杂性,因为组合通常比继承更简单。

3. 组合相比继承有哪些优势?

困难总比办法多,所以优秀的程序员们想尽各种方法来制造困难,再解决困难。组合(composition)、接口、委托(delegation)三个技术手段,一块儿来解决刚刚继承存在的问题。

  1. 针对"会飞"这样一个行为特性,我们可以定义一个 Flyable 接口,只让会飞的鸟去实现这个接口。对于会叫、会下蛋这些行为特性,我们可以类似地定义 Tweetable 接口、EggLayable 接口。
js 复制代码
public interface Flyable {
    void fly();
}

public interface Tweetable {
    void tweet();
}

public interface EggLayable {
    void layEgg();
}

public class Ostrich implements Tweetable, EggLayable {// 鸵鸟
    //... 省略其他属性和方法...
    
    @Override
    public void tweet() { //... }
    
    @Override
    public void layEgg() { //... }
}
public class Sparrow impelents Flayable, Tweetable, EggLayable {// 麻雀
    //... 省略其他属性和方法...
    @Override
    public void fly() { //... }
    
    @Override
    public void tweet() { //... }
    
    @Override
    public void layEgg() { //... }

但是这仅仅是解决了单继承的问题,套用了接口可以多实现的特性

  1. 每个会下蛋的鸟都要实现一遍layEgg() 方法,并且实现逻辑是一样的,这就会导致代码重复的问题。那这个问题又该如何解决呢?

针对三个接口再定义三个实现类,它们分别是:实现了 fly() 方法的 FlyAbility类、实现了 tweet() 方法的 TweetAbility 类、实现了 layEgg() 方法的 EggLayAbility 类。然后,通过组合委托技术来消除代码重复。具体的代码实现如下所示:

js 复制代码
public interface Flyable {

    void fly();
    }
    
public class FlyAbility implements Flyable {
    @Override
    public void fly() { //... }
}

// 省略 Tweetable/TweetAbility/EggLayable/EggLayAbility
public class Ostrich implements Tweetable, EggLayable {// 鸵鸟

    private TweetAbility tweetAbility = new TweetAbility(); // 组合
    
    private EggLayAbility eggLayAbility = new EggLayAbility(); // 组合
    
    //... 省略其他属性和方法...

    @Override
    public void tweet() {
        tweetAbility.tweet(); // 委托
    }

    @Override
    public void layEgg() {
        eggLayAbility.layEgg(); // 委托
    }
}

通过上面的演变过程,我们可以发现:

  • is-a 关系,我们可以通过组合和接口的 has-a 关系来替代;
  • 多态特性我们可以利用接口来实现;
  • 代码复用我们可以通过组合和委托来实现。

从理论上讲,通过组合、接口、委托三个技术手段,我们完全可以替换掉继承,在项目中不用或者少用继承关系,特别是一些复杂的继承关系。

4. 如何判断该用组合还是继承?

组合是通过定义更多的类和接口,来细粒度的动态嫁接了需要继承获取到的Feature,比如上面的Car的例子,如果使用继承,那你只能让car继承Engine类,但是通过在Car类里动态一个Engine对象,从而获取到Engine的能力。但是这也降低了代码的整洁。所以说,使用组合也是很考验技术的学问。

使用继承 : 如果类之间的继承结构稳定(不会轻易改变),层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。

使用组合 :系统越不稳定,继承层次深,继承关系复杂

除此之外,还有一些设计模式、特殊的应用场景,会固定使用继承或者组合。

相关推荐
一个不秃头的 程序员10 分钟前
代码加入SFTP JAVA ---(小白篇3)
java·python·github
丁总学Java22 分钟前
--spring.profiles.active=prod
java·spring
上等猿29 分钟前
集合stream
java
java1234_小锋33 分钟前
MyBatis如何处理延迟加载?
java·开发语言
菠萝咕噜肉i34 分钟前
MyBatis是什么?为什么有全自动ORM框架还是MyBatis比较受欢迎?
java·mybatis·框架·半自动
林的快手1 小时前
209.长度最小的子数组
java·数据结构·数据库·python·算法·leetcode
zh路西法1 小时前
【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(二):从FSM开始的2D游戏角色操控底层源码编写
c++·游戏·unity·设计模式·状态模式
向阳12181 小时前
mybatis 缓存
java·缓存·mybatis
上等猿1 小时前
函数式编程&Lambda表达式
java
夏旭泽2 小时前
设计模式-备忘录模式
设计模式·备忘录模式