文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏
参与翻译: rekii (22)

我用面向对象编程语言进行编程已经几十年了。我用过的第一种面向对象语言是C++,然后是Smalltalk,最后是.NET和Java。

我热心于利用继承,封装和多态带来的好处。即面向对象编程范式的三大支柱。

我渴望获得代码复用的承诺,并收获前人在这一令人兴奋的新领域的智慧。

想到把真实世界的对象映射到它们对应的类,我就抑制不住我的激动,我希望整个世界都整齐地摆放着。

第 1 段(可获 2 积分)

我错的不能再错了。

继承,第一个支柱倒了

乍看,继承似乎是面向对象编程范式最大的利好。所有给新手举的简单化了的关于形状层次结构的例子似乎都符合逻辑意义。

复用是今天流行的词汇。不,是今年,也许是永远都流行的词汇。

我完全相信这些观点,然后冲出去告诉这个世界我的新发现。

香蕉猴子丛林问题

心怀信仰和需要解决的问题,我开始建立类层次结构,并且开始写代码。世界一如往常。

第 2 段(可获 2 积分)

我永远不会忘记那一天,我准备继承一个现有的类,依次兑现代码复用的承诺。这是我等待已久的时刻。

一个新工程开始了,我想起了上一个工程里我那么喜欢的那个类。

没问题。复用可以解决问题。所有我需要做的就是把另一个工程里的类弄过来,然后用起来。

好吧...事实上...不止那个类。还需要他的父类。但是...但是就是这样。

额...等等...似乎还需要父类的父类...然后...还需要所有的父类。好吧...好吧...我会处理好的。没问题。

第 3 段(可获 2 积分)

太好了。不能编译。什么?噢...让我看看...这个对象包含了这个对象。所以我那个对象我也需要。没问题。

等等...我不止需要那个对象。我需要那个对象的父类和父类的父类,然后每一个包含进来的对象和所有他们的父类,以及相应的父类的父类的父类的父类....

啊。

Erlang的创造者Joe Armstrong有一句伟大的格言:

面向对象语言的问题在于,他们需要对象,但是不得不把所有围绕在对象身边的隐含的环境也包含进来。你想要一只香蕉,但是却得到一只大猩猩手里握着香蕉,以及整片丛林。

第 4 段(可获 2 积分)

香蕉猴子丛林问题解决方案

我可以通过不创造那么深的继承层次结构来摆平这个问题。但是加入继承是复用的关键,任何加在这个机制上面的限制肯定都会限制复用的好处,对吗?

对的。

所以一个可怜的为饮料行业做出杰出贡献的面向对象程序员,应该做什么呢?

组合和委派。等会儿再讲这个话题。

菱形继承问题

迟早,下面这个问题将会露出他丑陋的,对于某些语言来说难以解决的头颅。(这里有点拗口:意思是很多语言不能解决菱形继承问题)

大多数面向对象语言都不支持菱形继承,尽管菱形继承看起来很有逻辑意义。在OO语言中支持菱形继承的难点在哪?

第 5 段(可获 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函数呢?Scanner里的那个?还是Printer里的那个?当然不能是两个都继承。

菱形继承解决方案

解决方案很简单。不要菱形继承。

是的那是对的。大多数OO语言都不让你那么做。

第 6 段(可获 2 积分)

但是,但是...如果我不得不这样建模呢?我想要我的复用!

那么你必须要组合和委派。

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类现在组合了一个Scanner实例和一个Printer实例。他委派了Printer实现start函数。也可以简单的委派给Scanner。

第 7 段(可获 2 积分)

这个问题是继承支柱上的里另一个裂纹。

脆弱的基类

现在我让继承层次关系更浅,而且避免他们形成环状。没有菱形继承。

然后一切正常。直到...

有一天,我的代码工作正常,但是第二天就停止了工作。诡异的事情是,我没有修改过我的代码。

好吧,也许这是一个bug...但是等等...有些事确实改变了...

但改的不是我的代码。改的是我继承的类。

为什么改变基类还能影响到我的代码??

怎么回事...

考虑下面的基类(这是用Java写的,但是即使你不懂Java应该也很容易理解)

第 8 段(可获 2 积分)
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函数添加多个元素。

第 9 段(可获 2 积分)

这里是子类:

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实例添加每个元素。

第 10 段(可获 2 积分)

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
  }

对于基类所有者来说,函数依然工作正常。所有自动测试用例依然通过。

第 11 段(可获 2 积分)

但是对于基类所有者不会知道继承类的存在。然后子类所有者被粗鲁地弄醒。

现在 ArrayCount 的 addAll 调用父类的 addAll,addAll 内部调用 add,add 被子类覆盖。

这就导致了 count 变量在子类的 add 调用时递增,然后在子类的 addAll 调用时又增加了一次。

计算了两遍增加的元素数。

如果这可能发生,这确实发生了,那么继承类的作者必须知道基类是如何实现的。然后每个基类的变更都必须通知他们,因为这可能会以不可预测的方式破坏他们的继承类。

第 12 段(可获 2 积分)

啊!这个巨大的裂缝永久地威胁着继承这根支柱的可靠性。

脆弱基类的解决方案

又一次组合和委派来帮忙。

用组合和委派,我们从白盒子编程转到黑盒编程。白盒子编程中,我们需要关注基类的实现。

黑盒编程中,我们完全可以忽略基类的实现因为我们不能通过覆盖向基类注入代码。我们只需要关注接口。

