组合与继承(组合优于继承)

什么是继承和组合

在面向对象编程(OOP)中,继承和组合是两种主要的代码重用和模块结构设计方法。它们各有特点和适用场景,让我们来详细了解它们各自的含义、特点和区别。

继承

继承是一种使一个类(称为子类或派生类)能够继承另一个类(称为父类或基类)的属性和方法的机制。这是一种建立类之间关系的方式,允许在子类中重用(即继承)父类的代码,同时还可以添加或修改现有的行为。继承支持代码重用,同时也可以用来实现类型的多态。

主要特点:

  • "是一个"关系:继承表达了一个类别关系。例如,如果"狗"类继承自"动物"类,那么可以说每个狗是一个动物。
  • 代码复用:子类自动拥有父类的所有属性和方法,除非它们被子类覆盖。
  • 多态:继承支持多态,这意味着子类的对象可以被视为父类的实例,这对实现接口和多态行为非常重要。

示例( Java

csharp 复制代码
public class Animal {
    public void eat() {
        System.out.println("This animal eats food.");
    }
}

public class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("Dog eats dog food.");
    }
}

组合

组合是将一个或多个对象作为当前对象的一部分来构建程序的方法。这允许当前对象获得另一个对象的功能,而不是通过继承来获得。

主要特点:

  • "有一个"关系:组合表达的是包含关系。例如,如果"汽车"类包含"引擎"类的对象,那么可以说每辆汽车都有一个引擎。
  • 灵活性和低耦合:通过将功能封装在其它对象中,类的功能可以在运行时动态改变,这提高了系统的灵活性和降低了耦合。
  • 易于修改和维护:组合使得系统更容易扩展和维护,因为它帮助避免了继承带来的层次过深和过度耦合的问题。

