高级工程师为何说要多用组合少用继承?在工作中,假如你工作了,或者你参加过代码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)
三个技术手段,一块儿来解决刚刚继承存在的问题。
- 针对"会飞"这样一个行为特性,我们可以定义一个 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() { //... }
但是这仅仅是解决了单继承的问题,套用了接口可以多实现的特性
。
- 每个会下蛋的鸟都要实现一遍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的能力。但是这也降低了代码的整洁。所以说,使用组合也是很考验技术的学问。
使用继承 : 如果类之间的继承结构稳定
(不会轻易改变),层次比较浅
(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。
使用组合 :系统越不稳定
,继承层次深
,继承关系复杂
。
除此之外,还有一些设计模式、特殊的应用场景,会固定使用继承或者组合。