组合大于继承(Composition over Inheritance)?这是一个问题。
继承是一个纵向的扩展,组合是一个横向的扩展。
继承是从代码复用的角度来说的。但是继承并不直接等同于代码复用,如果一开始只是出于代码复用的目的而不区分类别和场景,就采用继承是不恰当的。
代码复用还可以使用组合的方式,比如前端的React框架有着一套极为强大的组合机制,官方也强烈建议使用组合的方式来应用React。
就大部分开发任务来说,其实继承出现的场景不多,主要还是代码复用的场景比较多,然而如果通过组合去进行代码复用又会显得比较麻烦。
从维护成本来看,组合其实会更好——因为不恰当地使用继承会导致高耦合:从而破坏封装,子类依赖于父类的实现,子类缺乏独立性。如果有人不小心改动了父类,就会牵一发而动全身,所有涉及到的子类的功能都有可能产生问题。
同时继承还会带来并不需要的属性或者行为方法。怎么办?要么覆写、要么换一个父类继承(这个方式貌似会导致出现其他的并不需要的属性或者行为方法ORZ...)。
覆写同样会导致问题,比如A是B的父类,他有一个属性嘴巴,这个嘴巴有一个说话和吃饭的功能,B继承了它,但是B不需要会说话,它吃饭就行了——于是它覆写了——这将可能导致其他继承自A的子类都变成哑巴。
一般如果硬要使用继承来使用的话,更通用的方法是,写一个基类,重写方法,然后更改一堆继承关系,还是很麻烦啊。
所以继承得遵循LSP(里氏替换原则):子类可以扩展父类的功能,但不能改变父类原有的功能。
它包含以下4层含义:
1. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
2. 子类中可以增加自己特有的方法。
3. 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
提到组合就出现了另一个原则——CARP(合成/聚合复用原则):
合成/聚合复用原则(Composite/Aggregate Reuse Principle或CARP)经常又叫做合成复用原(Composite Reuse Principle或CRP),就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新对象通过向这些对象的委派达到复用已有功能的目的。
React框架强烈建议组合,并以函数式编程单一功能原则——一个函数或者说组件只实现一个功能。那么这里也就出现了——使用组合的方式可能会比使用继承写更多的代码。
因为组合的问题是:
1. 整体类不能自动获得和局部类同样的接口。
2. 创建整体类的对象时,需要创建所有局部类的对象(或者说组合所有的局部类)。
综上,使用组合和继承应该遵循以下几个条件:
1. 精心设计专门用于被继承的类,继承树的抽象层应该比较稳定,一般不要多于三层;
2. 对于不是专门用于被继承的类,禁止其被继承;
3. 优先考虑用组合关系来提高代码的可重用性;
4. 子类是一种特殊的类型,而不只是父类的一个角色;
5. 子类扩展,而不是覆盖或者使父类的功能失效。
或许用面向接口编程的设计思想能比较好的解决继承导致的高耦合的问题(注意:面向接口编程和面向对象编程并不是平级的,它并不是比面向对象编程更先进的一种独立的编程思想,而是附属于面向对象思想体系,属于其一部分。或者说,它是面向对象编程体系中的思想精髓之一。)