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

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

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

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

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

为何说要多用组合少用继承?如何决定该用组合还是继承?_第1张图片

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

为何说要多用组合少用继承?如何决定该用组合还是继承?_第2张图片

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

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的能力。但是这也降低了代码的整洁。所以说,使用组合也是很考验技术的学问。

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

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

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

你可能感兴趣的:(java,前端,服务器)