1. 香蕉猴子丛林问题
使用一个类,但牵扯到许多类。
Erlang 的创建者 JoeArmstrong 有句名言:
面向对象语言的问题在于,它们依赖于特定的环境。你想要个香蕉,但拿到的却是拿着香蕉的>猩猩,乃至最后你拥有了整片丛林。
解决办法:不要把类层次建得那么深。
但如果继承是重用的关键,那么给继承机制添加的任何限制都会限制重用。
这是建议使用:包含和委托(Contain and Delegate)。
2. 菱形继承问题
Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
function start() {
}
}
Class Printer inherits from PoweredDevice {
function start() {
}
}
Class Copier inherits from Scanner, Printer {
}
Scanner 和 Printer 类都实现了名为 start 方法。但Copier继承哪个start?
解决办法:不要这样做。如果必须这样建模,使用包含和委托。
Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
function start() {
}
}
Class Printer inherits from PoweredDevice {
function start() {
}
}
Class Copier {
Scanner scanner
Printer printer
function start() {
printer.start()
}
}
Copier 类包含一个 Printer 实例和一个 Scanner 实例。然后将 start 函数委托给 Printer 类的实现。要委托给 Scanner 同理。
总结 1 和 2:尽量使用较浅的类层次结构,并保证里面没有环。
3. 关于基类
一个基类如下:
import java.util.ArrayList;
public class Array
{
private ArrayList<Object> a = new ArrayList<Object>();
public void add(Object element)
{
a.add(element);
}
public void addAll(Object elements[])
{
for (int i = 0; i < elements.length; ++i)
a.add(elements[i]); // this line is going to be changed
}
}
这个类的接口上有两个函数:add() 和 addAll()。add() 函数负责添加一个元素,addAll() 函数会调用 add 函数添加多个元素。 注意加注释的行,后面讲解这个坑。
下面一个继承类:
public class ArrayCount extends Array
{
private int count = 0;
@Override
public void add(Object element)
{
super.add(element);
++count;
}
@Override
public void addAll(Object elements[])
{
super.addAll(elements);
count += elements.length;
}
}
ArrayCount类是通用的Array类的特化。两者行为上的唯一区别就是ArrayCount会维护一个count,记录元素的个数。
下面分析一下:
Array的add()给局部的ArrayList添加一个元素。
Array的addAll()针对每个元素调用局部的ArrayList的add方法。
ArrayCount的add()调用父类的add()然后增加count。
ArrayCount的addAll()调用父类的addAll()然后给count增加相当于元素个数的数。
一切都很正常。
现在问题来了,基类中加注释的那行代码现在改成这样:
public void addAll(Object elements[])
{
for (int i = 0; i < elements.length; ++i)
add(elements[i]); // this line was changed
}
从基类作者的角度来看,这个类实现的功能完全没有变化。而且所有自动化测试也都通过了。
但是基类的作者忘记了继承的类,所以继承类的作者便被错误吵醒了。
现在ArrayCount的addAll()调用父类的addAll(),后者在内部调用add(),而add()被继承类重载了。
因此,每次继承类的add()被调用时,count都会增加,然后在继承类的addAll()被调用时再次增加。
count被增加了两次。
既然会发生这种现象,那么继承类的作者必须清楚基类是怎样实现的。而且,基类的每个改动必须要通知所有继承类的作者,因为这些改动可能会以不可预知的方式破坏继承类。
解决方法
这个问题还得要包含和委托来解决。
使用包含和委托,可以从白盒编程转到黑盒编程。白盒编程的意思是说,写继承类时必须要了解基类的实现。
而黑盒编程可以完全无视基类的实现,因为不可能通过重载函数的方式向基类注入代码。只需要关注接口即可。
这种趋势太讨厌了……
继承本应带来最好用的重用。
在面向对象语言中实现包含和委托并不容易。它们是为了继承方便而设计的。
如果你和我一样,你就会开始反思这个继承了。但更重要的是,这些问题应当引起你对于通过层次结构进行分类的反思。
4. 层次结构的问题
每到一个新公司时,我都要为在哪儿保存公司文档(即员工手册)而纠结。
是应该建一个Documents文件夹,然后在里面建个Company呢?
还是应该建个Company文件夹,然后在里面建个Documents呢?
两者都可以。但哪个是正确的?哪个更好?
层次分类的思想是因为基类(父类)更通用,继承类(子类)更专用。沿着继承链越往下走,概念就越专用。
但如果父节点和子节点能随意交换位置,那么显然这种模型是有问题的。
真正的问题出在层次分类是错误的。
那层次分类应该用在哪里?
包含关系。
真实世界里有很多包含关系(或者叫做独占关系)的层次结构。
但你找不到层次分类。仔细想一下,面向对象范式是根据充满了各种对象的真实世界建立的。但它用错了模型——层次分类在真实世界中没有类比。
真实世界里到处都是层次包含关系。层次包含关系的一个非常好的例子就是你的袜子。袜子放在装袜子的抽屉里,然后抽屉包含在衣柜里,衣柜包含在卧室里,卧室包含在房子里,等等。
硬盘上的目录也是层次包含关系的另一个例子——它们包含文件。
那我们该怎样分类呢?
仔细想一下公司文档,就会发现其实放在哪儿都无所谓。我可以放在Documents目录下或者放在Stuff目录下也可以。
我选择的分类法是标签。我给它加上不同的标签。
参考:
https://mp.weixin.qq.com/s?__biz=MzA4NDY3ODAwNA==&mid=2651234676&idx=1&sn=c5598c8577dd58e5f41caaf4fdf322b0&chksm=8411856fb3660c790332083d27e232b7da7556d7d0c1fc01e2420fbbd874db99cd8b8fd93eb3&mpshare=1&scene=23&srcid=&sharer_sharetime=1585705636549&sharer_shareid=1f90a8080b8d75a3636ab2ea2c26028b#rd