高级工程师为何说要多用组合少用继承?在工作中,假如你工作了,或者你参加过代码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)
三个技术手段,一块儿来解决刚刚继承存在的问题。
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() { //... }
但是这仅仅是解决了单继承的问题,套用了接口可以多实现的特性
。
针对三个接口再定义三个实现类,它们分别是:实现了 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(); // 委托 } }
通过上面的演变过程,我们可以发现:
从理论上讲,通过组合、接口、委托三个技术手段,我们完全可以替换掉继承,在项目中不用或者少用继承关系,特别是一些复杂的继承关系。
4. 如何判断该用组合还是继承?
组合是通过定义更多的类和接口,来细粒度的动态嫁接了需要继承获取到的Feature,比如上面的Car
的例子,如果使用继承,那你只能让car继承Engine类,但是通过在Car类
里动态一个Engine
对象,从而获取到Engine的能力。但是这也降低了代码的整洁。所以说,使用组合也是很考验技术的学问。
使用继承: 如果类之间的继承结构稳定
(不会轻易改变),层次比较浅
(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。
使用组合:系统越不稳定
,继承层次深
,继承关系复杂
。
除此之外,还有一些设计模式、特殊的应用场景,会固定使用继承或者组合。