共计 3883 个字符,预计需要花费 10 分钟才能阅读完成。
Charles Scalfani
原文:https://medium.com/@cscalfani…
我使用面向对象语言编程已经十几年了。我是用的第一个 OO 语言是 C ++, 然后是 Smalltak,最后是.NET 和 Java。
我迫切地想从面向对象的三大支柱,集成,封装和多态上得到收益。
我急于从这个到我面前的新领地得到对于重用的承诺。
我对于将现实对象映射到类的想法非常兴奋,并希望整件事能平滑迁移。
我想太多了。
继承,第一个跌落的支柱
最早,继承看起来是面向对象范式的最大收益。所有对新手灌输的关于形状继承的简化例子看起来逻辑上很合理。
我照单全收并且发现了新东西。
香蕉猴子雨林问题
我带着信仰和需要解决的问题,开始构建类继承和写代码,一切都很好。
我永远不会忘记那一天,当我打算开始从一个现有类使用继承来重用的时候,这是我一直在等待的时刻。
一个新项目来了,我想到在我上一个工程里的那个类。
没问题,重用来搞定。我从老工程里找到那个类并拷过来使用。但是。。。不只是那个类。我需要父类。但。。但先这样。啊。。。等下。。。看起来我们需要这个父类的父类。。。然后。。。我们需要所有父类。好吧。。好吧。。我来解决这个。没问题。我去。现在不能编译。为什么?哦,我知道了。。。这个对象包含了其他对象。所以我也需要那些。没问题。等等。。。我不只是需要那个对象。我需要对象的父类和他父类的父类,然后每个包含的对象和他们所有的父类。。。晕。
Joe Armstrong,Erlang 之父曾说过:
面向对象语言的问题是他们隐式的包含了他们周围的环境。你需要一个香蕉但是你得到的是一个拿着香蕉的大猩猩和整个雨林。
香蕉猴子雨林解决方案
我可以通过不写太深的继承来解决这个问题。但复用的关键就是继承,任何我对这个机制上的限制都直接限制了重用,是吧?是的。所以可怜的面向对象程序员,who’s had a healthy helping of the Kool-aid, to do? 组合和委托,后面说这个。
钻石问题
以下问题迟早会遇到,取决于使用的语言。
大部分 OO 语言不支持这个,尽管这个看起来符合逻辑。让 OO 语言支持这个有多难?想象下以下伪代码:
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 功能?Scanner?还是 Printer?不可能两个都实现。
钻石问题的解决方案
方案很简单。不要这么做。是的。大部分 OO 语言不让你这么做。但是,如果我的建模就是这样呢?我需要我的重用!那么你必须使用组合和委托。
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。这个问题也让继承范式开始出现问题。
脆弱的基类问题
所以现在我保证我的继承关系比较扁平,并不会出现环状引用。没有钻石问题。现在一切正常,直到。。。一天,我的代码运行正常,但后一天就不工作了。我没有变更我的代码。那么,这可能是个 bug。。。但等下。。。有些东西确实变了。。。但那个变动不在我的代码里。这个变动是在我继承的类里面。为什么基类的变动会导致我的代码有问题?我们先设想有个基类(我用 Java 写的,你不懂 Java 应该也可以比较容易的理解):
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。
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()的逻辑已经被派生类覆盖了。这样会导致数量 count 在每次派生类调用 add()时增加,然后在派生类调用 addAll()时再被增加一次。
这被计数了两次。
如果是这样,并且已经发生了,派生类的作者必须知道积累是被如何实现的。他们必须在每次基类变更时被通知到,因为这可能会导致派生类在不可预见的情况下工作。
太糟了!这个巨大的问题永久影响了继承范式的稳定性。
脆弱的基类解决方案
这次一样,包含和委托可以解决。使用包含和委托,我们从白盒编程转化成黑盒编程。白盒编程时,我们需要关注基类的实现。黑盒编程时,由于我们无法通过覆盖基类方法的方式来注入代码,我们可以完全忽略其实现。我们只需要关心接口。
这个趋势有点危险。。。继承应该是重用最重要的手段。OO 语言没有设计成让包含和委托方便使用。他们是被设计成让继承方便易用。如果你像我一样,你会开始对这个继承的问题开始惊奇。但更重要的是,这会让你对于继承的信心开始动摇。
继承问题
每次当我进入一家新公司,我都会对于找个地方放我公司文档的地方开始纠结,比如,员工手册。我是建一个目录叫“文档”然后在里面建个目录叫“公司”?或者我建一个目录叫“公司”然后在里面建个目录叫“文档”?都可以。但是哪一个是正确的?是最好的?目录继承的想法是基类 (父母) 更加通用,派生类 (子类) 会更加具体。而且我们自己会在继承链上做更加具象化的版本。(看上面形状继承的例子)但当一个父类和子类可以互相调换位置时,这个模型明显哪里出了问题。
继承问题解决方案
现在的问题是。。。分类继承不工作了。所以继承方式好在哪里?包含。如果你看下现实世界,你可以看到包含 (或排他所有权) 继承到处都是。而你找不到的是分类继承。让那个先等一会。面向对象范式来源于于现实世界,对象被另一个对象填入。但他使用了一个有问题的模型。分类继承,没有现实世界的基础。现实世界使用的是包含继承。一个容器包含继承的很好的例子是你的袜子。他们在袜子的抽屉里,然后被你衣服的抽屉包进去,然后又被你的卧室包含,然后又被你的房子包含。你硬盘的目录是另一个容器包含继承的例子。他们保存文件。所以我们如何对他们分类?如果你考虑下公司目录,其实我放在哪里没什么太大关系。我可以把他们放在一个叫“文档”的目录或放在一个叫“东西”的目录。我分类的方式是使用 tag 标签。我使用以下标签来给文件打标:
文档
公司
手册
标签没有顺序或继承。(这也解决了钻石问题)tag 与接口类似,你可以有多种类型与文档关联。看到这么多问题,看起来继承范式已经完了。再见,继承。
微信公众号「麦芽面包」,id「darkjune_think」开发者 / 科幻爱好者 / 硬核主机玩家 / 业余翻译家 / 书虫交流 Email: zhukunrong@yeah.net