# 慎用继承——以谨慎之心对待权力

# 为什么?

一般接口如果设计得不成功,尚可用Deprecated来补救,尽管不是一件令人愉快的事情。
可从父类继承下来的接口如果不适合,那就永远是一个甩不掉的包袱,除非把整个类都标为Deprecated。
  • 两点启示
就类的设计而言,依赖文档来禁止某个接口的使用实为下策,暴露了设计上的缺陷;
就类的使用者而言,应该认真阅读规范文档,避免错误的用户导致错误的程序。(P262)

# 继承是一种静态、显性的关系

静态指关系是在编译期间建立的,且无法在运行期间改变;
显性指关系是公开的,如果通过源代码来改变,将会影响客户代码。

这类强耦合关系降低了关键的应变能力,应该尽量避免。

# 合成关系是隐性的

属于内部实现,此外合成关系还是动态的,实现者可以再运行期间设定依托的具体类型。(P264)

合成和聚合(aggregation)关注整体与部分的关系,侧重静态结构;

委托(delegation)或转发(forwarding)关注表面受理者与实际执行者的分离,侧重动态功能。
它们在实际应用中经常是重合的,但也有例外,即合成不一定是为了委托,委托也不一定要通过合成。

如果合成是不透明的黑盒重用(black-box reuse),那么实现继承是半透明的灰盒重用(grey-box reuse)。
因此相比合成,实现继承还有一个很大的弊病,就是可能破坏封装。

# 继承还能通过什么方式来破坏封装?

首先,需要封装的信息不只是内部数据,还包括实现方式和内部逻辑。
当一个类通过继承成为另一个类的子类时,本身就暴露了了部分实现方式,从某种意义上来说已是一种不完整的封装。

其次,子类除了能和父类亲密接触外,还有一个特权:覆盖(override)。
这种权力很强大,也很微妙。对恶意者来说,它是一个安全漏洞;对初学者来说它又是一个温柔陷阱。

# 编程是这样一种游戏

既要进攻得力——保证意而为之的正面效应;又要防守得当——控制无意为之的负面效应。
如果只是为了进攻,那静态语言恐怕早就消失殆尽,因为动态语言的进攻手段灵活丰富地多。
滴水不漏的防守往往比水银泻地的进攻更困难,然重攻轻守乃人之通病,是以代码臭虫如附骨之疽。

# 由于子类与父类代码上是隔离的,多态机制又是隐性的,防守起来殊为不易。

  • 防守要点
首先,子类应坚持父类的外在行为,不能破坏父类指定的服务规范。
理论上人们容易理解里氏替换原则的重要性,但实践上经常会有疏漏——在覆盖父类的某一个方法时注意遵循,却无意破坏了相关方法的规范。
譬如,子类如果覆盖父类的add方法,很可能需要修改相应的remove方法,同时要估计addAll、removeAll等方法是否受到牵连。

其次,子类应该正视父类的内置逻辑,既不能忽视或破坏父类规范之类的逻辑关联,又不能假设或依赖规范之类的逻辑关联。

# 关于继承的用法,可概括为12字方针:“提倡接口继承,慎用实现继承”。

实现继承最大的硬伤是在类与类之间建立了强耦合关系,使代码趋于僵硬、脆弱和复杂。
		这种关系是永固的,一经建立无法解除;
		这种关系是纵深的,不限于相邻的父类和子类,更贯彻整个类族,这种关系还是双向的,从上到下,从下到上。
    一旦某个类选择了继承就进入了一个大家庭,与它们同呼吸、共命运、休戚相关。

从此该类不能退出家庭圈,除非毫不顾忌自己的客户;
该类不仅要了解其父类,还要了解一切祖先类;
不仅要了解public成员,还要了解protected成员。

最为麻烦的是,如果想覆盖某一个方法成员,还得了解其祖先类成员之间的内在逻辑关系;
如果想增加一个方法成员,还得祈祷它不与祖先类将来的版本发生冲突。
Java中的CountingStack类是著名的‘脆弱的基类’问题(Fragile Base Class Problem)的一个典型代表。

# 实现继承至少有3处用武之地:

	希望访问基础的protected成员;
	希望覆盖基础类的方法,并且难以用合成来变通;
	希望成为基础类的子类型。
  • 更实际的建议是:
每当为继承与否举棋不定之时,问自己两个问题:
	采用合成是否会遇到无法克服的困难?
	基础类是否专门为继承而设计的?

# final & abstract 修饰符

有些类型是不打算要后代的终极类型,Java中以final修饰。
另一个极端是,有些类必须被继承,否则不能被实例化,从而无法发挥其实例方法的作用,Java中以abstract修饰。
介于上述两者之间的类占大多数,但其中不适合继承的也占大多数。

# 继承树

每一种非抽象类都是一种具体类型,是在继承树上一步步进化和完善的结果,至此羽翼已丰,能独当一面了。
在概念上已经具化,在功能上已经完备,不在具有发展的动力。
作为用户定义的数据类,与内建的基础数据类型,一起构筑程序的终端数据类型。
  • 理想的继承树应该是
