Java 复用设计思想深度剖析:组合、继承与"多用组合少用继承"原则

原创2026-10-09 10:45:34456 阅读
文章标签:教程技术博客文档

Java 复用设计思想深度剖析:组合、继承与"多用组合少用继承"原则

本篇技术指南以 YCBlogs 仓库的 Java 类与对象学习体系为背景,系统讲解面向对象中的代码复用设计思想:从"组合(Composition)"与"继承(Inheritance)"两种复用方式的基本概念与完整代码案例入手,深入分析为何业界主张"多用组合少用继承"、组合相比继承究竟有哪些优势,并结合接口、委托(Delegation)与多态原理,给出"何时该用继承、何时该用组合"的可落地判断标准。读完本文,你将掌握一套完整的复用决策框架,能够识别深继承层次的坏味道,并熟练用"接口 + 组合 + 委托"重构出更灵活、更可维护的 Java 代码。

01 快速了解复用

1.1 什么是复用

对于 C 语言等面向过程语言来说,"复用"通常指的就是"复制代码"。任何语言都可以通过简单复制来达到代码复用的目的,但这样做效果并不好:复制意味着同样的逻辑存在多份拷贝,任何一处修改都要同步到所有副本,极易引入不一致的 Bug。

Java 围绕"类"(Class)来解决问题。我们可以直接使用别人构建或调试过的代码,而非创建新类、重新开始。也就是说,复用让开发者站在已有、经过验证的代码之上工作,而不必重复造轮子。

1.2 复用的核心思想:组合与继承

如何在不污染源代码的前提下使用现存代码,是需要技巧的。本章有两条核心路径:

  • 组合(Composition):第一种方式直截了当,在新类中创建现有类的对象,通过这种方式复用代码的功能,而非其形式。Java 中组合体现为在类的字段位置持有另一个对象的引用。
  • 继承(Inheritance):第二种方式更为微妙,创建现有类类型的新类。照字面理解:采用现有类的形式,又无需在编码时改动其代码。这种方式叫做继承,编译器会做大部分的工作(自动获得父类的字段与方法)。继承是面向对象编程(OOP)的重要基础之一。

关于继承的更多细节(继承格式、构造器初始化、单继承限制等),可参考仓库中的 继承思想详细分析 一文。

02 快速了解组合

2.1 什么是组合

组合是最简单直接的复用方式:你仅需要把对象的引用(object references)放置在一个新的类里,这就使用了组合。组合表达的是 has-a(有一个)关系——新类"拥有"被组合进来的对象。

2.2 组合的入门案例:SprinklerSystem

假设你需要一个对象,其中内置几个 String 对象、两个基本类型(primitives)属性字段,以及一个其他类的对象。对于非基本类型对象,将引用直接放置在新类中;对于基本类型属性字段则仅进行声明:

class WaterSource {
  private String s;
  WaterSource() {
    System.out.println("WaterSource()");
    s = "Constructed";
  }
  @Override
  public String toString() { return s; }
}

public class SprinklerSystem {
  private String valve1, valve2, valve3, valve4;
  private WaterSource source = new WaterSource();
  private int i;
  private float f;
  @Override
  public String toString() {
    return
      "valve1 = " + valve1 + " " +
      "valve2 = " + valve2 + " " +
      "valve3 = " + valve3 + " " +
      "valve4 = " + valve4 + "\n" +
      "i = " + i + " " + "f = " + f + " " +
      "source = " + source; // [1]
  }
  public static void main(String[] args) {
    SprinklerSystem sprinklers = new SprinklerSystem();
    System.out.println(sprinklers);
  }
}

这个案例里有几个关键点需要展开理解:

  • toString() 的特殊性:每个非基本类型对象都有一个 toString() 方法,在编译器需要字符串但手里只有对象的情况下会被自动调用。因此,在 [1] 处,编译器看到你试图"添加"一个 WaterSource 类型的对象,而字符串只能拼接另一个字符串,所以它先调用 toString() 将 source 转换成一个字符串,然后再拼接并传递给 System.out.println()。要让自定义类获得这种行为,只需编写一个 toString() 方法即可。
  • @Override 注解的作用:在 toString() 上使用 @Override 告诉编译器验证重写是否正确。它是可选的,但有助于在编译期发现拼写错误(或者更微妙的大小写字母输入错误),避免静默地新定义一个方法而非重写。
  • 默认初始化规则:类中的基本类型字段自动初始化为零(int 为 0、float 为 0.0),但对象引用被初始化为 null。如果尝试调用 null 引用的任何方法,会得到运行时异常(NullPointerException);方便的是,直接打印 null 引用却不会抛异常——这正是 toString() 中 source = "null" 的由来。