示例( Java

csharp 复制代码
public class Engine {
    public void start() {
        System.out.println("Engine starts.");
    }
}

public class Car {
    private Engine engine;

    public Car() {
        engine = new Engine(); // Car "has an" engine.
    }

    public void startCar() {
        engine.start();
    }
}

继承 vs 组合

虽然继承和组合都是复用代码的有效方式,但通常建议尽可能使用组合而不是继承。这是因为组合提供了更高的灵活性和更低的耦合。继承可能导致代码难以理解和维护,尤其是在大型或复杂的继承层次中。

在很多现代编程实践中,推荐使用组合来达到代码复用的目的,尤其是在面向对象设计中,这样可以保持代码的灵活性和可扩展性。

设我们需要在不同的类中使用日志记录功能。我们可以创建一个专门的日志类,然后在需要日志功能的类中将其作为一个属性来引入。

使用组合

首先,我们定义一个 Logger 类,负责日志记录:

typescript 复制代码
public class Logger {
    public void log(String message) {
        System.out.println("Log: " + message);
    }
}

然后,我们创建两个使用日志记录的类,一个用于数据库操作,另一个用于网络通信:

arduino 复制代码
public class Database {
    private Logger logger;

    public Database() {
        this.logger = new Logger();
    }

    public void save(String data) {
        // 在保存数据之前进行日志记录
        logger.log("Saving data: " + data);
        System.out.println("Data saved: " + data);
    }
}

public class Network {
    private Logger logger;

    public Network() {
        this.logger = new Logger();
    }

    public void fetch(String url) {
        // 在获取网络数据前进行日志记录
        logger.log("Fetching from URL: " + url);
        System.out.println("Data fetched from: " + url);
    }
}

最后,我们需要一个主类来运行这些示例:

java 复制代码
public class Main {public static void main(String[] args) {Database db = new Database();
        db.save("Example data");
Network network = new Network();
        network.fetch("http://example.com");
    }
}

在这个例子中,Logger 类被 DatabaseNetwork 类复用,每个类中都有一个 Logger 的实例。这样,Logger 类可以独立于其他类进行修改和维护,而其他类则可以通过组合方式复用 Logger 提供的日志记录功能。这种设计模式减少了代码重复,提高了模块的独立性,便于维护和扩展。

使用继承来实现日志记录功能也是可行的,但可能会遇到一些设计上的问题。接下来,我将通过Java代码示例进行说明,并与之前的组合方式进行对比。

使用继承

假设我们将日志记录功能放在一个基类中,然后通过继承这个基类来实现日志功能的复用。

首先,定义一个基类 Loggable,包含日志记录的方法:

typescript 复制代码
public class Loggable {
    public void log(String message) {
        System.out.println("Log: " + message);
    }
}

然后,定义两个子类 DatabaseNetwork,它们通过继承 Loggable 来获得日志记录功能:

scala 复制代码
public class Database extends Loggable {
    public void save(String data) {
        // 在保存数据之前进行日志记录
        log("Saving data: " + data);
        System.out.println("Data saved: " + data);
    }
}

public class Network extends Loggable {
    public void fetch(String url) {
        // 在获取网络数据前进行日志记录
        log("Fetching from URL: " + url);
        System.out.println("Data fetched from: " + url);
    }
}

主类和方法调用

java 复制代码
public class Main {public static void main(String[] args) {Database db = new Database();
        db.save("Example data");
Network network = new Network();
        network.fetch("http://example.com");
    }
}

使用继承的问题

  1. 继承层次限制:如果一个类已经继承了另一个类,那么它不能再继承 Loggable 类(Java不支持多重继承)。这限制了类的灵活性。
  2. 耦合过强:子类依赖于父类的实现细节。如果父类的实现改变,可能会影响所有继承自该类的子类。
  3. 代码的复用性降低:继承通常用于表示"是一个"关系,而不是"有一个"关系。在很多情况下,日志功能并不是对象的本质属性,而更多是一种附加功能。

与组合的对比

使用组合,每个类只包含它需要的组件(如日志记录器)。这种方法提高了代码的复用性和灵活性,同时降低了类之间的耦合。组合更符合开闭原则,即对扩展开放,对修改封闭。这意味着添加新功能或者改变现有功能更为简单,不需要修改现有的类结构。

结论

在软件设计中,推荐尽可能使用组合而非继承,以获得更高的灵活性和更低的耦合度。继承应当在表示明确的"是一个"关系时使用,而非仅仅为了复用代码。

问题

  1. 都说组合优于继承,但是如果没有继承怎么复用功能逻辑呢?

    1. 对象组合:直接在一个类中包含另一个类的实例作为字段,从而可以访问那个实例的公共接口。这种方式可以让你选择性地复用所需的功能,而不必继承整个类的实现。
    2. 策略模式:定义一系列算法或行为,将它们封装在独立的类中,并使它们可以互换。这样的设计使得算法可以独立于使用它们的客户端变化。
    3. 委托:类A可以包含一个对类B的引用,并且类A的方法可以委托给类B的实例来处理。这样,类A可以使用类B提供的功能,而无需通过继承获得。
    4. 服务组件:使用服务或组件,如数据库访问逻辑、网络服务等,这些可以被多个客户端或类复用,而无需继承。通常这些服务会通过接口提供,以确保灵活性和可替换性。
    5. 接口和抽象类:虽然使用接口和抽象类仍然涉及到继承的概念,但它们是为了定义约定和提供部分实现,而非提供完整的类实现。这样可以确保在不同的实现之间保持一致的接口。
    6. 使用这些方法,你可以根据具体情况选择最合适的方式来复用代码,而不必依赖于传统的继承机制。这样不仅可以减少代码之间的依赖,还可以增加代码的可维护性和可扩展性。
  2. 组合也就是把共用的逻辑 抽离到一个类中,然后做为需要该功能的类的属性中,那么这样做的好处是什么?

    1. 灵活性增强:通过组合,你可以在运行时动态地改变对象的行为,只需要替换掉对象内部的组件即可。
    2. 降低耦合度:避免了类与类之间的紧密耦合。在继承中,子类依赖于父类的实现细节,而组合仅依赖于组件的接口。
    3. 更容易理解和维护:每个类都专注于一个具体的任务,使得代码更加模块化,更易于理解和维护。
    4. 可复用性增强:可以更容易地在不同的环境或框架中重用组件,因为组件不依赖于特定的类结构。
相关推荐
it_czz4 分钟前
LangSmith vs LangFlow vs LangGraph Studio 可视化配置方案对比
后端
蓝色王者6 分钟前
springboot 2.6.13 整合flowable6.8.1
java·spring boot·后端
花哥码天下1 小时前
apifox登录后设置token到环境变量
java·后端
hashiqimiya2 小时前
springboot事务触发滚动与不滚蛋
java·spring boot·后端
TeamDev2 小时前
基于 Angular UI 的 C# 桌面应用
前端·后端·angular.js
PPPHUANG2 小时前
一次 CompletableFuture 误用,如何耗尽 IO 线程池并拖垮整个系统
java·后端·代码规范
用户8356290780513 小时前
用Python轻松管理Word页脚:批量处理与多节文档技巧
后端·python
想用offer打牌3 小时前
一站式了解Spring AI Alibaba的流式输出
java·人工智能·后端
秋说3 小时前
华为 DevKit 25.2.rc1 源码迁移分析使用教程(openEuler + ARM64)
后端
ServBay3 小时前
C# 成为 2025 年的编程语言,7个C#技巧助力开发效率
后端·c#·.net