组合VS继承

为什么组合好于继承?

Wiki中Com­po­si­tion over Inher­i­tance定义是领域建模:

使用组合而不是继承是一种设计原则,能够带来设计的更高灵活性,带给业务领域的类代码更长时间的稳定性。

“拆分”和“复用”是系统设计中2个永恒的主题,在这两者之间如何达到一种平衡是艺术。从技术角度讲,继承和组合没有好与坏之说。不知道大家是否注意到,“组合比继承好”是一种实战观点,都是经验之谈。其实这很好理解。因为业务越来越复杂,变化越来越快,原来的业务设计思路已经不满足现在的业务需要,继承会限制系统的重构。从业务角度看,如果有人能够综观某个事物(行业)几十年的发展,也就是常说的吃透了业务,在这种假定之下来用继承来设计业务,那一定会成为经典。但现实的场景中,这样的人太少,对于快速发展的中国尤其如此,所以我们才说“组合比继承好”。从本质上说,这是我们总是暗示自己“我们总是目光短浅,组合的风险比继承少”。

两种比较常用的思考方法(或解决方法),用哪一种,还是取决于问题域。
一般说来,“继承”适用于“算法”(业务过程)的分解;“组合”适用于“数据结构”(业务对象)的分解。
比如一个服务类,要验证输入、处理业务、输出结果,那么定义一个父类,把这个过程的整体描述出来,然后,子类去处理细节。(一般/特殊的思维方式)
再比如一个订单涉及用户、商户、商品等,那么显而易见,也自然而然,订单就该划分出这么多对象。(整体/局部的思维方式)
“继承”在代码中的使用相对要求高一些,因为抽象的角度如果不准确或有点晦涩,那么维护代码的人(可能还不了解业务过程)就不能不抓狂了。
看“继承”关系,了解业务过程的划分;看“组合”关系,了解业务对象的划分。如果要从“继承”关系,了解业务对象,从“组合”关系,了解业务过程,那当然比较麻烦了,因为方向错了,无异于缘木求鱼。

组合和继承实际可能代表两种完全不同的思维方式

这两种思维体现在Unix和Windows的不同上。
Unix是典型的组合思维,包括Go语言,我们使用Unix命令行,比如“ps -ax | grep java”,这种管道式过滤能力非常强大和灵活,缺点是初学者不容易掌握,不形象。

而Windows是典型的继承思维,大窗口里有小窗口,不断打开小窗口,就进行树结构的深入层次,这种方式非常形象,适合初学者,但是问题也是显然,不够灵活强大,以至于我们经常借助于DOS的bat来完成各种特定的要求,以至于需要编程,使用编程语言组合成我们希望得到的功能。

Windows这种窗口方式非常对象化,也是微软长期对操作系统这个业务掌握后的抽象,它是一种形象化的树形结构,带领初学者进入深处。

Unix的命令组合是一种动态的树形结构,类似函数编程,通过通过函数碎块组合成使用者想达到的强大功能。

很多人思维如果局限在Windows这种继承思维中,会觉得Unix好像不人性化,对程序员不够尊重,这其实是身在庐山中不识庐山真面貌。

This is why you need Composition over Inheritance

https://theburningmonk.com/2015/03/this-is-why-you-need-composition-over-inheritance/

在实践中如何使用组合

https://fsharpforfunandprofit.com/posts/recipe-part2/

组合与继承的区别和联系(引用https://github.com/hollischuang/toBeTopJavaer/blob/master/basics/java-basic/inheritance-composition.md

在继承结构中,父类的内部细节对于子类是可见的。所以我们通常也可以说通过继承的代码复用是一种白盒式代码复用。(如果基类的实现发生改变,那么派生类的实现也将随之改变。这样就导致了子类行为的不可预知性;)

组合是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的,所以我们也说这种方式的代码复用是黑盒式代码复用。(因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法)

继承,在写代码的时候就要指名具体继承哪个类,所以,在编译期就确定了关系。(从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。)

组合,在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在运行期确定。

优缺点对比

组 合 关 系 继 承 关 系
优点:不破坏封装,整体类与局部类之间松耦合,彼此相对独立 缺点:破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性
优点:具有较好的可扩展性 缺点:支持扩展,但是往往以增加系统结构的复杂度为代价
优点:支持动态组合。在运行时,整体对象可以选择不同类型的局部对象 缺点:不支持动态继承。在运行时,子类无法选择不同的父类
优点:整体类可以对局部类进行包装,封装局部类的接口,提供新的接口 缺点:子类不能改变父类的接口
缺点:整体类不能自动获得和局部类同样的接口 优点:子类能自动继承父类的接口
缺点:创建整体类的对象时,需要创建所有局部类的对象 优点:创建子类的对象时,无须创建父类的对象

Go语言是彻底的面向组合的并发语言

If C++ and Java are about type hierarchies and the taxonomy of types, Go is about composition.
如果说C++和Java是关于类型的层次和分类,那么Go是关于组合。

  • Go语言通过组合实现了Java传统语言中使用继承实现的多态性。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    package main

    import (
    "log"
    )

    type B struct{ }
    func (b B) foo() { log.Printf("...") }

    type A struct {
    B
    }

    func main() {
    var a A
    a.foo()
    }

继承(单继承)和接口(多继承)

继承- 事物的自然属性和行为。比如:你从您的父亲或爷爷或更老的祖先那里获取的属性和行为。
接口- 事物的社会属性和行为。比如:你从一个自然人长大以后,你要工作了,经济不景气,一连学了不少技能(驾驶,烹饪),换了好几份工作(打手,歌星)等等。驾驶,烹饪是一种行为能力,打手,歌星属于身份。

进一步,借用一下苏格拉底的问题:我是谁,我来自哪里,我将去哪里?

我们也可以提出类似的问题:对象是什么?对象从哪里来(继承)?对象将去哪里(接口)?这问题可大可小,可不回答。

落实在代码中,如果是有状态的行为复用,用继承;如果是无状态的行为的复用,用接口。

在有些语言中,继承之所以是单继承:原因怕是犹如鸠摩智一般,练了小无相功、又学了少林七十二绝技、易筋经,而又没有领悟化解之佛法(管理好各种继承来的状态),容易走火入魔呀。
http://www.w3china.org/blog/more.asp?name=sixsun&id=41657

参考

https://www.jdon.com/46681

-------------本文结束感谢您的阅读-------------