所有的叶子都是具体类,而所有的树枝都是抽象类。
实际应用当然不可能完全做到,但应尽可能地向此靠拢。

允许一个类被继承,意味着其服务对象除了普通客户之外,又增添了一类特殊的客户——继承者。
这是一项重大的决定,在设计和规范上都应该三思而后行。
其中谨防继承带来的封装破坏是关键。如何封堵这个漏洞?
除了禁用protected域成员,保证protected方法成员的规范性和稳定性外,最要紧的是防止覆盖的副作用。

# 覆盖

可覆盖的方法具有可扩展性,但也是破坏封装的罪魁祸首。

相反,不可覆盖的方法(Java 中 final修饰的方法)丧失了部分的灵活性,但同时也具备了稳定性和可靠性。
它不仅能避免继承带来的安全漏洞和封装破坏,还能节省时空上的开销,并可通过内联带来性能上的改善。

总之,实例方法默认不可覆盖或许更合理。
  要知道,使用默认或出于无意,不用默认才是蓄谋。
	鉴于多态给继承带来的微妙影响,最好避免无意的多态,确保多态是有意而为之的设计决定。
		这也暗合语法与语义的一致性原则。

# 语法对设计的影响是真切的

——程序员多习惯性地采用默认的方式进行编程设计。
	
就解释了C++程序的函数多为非virtual 而Java 程序的方法多为非final的现象
可以想见的其他原因是
			C++程序更追求效率,而虚函数有多余的时空开销;
			C++程序经常不通过指针或引用而直接操作对象,此时多态无法起作用;
			Java的虚拟机能更好地对多态方法进行内联优化。

# C++中非虚接口(Non-Virtual Interface,NVI)模式

将公有函数非虚化,将虚函数私有化。
		C++中private 虚函数可以被覆盖,Java和C#中不能,后两者在此模式中将采用protected)

该模式可以理解为
		一个方法,如果是公有的,就不要是多态的;如果使多态的,就不要是公有的。

任何模式都有适用的场景,非虚接口模式也不例外,更合逻辑的断言是:
		在公有和多态产生矛盾时,即是非虚接口模式大显身手之际。

# 软件设计的四字要诀

外静内动。
即保持外部的接口不变,允许内部的实现变动。
大到库、框架、架构等的设计,小到具体的函数、类等的实现概莫能外。
在规范抽象中,静的是功能规范,动的是实现细节;
在数据抽象中,静的是API接口,动的是接口实现;
在多态抽象中,静的是interface接口,动的是实现类;
在非虚接口模式中,静的是多态的对外接口,动的是多态的对内挂钩。
(挂钩即使Hook,也就是callback。借助多态,处于底层的父类调用了处于高层的子类的方法,正是一种回调。
这种内部回调因子类而异,故谓之动;非多态的外部接口不能被覆盖,不因子类而异,故谓之静。)
看上去灵活性欠缺,但凡事过犹不及,灵活过度导致失去控制。过度的多态带来的危害足以另我们警醒。

# 模板方法的基本思想:(P278)

固定基本骨架,同时你保留部分可扩展点,是算法既保持足够的坚固性,又不失必要的柔韧性。

在可扩展点处,父类通过回调达成对子类的控制反转。
		因此模板方法可以看成是微型框架模式,正如设计模式可以看做微型架构模式。
    
该模式解决‘脆弱的基类’问题是有前提的,
		如果类的一个公有方法直接或间接调用了自身的另一个多态方法。
			如果这种自用只为一时之便,则尽可能避免。

		如果二者之间确实存在必然的联系,则类的维护者有责任将此联系规范化,让多态的被调方法非公有化,
    以便分离接口和挂钩的职责,保障代码的健壮性,并减轻子类的负担。

		如果规范与设计双管齐下,一个类才算得上是合格的父类。

# 推荐的方法修饰符

	公开接口 
		public
		非多态
	内部挂钩
		protected或private(C++)
		多态
	内部接口
		protected或package(Java)internal(C#)
		多态
	自用
		private
		非多态

# 多重继承

	Java和C#不支持,C++支持但不提倡

	这里的多重继承是指多重实现继承,接口继承不在受限之列。
  
	多重继承最大的问题是,语法复杂,语义晦涩,尤以著名的菱形问题为代表。(P279)

	在单继承的机制下,一旦一个类继承了另一个类,变没机会再继承其他类了。
	面对唯一的名额,选择起来自须反复斟酌。

	与此对照,同时合成多个类是毫无疑问的,这再次体现了合成的优势。

# 使用继承其他应该注意的问题:

	继承的层次不宜过深;
	在构造方法中不宜间接或者直接的调用多态方法;
	在克隆和序列化时也需要特别谨慎;
	‘脆弱的基类’问题,除了反应在语义上,还可能反应在语法上,如修改基类导致的二进制兼容问题;等等