掘金 后端 ( ) • 2024-04-12 13:57

什么是继承和组合

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

继承

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

主要特点:

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

示例( Java

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

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 类,负责日志记录:

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

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

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);
    }
}

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

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,包含日志记录的方法:

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

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

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);
    }
}

主类和方法调用

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. 可复用性增强:可以更容易地在不同的环境或框架中重用组件,因为组件不依赖于特定的类结构。