2.3 四种初始化引用方式:Bath 案例

编译器不会为每个引用自动创建一个默认对象,这是有意义的,因为在许多情况下这会导致不必要的开销。初始化引用一共有四种方式:

  1. 定义时初始化:当对象被定义时直接赋值,这意味着它总是在调用构造函数之前初始化。
  2. 构造函数中初始化:在该类的构造函数中完成。
  3. 延迟初始化:在实际使用对象之前才初始化。在对象创建开销大且不需要每次都创建对象的情况下,这种方式可以显著减少开销。
  4. 实例初始化:使用实例初始化块(instance initializer)完成。

下面用 Bath 案例把四种方式完整演示一遍:

class Soap {
  private String s;
  Soap() {
    System.out.println("Soap()");
    s = "Constructed";
  }
  @Override
  public String toString() { return s; }
}

public class Bath {
  private String // Initializing at point of definition:
    s1 = "Happy",
    s2 = "Happy",
    s3, s4;
  private Soap castille;
  private int i;
  private float toy;
  public Bath() {
    System.out.println("Inside Bath()");
    s3 = "Joy";
    toy = 3.14f;
    castille = new Soap();
  }
  // Instance initialization:
  { i = 47; }
  @Override
  public String toString() {
    if(s4 == null) // Delayed initialization:
      s4 = "Joy";
    return
      "s1 = " + s1 + "\n" +
      "s2 = " + s2 + "\n" +
      "s3 = " + s3 + "\n" +
      "s4 = " + s4 + "\n" +
      "i = " + i + "\n" +
      "toy = " + toy + "\n" +
      "castille = " + castille;
  }
  public static void main(String[] args) {
    Bath b = new Bath();
    System.out.println(b);
  }
}
/* Output:
Inside Bath()
Soap()
s1 = Happy
s2 = Happy
s3 = Joy
s4 = Joy
i = 47
toy = 3.14
castille = Constructed
*/

运行结果揭示了组合初始化的完整时序:

  • s1、s2 在定义处初始化(方式一),最先完成;
  • s3、toy、castille 在构造函数中初始化(方式二),其中 Soap 的构造在 Bath 构造函数内被调用,所以打印出 Soap();
  • i 通过实例初始化块赋值(方式四),注意实例初始化块在所有构造逻辑之前执行;
  • s4 属于延迟初始化(方式三),在 toString() 第一次被调用时才赋值。之所以在 toString() 里才初始化,是为了保证"在使用字段的时候所有的属性都已被初始化"。

需要注意:当你不在定义处初始化时,仍然不能保证在向对象引用发送消息之前执行任何初始化——如果你试图对未初始化的引用调用方法,未初始化的引用将产生运行时异常。

2.4 复杂的组合案例:PlaceSetting(组合 + 继承)

实际开发中,你经常会同时使用组合和继承。下面的例子展示了如何用继承创建一组组件类,再通过组合把它们装配进一个更大的类,并理清构造函数的初始化顺序:

class Plate {
  Plate(int i) {
    System.out.println("Plate constructor");
  }
}

class DinnerPlate extends Plate {
  DinnerPlate(int i) {
    super(i);
    System.out.println("DinnerPlate constructor");
  }
}

class Utensil {
  Utensil(int i) {
    System.out.println("Utensil constructor");
  }
}

class Spoon extends Utensil {
  Spoon(int i) {
    super(i);
    System.out.println("Spoon constructor");
  }
}

class Fork extends Utensil {
  Fork(int i) {
    super(i);
    System.out.println("Fork constructor");
  }
}

class Knife extends Utensil {
  Knife(int i) {
    super(i);
    System.out.println("Knife constructor");
  }
}

