【编程思想】为什么rust和go语言都拥抱组合而舍弃了继承?
转载请注明:
作者:TodoCoder
出处: https://www.todocoder.com/posts/007.html
公众号转载微信搜: TodoCoder
大家好,我是Coder哥,今天我们来聊一下继承和组合。
昨天刷到一个问题,有人问什么原因让rust和go等新型语言拥抱组合舍弃继承?
仔细一想确实是,之前一直在用Java,最近才开始用Go语言,在用Java的时候有一个准则是,如果没充分的理由不要用继承
。但是在开发的时候也一直很随意,习惯性的撸继承,但是Go就很不一样了,没得选,_!!,只能用组合。
那么现代语言为啥提倡用组合呢?其实不只是新的语言,包括Java这个老大哥,在**《Effective Java》的 第16条**中有这样一句话:复合优先于继承, 继承是实现代码重用的的有力手段,但它未必是最好的方法。
所以我们是该仔细的思考一下。我们不讨论具体语言,只谈思想,我觉得可以从以下几个方向来考虑一下:
- 继承与组合的特点。
- 在实际应用中继承会带来什么问题?
- 组合是怎么解决这个问题的?
让我们结合一个实际应用,通过抛弃继承,拥抱接口、拥抱组合的方式来一步步优化。相信看完这个文章你会对继承和组合有一个新的认识。
继承与组合的特点
这里就不说继承和组合的定义了,继承和组合是面向对象编程中两种常见的代码重用方式。它们都可以实现代码的复用,但是他们有各自的优缺点。这里先总体说一下。
继承:
优点:
- 它可以实现代码的重用,从父类继承的属性和方法可以在子类中直接使用。
- 继承链的扩展。通过继承可以构建继承链,使得子类可以继承祖先类的所有属性和方法,从而提高代码的可扩展性和可维护性。
- 继承和组合都可以实现多态,即同一个方法在不同的子类中表现出不同的行为。
缺点:
- 父类的改变会影响子类。如果父类的实现发生变化,所有继承自该父类的子类都需要相应地进行修改,这会增加代码的维护成本。
- 继承关系的耦合度高。子类和父类之间是紧密耦合的关系,这会影响代码的灵活性和可移植性。
那么组合呢,组合相对于继承有如下特点:
组合:
优点:
- 组合可以减少代码的耦合性,因为对象之间的关系是松散的,修改一个对象不会影响到其他对象。
- 组合可以实现更灵活的代码设计,因为可以根据需要组合不同的对象。
- 接口隔离。组合可以实现接口隔离,将不同的功能模块分别实现,提高代码的可复用性。
缺点:
- 代码量增加。相比于继承,组合需要增加更多的代码来实现不同的模块组合。
- 对象之间的交互复杂。组合关系下,对象之间的交互有时需要复杂的接口定义和实现,增加了代码的复杂度。
在实际应用中继承会带来什么问题以及我们怎么优化它?
1、初步问题
比如说我们要设计一个关于车的类。按照面向对象编程的思想,我们将“车类”这样一个事务抽象成一个BaseCar类,默认有run的行为。那么所有车类都可以继承这个抽象类。比如,汽车,卡车等。
public class BaseCar {
//... 省略其他属性和方法...
public void run() { //... }
}
// 汽车
public class Car extends AbstractCar {
}
但是,基于对车
这个对象的理解和需求,车出了跑,还可以修轮胎,可以修引擎等。那么AbstractCar
就变成如下的类,那么这个时候有个自行车
的类需要实现,自行车不能修引擎,要怎么写呢
public class BaseCar {
//... 省略其他属性和方法...
public void run() { //跑... }
public void repaireTire() { //修轮胎... }
public void repaireEngine() { //修引擎... }
}
// 自行车
public class Bicycle extends BaseCar {
//... 省略其他属性和方法...
public void repaireEngine() {
throw new UnSupportedMethodException("我没有引擎!");
}
}
按照这个逻辑,上面的代码看似解决了问题,实则可能会堆成屎山,上面的设计有三个点有很大隐患:
第一个是,如果我们把基类的行为实现都放到基类里面,比如说,如果后面增加了自动驾驶功能
,全景功能
,带天窗功能
,那是不是都要堆到基类里面了,虽然能提高复用性,但是也会改变所有子类的功能,这也会导致代码的复杂性提升,这点是我们并不想看到的。
第二个点是,对于没有没有那些功能的对象,比如自行车,就不应该把修引擎的功能暴露到自行车类里面。
第三个点是,如果扩展到其他对象怎么办,比如说人也会跑,飞机也会跑。那么这个设计后面就不好扩展了,也不够灵活。
那么对于上面的问题我们要怎么解决呢。你是不是想到了接口,对,接口更多的是行为的定义,抽象类更多的是定义的某一类类型的基础通用行为的实现。其实抽象类的加入也提高了代码的复杂度。
2、接口优化
对于以上问题,我们无视具体对象,只看行为,比如,跑,修引擎,修轮胎,这些功能行为,我们可以定义成接口:IRun
,IEngine
,ITire
,这三个接口:
public interface IRun {
void run();
}
public interface IEngine {
void repaireEngine();
}
public interface ITire {
void repaireTire();
}
那么我们实现汽车这个类的时候可以,实现IRun、IEngine、ITire
这三个接口,我们实现自行车类的时候可以实现IRun
这一个接口,那么如果想写个人类的对象的时候,也只需要实现IRun
这个接口就可以了。
public class Car implements IRun, IEngine, ITire {//汽车
//... 省略其他属性和方法...
@Override
public void run() { //跑... }
@Override
public void repaireEngine() { //修引擎... }
@Override
public void repaireTire() { //修轮胎... }
}
public class Bicycle impelents IRun, ITire{//自行车
//... 省略其他属性和方法...
@Override
public void run() { //跑... }
@Override
public void repaireTire() { //修轮胎... }
}
public class Person impelents IRun {//人
//... 省略其他属性和方法...
@Override
public void run() { //跑... }
}
这样是不是灵活性就更好了,到这是不是理解了为啥Go、Rust等现代语言,踢出了继承,踢出了抽象类,保留了接口实现的原因了吧。
但是,只看上面代码好像还有问题,那每个对象都要写一遍run,repaireEngine,repaireTire
等功能,这样岂不是很麻烦,说好的复用呢???别急,组合该登场了。
3、组合优化
对于上面的问题,我们可以通过先实现接口,然后通过组合、委托的方式来解决。代码如下:
public class CarRunEnable implements IRun {
@Override
public void run() { // 车跑... }
}
public class PersonRunEnable implements IRun {
@Override
public void run() { // 人跑... }
}
//省略其他实现 EngineEnable/TireEnable
public class Car implements IRun, IEngine, ITire {//汽车
private CarRunEnable runEnable = new CarRunEnable(); //组合
private EngineEnable engineEnable = new EngineEnable(); //组合
private TireEnable tireEnable = new TireEnable(); //组合
//... 省略其他属性和方法...
@Override
public void run() { //跑...
runEnable.run();
}
@Override
public void repaireEngine() { //修引擎...
engineEnable.repaireEngine();
}
@Override
public void repaireTire() { //修轮胎...
tireEnable.repaireTire();
}
}
public class Bicycle impelents IRun {//自行车
private CarRunEnable runEnable = new CarRunEnable(); //组合
private TireEnable tireEnable = new TireEnable(); //组合
//... 省略其他属性和方法...
@Override
public void run() { //跑...
runEnable.run();
}
@Override
public void repaireTire() { //修轮胎...
tireEnable.repaireTire();
}
}
public class Person impelents IRun {//人
private PersonRunEnable runEnable = new PersonRunEnable(); //组合
//... 省略其他属性和方法...
@Override
public void run() { //跑...
runEnable.run();
}
}
看上面的代码逻辑是不是就很爽快,加功能,随便加,不影响其他的类,耦合度降低了很多,但是内聚性也不必继承差,这就是所谓的高内聚低耦合。唯一的缺点是,代码量变多了。
那么我们回到最开始的那个问题,什么原因让rust和go等新型语言拥抱组合舍弃继承?
,相信你心中已经有了答案。
到最后了,感谢各位能看到这里
参考书籍:
《Effective Java》: https://www.todocoder.com/pdf/java/002003.html
《Java编程思想》:https://www.todocoder.com/pdf/java/002002.html