第 13 段(可获 2 积分)

这个趋势让人感到不安...

继承被认为是代码复用的巨大胜利。

面向对象语言并没有让组合和委派变得容易实现。它们被设计成让继承更容易实现。

如果你想我一样,你会开始怀疑继承。但是更重要的是,这会动摇你对继承构建层次结构的信心。

层次结构问题

每次我到一个新公司,我总是纠结一个问题,就是我该在哪里新建一个文件夹来存放公司的文档,例如员工手册。

我要新建一个叫做Documents的文件夹,然后在里面新建Company文件夹吗?

第 14 段(可获 2 积分)

还是新建Company文件夹,然后在里面新建Documents文件夹?

都能正常工作,但是哪一种方式是正确的?哪一种是最好的?

转化成层次结构的观点就是有一般化的基类(父类)和具体的继承类(子类)。甚至是更特殊的我们自定义的继承链的子类。(看上面的形状层次结构)

但如果父类和子类可以任意转化角色,那么很显然这个模型某个地方存在错误。

层次结构模型解决方案

错误的地方在于...(译注:即其实没错,没法解决)

第 15 段(可获 2 积分)

分类层次结构不好使

层次结构擅长做什么?

包含

如果你观察真实的世界,你会发现到处都是‘包含(或者说独家持有)层次结构’。

反而,现实中找不到的是分类层次结构。再深入一点。面向对象编程范式是建立在充满对象的真是世界上的。但是他却用了一个蹩脚的模型,即分类层次结构。在真实世界没有类似的结构。

但是真是世界充满了包含层次结构。一个绝好的例子是你的袜子。他们在一个袜子抽屉里,抽屉包含在一个衣柜里,衣柜包含在你的卧室里,卧室包含在你的房子里,等等。

第 16 段(可获 2 积分)

另一个包含层次结构的例子是目录,目录存在你的硬盘里,目录包含了文件。

所以,我们如何组织这个结构呢?

如果你想起来公司文件的例子,其实把文件放哪个文件夹根本不重要。我可以把文件放在一个叫做Documents的目录或者叫Stuff的目录。

我用标签来组织这件事。我给文件打上下面的标签

Document
Company
Handbook

标签没有顺序或者层次结构(这也解决了菱形继承的问题)

标签跟接口类似,因为你可以给文件关联多种类型。

第 17 段(可获 2 积分)

但是有了这么多裂痕,似乎继承的柱子已经塌了。

再见,继承。

封装,第二个要倒塌的支柱

乍看,封装似乎是第二大面向对象的利好。

对象状态变量被保护起来不让外界访问,即封装在对象内部。

不需要多久,我们就不得不担心谁都可以访问全局变量的问题。

封装可以让你的变量更安全。

封装简直不可思议!!

封装相安无事...

直到...

引用问题

第 18 段(可获 2 积分)

为了提高效率,对象不是通过传值而是通过传引用的方式调用函数。

意思是说,不是直接传递对象,而是传递对象的引用或者指针。

如果一个对象的引用被传递到另一个对象的构造函数中,构造函数就可以把那个对象的引用放进私有变量,通过封装保护起来。

但是那个被传递的对象就不安全了!

为什么呢?因为其他代码有了这个对象的指针,即那段被叫做构造函数的代码。必须要把对象的引用传递给构造函数吗?

第 19 段(可获 2 积分)

引用解决方案

构造函数不得不拷贝传递进来的对象。并且不是浅拷贝,而是深拷贝,即拷贝包含在被拷贝对象内的对象,以及包含在这些对象内的所有其他对象,一层一层。

非常影响效率。

然后皮球来了。不是所有的对象都可以被拷贝。有些对象有相关联的系统资源,这会使得拷贝变得毫无用处甚至根本不可能拷贝。

每种主流OO语言都有这个问题。

再见,封装。

多态,第三根倒塌的柱子

多态机制是面向对象三人组中红头发的继子。(译注:无厘头喜剧《三个臭皮匠》的梗,图片左一,名字叫Larry)

第 20 段(可获 2 积分)

它就像三人组中的Larry Fine。

他们去哪,他也在哪,但是他只是一个配角。

这不是说多态不伟大,只是说不OO语言不需要多态机制。

接口可以提供这些这些功能。不需要OO的其他支持。

而且接口没有数量的限制。

所以,不需要那么麻烦,我们可以对OO多态说拜拜,然后欢迎基于接口的多态机制。

破碎的承诺

OO早期的时候做出了很多承诺。这些承诺依然在被传递给坐在教室里,读着博士和上着网络课程的稚嫩的程序员。

第 21 段(可获 2 积分)

我花了好多年的时间才明白OO是怎么欺骗我的。当年的我真是太嫩太天真太容易相信别人了。

然后我就遍体鳞伤。

拜拜,面向对象编程。

那么,用什么技术编程?

你好,函数式编程。在过去的几年里合作是如此地愉快。

你要知道,我不是要凭着这一面之词对函数式编程做出什么保证。我想先尝试它然后再去相信它。

一朝被蛇咬,十年甚至永远怕井绳。

你懂的。

如果你喜欢这篇文章,点击下面的💚 ,这样其他人就能在medium.com上看到。

如果你想加入一个web开发社区,互助学习如何使用函数式编程语言Elm进行web应用开发,请查看我的Facebook群组,Learn Elm Programming https://www.facebook.com/groups/learnelm/

第 22 段(可获 2 积分)

文章评论