// A cultural way of doing something:
class Custom {
  Custom(int i) {
    System.out.println("Custom constructor");
  }
}

public class PlaceSetting extends Custom {
  private Spoon sp;
  private Fork frk;
  private Knife kn;
  private DinnerPlate pl;
  public PlaceSetting(int i) {
    super(i + 1);
    sp = new Spoon(i + 2);
    frk = new Fork(i + 3);
    kn = new Knife(i + 4);
    pl = new DinnerPlate(i + 5);
    System.out.println("PlaceSetting constructor");
  }
  public static void main(String[] args) {
    PlaceSetting x = new PlaceSetting(9);
  }
}
/* Output:
Custom constructor
Utensil constructor
Spoon constructor
Utensil constructor
Fork constructor
Utensil constructor
Knife constructor
Plate constructor
DinnerPlate constructor
PlaceSetting constructor
*/

这个案例展示了两个"强制"与一个"不强制":

  • 编译器强制你初始化基类:PlaceSetting 构造函数第一行必须通过 super(i + 1) 调用基类 Custom 的带参构造,否则编译报错;对基类构造函数的调用必须是派生类构造函数中的第一个操作。
  • 编译器强制从基类向外初始化:构造从最顶层基类 Custom 开始,逐层向下,每个成员对象(Spoon、Fork、Knife、DinnerPlate)也遵循"先初始化自己的基类(Utensil、Plate),再执行自己的构造体"的规则。
  • 编译器不监视成员对象的初始化:它并不确保你初始化了所有成员对象——成员对象默认是 null,忘记 new 只会到运行期才暴露问题。

关于继承初始化顺序的更完整验证(成员变量先于构造函数、从最顶层父类逐级向下),可参考 继承思想详细分析 中的 Animal → Dog → Huskie 三级日志案例。

另外注意,这个案例中类被干净地分离:你甚至不需要方法复用代码的源代码,最多只导入一个包——这一点对继承和组合都成立。

03 深入理解继承:复用与委托

3.1 继承的格式与意义

继承用来表示类之间的 is-a 关系(猫是一种哺乳动物),Java 使用 extends 关键字实现:class 子类名 extends 父类名 {}。单独的这个类称为父类、基类或超类;多个子类称为子类或派生类。

继承最大的好处是代码复用:把多个类的相同属性和方法抽取到父类,子类自动获得这些成员。同时继承也是多态的前提——多态要求存在继承关系的子类和父类、子类重写父类方法、父类引用指向子类对象(向上转型)。基于继承的多态与基于接口的多态的具体区别,可参见仓库中的 多态思想详细分析。

3.2 委托:介于继承与组合之间的第三种复用关系

Java 不直接支持的第三种重用关系叫做委托(Delegation),它介于继承和组合之间:你将一个成员对象放在正在构建的类中(像组合),同时又在新类中公开来自成员对象的所有方法(像继承)。

以宇宙飞船的控制模块为例。先定义一个控制模块类:

public class SpaceShipControls {
  void up(int velocity) {}
  void down(int velocity) {}
  void left(int velocity) {}
  void right(int velocity) {}
  void forward(int velocity) {}
  void back(int velocity) {}
  void turboBoost() {}
}

用继承来建造宇宙飞船虽然可行,但语义是错误的——DerivedSpaceShip 并不是真正"一种" SpaceShipControls:

public class DerivedSpaceShip extends SpaceShipControls {
  private String name;
  public DerivedSpaceShip(String name) {
    this.name = name;
  }
  @Override
  public String toString() { return name; }
  public static void main(String[] args) {
    DerivedSpaceShip protector = new DerivedSpaceShip("NSEA Protector");
    protector.forward(100);
  }
}

更准确的说法是:一艘宇宙飞船包含了控制模块,同时控制模块中的所有方法都暴露在宇宙飞船上。委托正好解决这个难题:

public class SpaceShipDelegation {
  private String name;
  private SpaceShipControls controls = new SpaceShipControls();
  public SpaceShipDelegation(String name) {
    this.name = name;
  }
  // Delegated methods:
  public void back(int velocity) { controls.back(velocity); }
  public void down(int velocity) { controls.down(velocity); }
  public void forward(int velocity) { controls.forward(velocity); }
  public void left(int velocity) { controls.left(velocity); }
  public void right(int velocity) { controls.right(velocity); }
  public void turboBoost() { controls.turboBoost(); }
  public void up(int velocity) { controls.up(velocity); }
  public static void main(String[] args) {
    SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector");
    protector.forward(100);
  }
}

方法被转发到底层的 control 对象,对外接口与继承得到的接口完全相同。但委托的优势在于可控:你可以只暴露成员对象方法的一个子集,而不是像继承那样全盘接收,这对封装性更友好。委托是后面"组合 + 接口 + 委托替代继承"方案的核心机制之一。

04 结合组合与继承:经典设计原则

4.1 经典设计原则:组合优于继承

在面向对象编程中,有一条非常经典的设计原则:组合优于继承,多用组合少用继承。随之而来的三个核心追问是:为什么不推荐使用继承?组合相比继承有哪些优势?如何判断该用组合还是继承?

4.2 为何不推荐使用继承

继承是面向对象的四大特性之一,用来表示类之间的 is-a 关系,可以解决代码复用问题。但继承层次过深、过复杂,也会影响代码的可维护性,所以网上很多人认为继承是一种反模式,应该尽量少用甚至不用。

用一个经典的"鸟类"设计案例来说明。假设要把"鸟类"定义为抽象类 AbstractBird,麻雀、鸽子、乌鸦等都继承它。那么问题来了:大部分鸟都会飞,能不能在 AbstractBird 中直接定义 fly() 方法?

答案是不能。因为鸵鸟不会飞——鸵鸟继承带 fly() 的父类,就具有了"飞"的行为,这不符合现实认知。有人会说:在子类中重写 fly() 抛出异常不就行了吗?

public class AbstractBird {
  //...省略其他属性和方法...
  public void fly() { //... }
}

public class Ostrich extends AbstractBird { //鸵鸟
  //...省略其他属性和方法...
  public void fly() {
    throw new UnSupportedMethodException("I can't fly.'");
  }
}

再看一个更贴近工程实践的代码案例——把"会不会飞、会不会叫、会不会游泳"三个行为维度硬塞进一个抽象类:

//多组合,少继承
//比如。会不会飞,会不会叫,会不会游泳,这样定义抽象类会比较多。有的可以不用
public abstract class AbstractBird {
    //...省略其他属性和方法...
    public void fly() { //... }
    public void eat(){ }
    public void call(){ }
    public void swimming(){ }
}

public class BirdA extends AbstractBird {
    //A,不会飞,会叫,会游泳
    public void fly() { throw new RuntimeException("I can't fly.'"); }
    @Override public void call() { super.call(); }
    @Override public void swimming() { super.swimming(); }
}

public class BirdB extends AbstractBird {
    //B,不会飞,不会叫,会游泳
    public void fly() { throw new RuntimeException("I can't fly.'"); }
    @Override public void call() { throw new RuntimeException("I can't call.'"); }
    @Override public void swimming() { super.swimming(); }
}

public class BirdC extends AbstractBird {
    //C,会飞,不会叫,不会游泳
    public void fly() { }
    @Override public void call() { super.call(); }
    @Override public void swimming() { super.swimming(); }
}

这种设计思路虽然能解决问题,但不够优美,至少存在两处硬伤:

  • 除了鸵鸟,不会飞的鸟还有企鹅等一大类,每个都要重写 fly() 抛出异常,徒增编码工作量;
  • 它违背了最小知识原则(Least Knowledge Principle,也叫最少知识原则或迪米特法则):父类暴露了不该暴露的接口给外部,增加了类被误用的概率。

如果继续沿着继承的思路"修补",派生 AbstractFlyableBird 和 AbstractUnFlyableBird 两个子抽象类,继承层次就变成了三层。再叠加第二个行为维度"会不会叫",两两组合产生四种情况,就需要四个抽象类(AbstractFlyableTweetableBird、AbstractFlyableUnTweetableBird、AbstractUnFlyableTweetableBird、AbstractUnFlyableUnTweetableBird);如果再考虑"是否会下蛋"这个行为,那就要组合爆炸了。

深层问题随之浮现:类的继承层次会越来越深、继承关系越来越复杂。这种设计一方面导致代码可读性变差——要搞清楚某个类有哪些方法、属性,必须一路追溯父类、父类的父类直到最顶层;另一方面破坏了封装特性——父类实现细节暴露给子类,子类实现依赖父类实现,两者高度耦合,父类代码一旦修改,就会影响所有子类逻辑。

继承最大的问题:继承层次过深、继承关系过于复杂,会影响代码的可读性和可维护性,这正是不推荐使用继承的根本原因。

4.3 组合的优势:接口 + 组合 + 委托

实际上,可以利用**组合(composition)、接口、委托(delegation)**三个技术手段一起解决继承存在的问题。

接口表示具有某种行为特性。针对"会飞",定义 Flyable 接口,只让会飞的鸟实现它;类似地定义 Tweetable(会叫)和 EggLayable(会下蛋)接口:

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() { //... }
}

但接口只声明方法、不定义实现,每个会下蛋的鸟都要重复实现一遍逻辑相同的 layEgg(),导致代码重复。解决方案是:针对每个接口再定义实现类,然后用组合 + 委托消除重复:

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 关系、支持多态特性、代码复用——都有替代方案:

继承的作用 替代技术手段
is-a 关系 组合 + 接口的 has-a 关系
多态特性 接口(多态基于接口的实现)
代码复用 组合 + 委托

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

这里对抽象类与接口的选择做一点补充:如果表示 is-a 关系且为了解决代码复用问题,用抽象类;如果表示 has-a 关系且为了解决抽象(解耦)而非代码复用问题,用接口。抽象类是自下而上(先有子类重复,再抽象出父类)的设计思路,接口是自上而下(先定义契约,再考虑实现)的设计思路。更详细的对比见仓库中的 抽象类和接口分析。

05 组合与继承的选择

5.1 如何选择

尽管鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。把继承改写成组合意味着更细粒度的类拆分——要定义更多的类和接口,这或多或少会增加代码的复杂程度和维护成本。所以在实际项目中,还是要根据具体情况来选择。

判断标准可以归纳为两条:

  • 继承结构稳定(不会轻易改变)、层次浅(最多两层继承关系)、关系不复杂 → 可以大胆使用继承。
  • 系统不稳定、继承层次深、继承关系复杂 → 尽量使用组合替代继承。

此外,仅仅为了代码复用而强行抽象父子关系也是反模式。比如 Crawler 类和 PageAnalyzer 类都用到了 URL 拼接和分割功能,但两者既不是父子关系也不是兄弟关系。此时生硬地抽一个 Url 父类出来,会让不熟悉设计思路的同事觉得"莫名其妙";用组合则更合理、更灵活:

public class Url {
  //...省略属性和方法
}

public class Crawler {
  private Url url; // 组合
  public Crawler() {
    this.url = new Url();
  }
  //...
}

public class PageAnalyzer {
  private Url url; // 组合
  public PageAnalyzer() {
    this.url = new Url();
  }
  //..
}

5.2 案例分析:Car(has-a 与 is-a 的判断)

组合和继承都允许在新类中放置子对象(组合是显式的,继承是隐式的)。判断依据很朴素:当你想在新类中包含一个已有类的功能时,使用组合而非继承——在新类中嵌入一个对象(通常是私有的)以实现其功能,使用者看到的是新类定义的接口,而非嵌入对象的接口。

经典的 Car 案例:

class Engine {
    public void start() {}
    public void rev() {}
    public void stop() {}
}

class Wheel {
    public void inflate(int psi) {}
}

class Window {
    public void rollup() {}
    public void rolldown() {}
}

class Door {
    public Window window = new Window();
    public void open() {}
    public void close() {}
}

public class Car {
    public Engine engine = new Engine();
    public Wheel[] wheel = new Wheel[4];
    public Door left = new Door(), right = new Door(); // 2-door

    public Car() {
        for (int i = 0; i < 4; i++) {
            wheel[i] = new Wheel();
        }
    }

