组合和继承
组合优于继承的关键因素是去耦合,或者说,就像积木一样的模块,且应当尽可能的消除外部的状态和副作用。理想的组合,彼此应当是原子的。只对自身的内部负责,而不关心外部。但类就不一样了,继承本身就暗示了相反的东西,它的关键点在于「状态和行为的共享」——这会造成无形间的耦合,这是代价,但在某些时候确实是需要的。这里我们可以从何时用组合看出来。什么时候用它?组合里面提到「有一个」和「是一个」的区别,类本身是字面意义上的「一类」,换言之,他们是一个物种——共享结构上固定的(就像上面所说的)、提前定义好的父类状态(属性)和方法(实现的具体行为)。
而我们所说的耦合,指的就是这种状态共享上带来的隐藏关联。父类和子类共享一个可变的状态,但它对下面的继承者扩展了什么是无知的,换言之它无法预料子类到底依赖了自己的哪个部分,也许你更改了一个无关紧要的东西,但远在天边的另一个功能坏了;或者反过来,任何一个子类改了祖父类的状态,使用这个状态的他自己的父类就可能出问题,等等诸如此类——这就是隐式依赖,顺著整个链条传导的后果。
正常来说哪怕是扩展,用的时候都不该出问题,如果约定良好,确保继承时的纪律和测试覆盖,的确可以让继承链出问题时不至于出生产事故。但是,我为什么非要搞得这么耦合呢?万一子类就是胡搞,导致方法直接出错,或者搞了非预期的副作用,只要你是关联的,那就还是会出问题。毕竟在团队开发中,每个人都只能看到自己的一个部分,甚至人员流动之下,前后都是彼此不共享信息的。撰写父类的人不可能穷尽一切,只要开了扩展点,那什么牛鬼蛇神都可能出来。
如果我需要专注于状态和行为的具体实现,且希望后续的依赖方「继承」(字面意义上)这些内容,那这种情况下用基类和继承的模式是标准做法,没什么冗余的。毕竟我不能让人类当一个佛兰肯斯坦,用一大堆不相干的「积木」去拼凑,就算这样最终能跑起来,它也会像科学怪人那样是让人困惑的——虽然就像上述说的一样,能动,但我们的印象就是人类有其固有的状态,这些状态必须是继承的而不是拼凑的,人类必须得是「是一个」,所以根本不是标准做法了,而是成了「反模式」。
但有些情况下我们就需要组合:积木、去耦合化的模块。它面向的是「有一个」。我只是想要一个复用的功能,就跟乐高一样,可以在任何地方拼,也不依赖什么必然的固定状态,那组合就再合适不过了。你拼成房子还是直升机我不感兴趣,只要你能拼,那你就能用。
而对组合来说,至关重要的一个东西就是「接口」。何为接口?如果说类是专注于状态和行为的具体实现,那么接口就是专注于结构本身的约束——它不关心实现,而只关心这个实现应该是什么形状的。
从另外一个方面说,组合并不与类排斥。因为组合的灵活性,它的拼装场所可以发生在任何地方——例如类的里面。即便是类,我也没必要把各种工具方法都作为这个类的固有行为塞进去,那只会造成过于臃肿的方法膨胀。需要的时候直接使用组合工具,为什么不呢?只要组合能够确保自己的输入和输出是不变的,那么它对任何使用方也都是明确的。