组合凭什么大于继承
“组合大于继承”这句话大家肯定都听过,但要我说,这纯属扯淡。
组合和继承是平等的逻辑关系,分别表示has-a和is-a,哪有什么大于小于。就像辣椒和土豆,也许辣椒炒肉就是比土豆炒肉好吃,但你能说辣椒大于或优于土豆吗?
程序员们的第一行代码,往往是打印'hello world',因为他们最初的梦想,就是用代码改变世界。这些痴迷代码的nerd,相信现实世界运行在某种规则之上,而对规则的抽象与模拟,正是程序员们争相讨论、乐此不疲的事。
比如,实现一只鸟,程序员们就会想,这很简单。鸟都需要吃东西,鸟的特色是有翅膀会飞。
一个对现实事务的模拟,就这么优雅的实现出来了。
class Bird {
eat(){}
fly(){}
}
后来,需要模拟一只鸡,鸡也很明确,属于鸟。虽说溜达鸡绝大时间都在地上,但也勉强属于会飞。
为了让代码简洁优雅,可以直接把Bird的代码拿来复用。在此基础上加个打鸣的方法。
class Chicken extends Bird {
constructer(){
super()
}
// 打鸣
crow(){}
}
在用继承的方法实现很多鸟类之后,难题出现了。
现在要实现一只鸵鸟。鸡鸭鹅你说会飞,大家勉强认可。鸵鸟你说它会飞,多少有点过分了。
现在有两个解决方案。
方案一:把会飞从Bird类拿出来。
显然鸵鸟证明了会飞不是鸟类普遍具备的特点。这个方案的优点是可以从根源上解决问题。缺点是波及范围太大了,大量鸟类继承Bird,因此失去了飞行能力,你可能得一一检查所有鸟类的实现,再逐个把飞行功能添加回去。
这种改动就像房子快盖好了却发现地基有问题。哪怕开发同学为了代码的优雅,可以乐此不疲、不嫌累的折腾,但测试同学受不了。你的一点小改动,他的工作量要直线增加。
方案二:对鸵鸟做特殊处理,在鸵鸟类里重写飞行函数,让鸵鸟试图飞行时代码报错。
这个方案的好处是改动范围小,缺点是代码变得丑陋、危险。如果有一天代码交到其他开发者手里,他不管三七二十一,让所有鸟类起飞,那就爆雷了。他一定一遍骂娘一遍看git提交记录,查这行臭代码是谁加上去的。
所以后来,一些工程实践派的大佬们提出了组合大于继承。
组合大于继承
既然鸟都不一定会飞,那到底什么一定会飞?会飞的一定会飞。
所以大佬们用朴素的哲学思想实现了便于组合的Flyable类。
interface Flyable {
fly(): void;
}
如果鸡需要会飞,只需要拥有Flyable成员。
class Chicken extends Bird implements Flyable {
constructor(private flyer: Flyable = new WingFlight()) {}
fly() { this.flyer.fly(); }
}
鸵鸟不会飞,那就不需要添加Flyable成员。
class Ostrich extends Bird {
constructor() {
super();
}
run() { console.log("在荒漠中狂奔..."); }
}
甚至飞机也会飞,它也能优雅的复用飞行代码。
class Airplane implements Flyable {
constructor(private flyer: Flyable = new JetFlight()) {}
fly() { this.flyer.fly(); }
}
这种组合思路像把代码功能拆分成乐高积木。实现功能的过程就像搭积木的过程。
有搭积木经验的宝宝们都知道,单个积木越小、越细,那成品的自由度和可能性就越大,积木单元越大自由度越小。在软件工程中也是如此,为了避免出现上文”脆弱基类问题”,每个功能模块都要尽可能的细致拆分。这样的好处是代码更加灵活,复用起来更容易,测试起来也方便。
但缺点也很明显,过于细致的拆分导致模板代码、重复代码的出现。程序中对类型的定义也变得模糊了,汽车不再是那个交通工具,而变成了大沙发加四个轮子。摩托车也跟着变成了小沙发加两个轮子。这种代码转交给其他开发者维护起来并不容易,他很可能把小沙发加两个轮子理解成了电动自行车或者别的什么冒着黑烟的盗抢工具,从而强迫他们走自行车道、剥夺他们的路权和加油权。
结论
谈到面向对象,大家还是会提起“封装、继承、多态”。而组合大于继承,更多的是工程实践而非面向对象的特性。事实上,你甚至不需要class类,就可以优雅组合很多功能代码。React组件就是一个例子。

这是一个常见的输入框组件,但它的实现组合了背景框、输入组件、分割线、还有若干个图标组件。每个组成部分既可以单独工作,又可以有机集合。这无疑是“组合大于继承”的最佳案例。

但早期版本的React组件又是对“继承”概念的正确示范。就算天塌下来了,自定义组件也是继承自Component类,继承关系如此明确,永远不动摇。
继承是对“is-a”逻辑的表达,组合是对“has-a”逻辑的表达。这里没有孰优孰劣,只是在工程实践中,因为需求总是不确定的、多变的,所以对“is-a”的断言就变得稀有,对"has-a"的使用变得逐渐泛滥。
一句“组合大于继承”,可能蕴含了太多次开发者面对需求变更时的隐忍,还有对产品经理、老板、甲方的一次次纵容,最后咬咬牙,让两个本该平行的工程分出了高下优劣之别。