    public static void main(String[] args) {
        Car car = new Car();
        car.left.window.rollup();
        car.wheel[0].inflate(72);
    }
}

这个例子中 Car 的组合本身就是问题分析的一部分(不是底层设计的一部分),所以成员声明为 public 有助于客户端程序员理解如何使用类,也降低了类创建者面临的复杂度。但请记住这是一个特例——通常属性还是应该声明为 private。

用继承时,则是使用一个现有类并开发出它的新版本,通常意味着把通用类特殊化。稍微思考就会发现:用一个交通工具对象来组成一部车毫无意义——车不包含交通工具,它就是交通工具。"是一个"关系用继承表达,"有一个"关系用组合表达。这正是本节判断标准的最好注脚。

5.3 设计模式与组合/继承的固定搭配

除了稳定性与层次深浅之外,还有一些设计模式会固定使用继承或组合:

  • 使用组合关系的模式:装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等。
  • 使用继承关系的模式:模板模式(template pattern)。

以装饰者模式为例,它"以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案",核心是持有构件对象的实例并委派调用(private Component component; 再加接口一致的转发方法),这正是组合 + 委托的典型结构。Java IO 标准库的 InputStream → FilterInputStream → BufferedInputStream/DataInputStream 就是装饰者模式最著名的应用,具体分析见 装饰者设计模式介绍。

策略模式同样体现组合思想:环境类(Context)持有抽象策略的引用,通过构造函数注入具体策略对象,运行时利用多态动态调用——"针对一组算法,将每一个算法封装到具有共同接口的独立类中,从而使得它们可以相互替换",见 策略者设计模式。

5.4 必须使用继承的特殊场景

还有一个场景必须使用继承:不能改变函数的入参类型,而入参又非接口,为了支持多态只能采用继承。比如 FeignClient 是外部框架类,我们没有权限修改它的代码,但希望重写它运行时执行的 encode() 方法:

public class FeignClient { // feign client框架代码
  //...省略其他代码...
  public void encode(String url) { //... }
}

public void demofunction(FeignClient feignClient) {
  //...
  feignClient.encode(url);
  //...
}

public class CustomizedFeignClient extends FeignClient {
  @Override
  public void encode(String url) { //...重写encode的实现...}
}

// 调用
FeignClient client = new CustomizedFeignClient();
demofunction(client);

这种"外部类 + 非接口入参 + 需要重写行为"的组合,只能通过继承来达成多态替换。

尽管有人主张杜绝继承、100% 用组合替代继承,但这里的观点没那么极端。"多用组合少用继承"的口号之所以喊得响,只是因为长期以来存在过度使用继承的问题。组合并不完美,继承也不是一无是处——只要控制好各自的副作用、发挥各自的优势,在不同的场合下恰当地选择继承还是组合,这才是我们所追求的境界。

06 总结一下笔记

  1. 为什么不推荐使用继承? 继承是面向对象的四大特性之一,表示类之间的 is-a 关系,可以解决代码复用问题。但继承层次过深、过复杂会严重影响代码的可维护性:一方面阅读成本高(要追溯整条父类链),另一方面破坏封装、子类与父类高度耦合。这种情况下应该尽量少用甚至不用继承。

  2. 组合相比继承有哪些优势? 继承的三个作用(is-a 关系、多态特性、代码复用)都可以通过组合、接口、委托三个技术手段达成;除此之外,组合还能解决层次过深、过复杂的继承关系影响代码可维护性的问题。

  3. 如何判断该用组合还是继承? 组合并不完美、继承并非一无是处,应根据具体情况选择:继承结构稳定、层次浅、关系不复杂,可以大胆使用继承;反之尽量用组合替代。同时,装饰者模式、策略模式、组合模式固定使用组合,模板模式固定使用继承;而"外部类 + 非接口入参 + 需要重写行为"的场景则必须使用继承。

相关延伸阅读:本文是 YCBlogs 仓库 Java 面向对象体系中的一节,配套资料包括 继承思想详细分析、多态思想详细分析、抽象类和接口分析,设计原则侧可参考 面向对象六大原则 与 多用组合和少继承 两篇姊妹文章。

登录后查看全文
YCBlogs