再见,面向对象编程

我从事面向对象语言的编程已有数十年了。我使用的第一种面向对象语言是 C++,然后是 Smalltalk,最后是 .NET 和 Java。

我曾狂热地利用 继承封装多态 的好处。即范式的三个支柱。

我也曾渴望获得复用的承诺,并借用前人在这个激动人心的新景观中所获得的智慧。
一想到要将现实世界的对象映射到它们的类中,我就按捺不住激动的心情,希望整个世界都井然有序。

我大错特错了。

继承,第一个倒下的支柱

乍一看,继承似乎是面向对象范式的最大好处。对于灌输这个概念的新人来说,所有简单的形状层次结构示例似乎都有逻辑意义。

重用是当今的热门话题。不……它将永垂青史。

我接受了这一切,并带着我的新发现冲向世界。

香蕉猴丛林问题

出于宗教信仰和解决问题的需要,我开始构建类层次结构并编写代码。一切都对了。

我永远不会忘记那一天,我准备通过继承现有类来兑现重用的承诺。这就是我一直在等待的时刻。

一个新项目出现了,我回想起我上一个项目中非常喜欢的那个类。

没问题。重新使用再抢救一下。我要做的就是从另一个项目中获取该类并使用它。

好吧……实际上……不只是那个类。我们也需要它的父类。但是……仅此而已。

嗯...等等...看来我们也将需要父类的父类...然后...我们将需要所有父类。好吧好吧我会处理的没问题。

太好了现在它将无法编译。为什么??哦,我明白了...... This 对象包含 the other 对象。没问题。
等等...我不仅仅需要 that 对象。我需要 that 的父对象及其父对象的父对象,依此类推,包括每个包含的对象以及包含对象的所有父对象以及其父对象,父对象,父对象……

啊。

Erlang 的创建者 Joe Armstrong 引用了一句很棒的话:

面向对象语言的问题在于,它们具有随身携带的所有隐式环境。您想要香蕉,但是得到的是一只大猩猩,拿着香蕉和整个森林。

香蕉猴丛林解决方案

对于这个问题我可以通过创建简单点的层次结构来解决。但是,如果继承是重用的关键,那么我对该机制的任何限制肯定会限制重用的好处对吧?

毫无疑问,是的。

那么,一个糟糕的面向对象程序员经过 Kool-adi 的帮助后会怎么去做呢?

Contain and Delegate。稍后将对此进行详细说明。

钻石问题(菱形继承问题)

迟早,以下问题将变得很难解决,并且根据语言的不同,还会出现无法解决的问题。

即使这种做法似乎合乎逻辑,但大多数 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 函数?ScannerPrinter ?不可能两者都是。

钻石解决方案

解决方案很简单。但是不要那样做

是的,这是对的。大多数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

这个问题是继承支柱中的另一个难题。

脆弱的基类问题

因此,我正在简化层次结构,以防止其具有周期性。我没有钻石。

世界上的一切都是正确的。直到...

有一天,我的代码可以工作,而第二天它将停止工作。这是意外。 我没有更改代码

好吧,也许是个错误...但是等等... 某些事情确实发生了变化 ......

但这不是我的代码。原来更改是在我继承的类中进行的。
基类的更改如何破坏我的代码?

额...

想象一下以下基类(它是用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]); // 此行将被更改
  }

重点来了:请注意代码的注释行。稍后将更改此行,这将使事情中断。

此类在其接口上具有2个方法,add()addAll()add() 函数将添加单个元素,而 addAll() 将通过调用add方法 添加多个元素。

这是派生类:

public class ArrayCount extends Array
{
  private int count = 0;

  [@Override](http://twitter.com/Override)
  public void add(Object element)
  {
    super.add(element);
    ++count;
  }

  [@Override](http://twitter.com/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]); //此行已更改
}

就基类的所有者而言,它仍然按照通告的方式运行。
 自动化测试仍然通过

就基类的所有者而言,它仍然按照通告的方式运行。并且派生类的所有者被粗暴的唤醒。(译者: 没太理解, 请帮忙修正)

现在 ArrayCount 类中的 addAll() 方法调用其父类的 addAll() 方法 , 父类 内部 调用已被 派生 类 的 add() 方法 重写

这导致每次调用 派生 类的 add() 方法时 count 都会增加,
然后 派生 类中的 addAll() 方法 再次增加 了元素数。

计算了两次。

如果有可能的话,派生类的作者必须知道如何实现基类。并且必须通知他们有关基类的每项更改,因为它可能以不可预测的方式破坏其派生类。

啊!这个巨大的裂缝永远威胁着宝贵的继承支柱的稳定性。

脆弱的基类解决方案

再次包含并委托救援。

通过使用 Contain 和 Delegate,我们从白盒编程转到黑盒编程。使用白盒编程,我们必须查看基类的实现。

使用黑盒编程,我们可以完全不了解实现,因为我们不能通过重写基类的某个函数将代码注入基类。我们只需关注接口。

这个趋势令人不安...

继承被认为是重用的巨大胜利。

面向对象的语言不能使Contain和Delegate易于实现。它们的设计目的是让继承变得容易。

如果你像我一样,你就开始怀疑这种继承的设计了。但更重要的是,这应该动摇您对通过层次进行分类的能力的自信心了。

层次结构问题

每次换一家新公司时,我都会在创建放置公司文档的目录(例如员工手册)遇到难题。

我是应该创建一个 Documents 的目录,然后再在里面创建一个 Company 的目录?

还是先创建一个 Company 的目录,再在里面创建一个 Documents 目录呢?

两者都可以。但是哪个是对的?哪个最好?

类别层次结构的概念是存在更通用的基类(父类),而派生类(子类)是这些类的更专业版本。当我们沿着继承链往下走的时候,我们会变得更加专门化。(请参阅上面的形状层次结构)。

但如果父对象和子对象可以任意切换位置,那么该模型显然有问题。

层次结构解决方案

问题是.....

类别层次结构不工作.

那么层次结构有什么好处?

包容.

如果你查看现实世界,你会看到到处都是包容(或独占所有权)层次结构。

你将找不到分类层次结构。面向对象的范式是基于现实世界的,现实世界充满了对象。但后来它使用了一个破碎的模型,即类别层次结构,其中没有真实世界的类比。

但现实世界充满了包容层次结构。包含层次结构的一个很好的例子就是 SOCKS。它们位于袜子抽屉中,该抽屉包含在梳妆台的一个抽屉中,该抽屉包含在卧室中,该抽屉包含在房间中,等等。

硬盘驱动器上的目录是包容层次结构的另一个示例。它们包含文件。

那么我们如何分类呢?

如果你想到公司文档,我把它们放在哪里几乎无关紧要。我可以将它们放在 Documents 文件夹或名为 Stuff 的文件夹中。

利用标记对其进行分类。可以使用以下 tags:

Document
Company
Handbook

Tags 标记文件,没有顺序或层次结构。(这也解决了菱形问题。)

标签类似于接口,这样可以将多种类型与文档关联。

但有这么多裂缝,看起来继承支柱已经倒下。

再见,继承。

封装,第二个倒下的支柱

Laravel

乍一看,封装似乎是面向对象编程的第二大优点。

保护对象状态变量不受外部访问,即将他们封装在对象中。

我们不再需要担心谁知道谁在访问全局变量。

封装对于变量是安全的。

封装这个事情是不可思议的!!

封装万岁。。。

直到。。。

参考问题

为了提高效率,对象不是通过它的值而是通过引用传递给函数。

这意味着函数不会传递对象,而是传递一个指向对象的reference或指针

如果将对象通过引用传递给对象构造函数,则构造函数可以将该对象引用放入受封装保护的私有变量中。

但传递的对象不安全

为什么呢?因为其他一些代码具有指向对象的指针,即,调用构造函数的代码。它必须具有对对象的引用,否则无法将其传递给构造函数?

参考解决方案

构造函数必须克隆传入的对象。而且不是浅层克隆,而是深层克隆,即传入的对象中包含的每个对象以及这些对象中的每个对象,以此类推。

效率非常重要。

也有例外。并非所有的对象都能克隆。有些具有与之相关的操作系统资源,从而使克隆在最佳或最坏的情况下也无法进行克隆。

每一个 主流的 面向对象语言 都存在这个问题。

再见,封装。

多态性,倒下的第三个支柱

多态是面向对象三位一体的红发继子。

有点像是这个群体的 Larry Fine 。

他们所到之处他都在,但他只是个配角。

并不是说多态性不好,而是您不需要面向对象的语言就可以做到这一点。

接口将会提供这一点。而且没有了面向对象的所有包袱。

并且有了接口,您混入多少种不同的行为都不会有任何的限制。

因此,事不宜迟,告别 面向对象的 多态,迎接 基于接口的 多态。

失信

嗯,OO在早期肯定承诺了很多。这些承诺仍然是对坐在教室里、阅读博客和参加在线课程的天真程序员做出的。

我花了很多年才意识到OO是怎么骗我的。我也是睁大眼睛,缺乏经验,轻信别人。

我被烧死了。

再见,面向对象编程。

那又怎样?

你好哇,函数式编程。过去几年里和你一起合作真实太棒了。

但请注意,我不会把你的任何承诺当真。我要亲眼看到才能相信。

一朝被蛇咬,十年怕井绳。

你懂的。

如果你喜欢这篇文章,点击下方的 💚,这样其他人也能在 Medium 看见它。

如果您想加入一个 Web 开发者社区,学习并帮助彼此使用 Elm 中的函数编程开发 Web 应用,请查看我的 Facebook 群组,Learn Elm Programming https://www.facebook.com/groups/learnelm/

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.com/@cscalfani/goodbye-ob...

译文地址:https://learnku.com/laravel/t/38463

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 3

:dog: 嘤嘤嘤,这篇文章着实比较难翻译,比较偏口语化。原文的排版要比译文的好一点,后来发现原因在于译文的对照原文与实际的原文排版不太一样呀,不知道是哪方面失误了,最终导致整篇译文的排版有点不太统一。(不过还是要感谢一下辛苦的站长和管理员啦~)

英文水平有限(比如我 :sob:)或者只能不科学地上网的同学只能多看几遍译文了,有条件可以去看看原文,发布于 2016 年。ps. 感觉 Medium 有点像国外的知乎。

2个月前 评论
Summer 2个月前
Complicated

其实我感觉会出现文章中说的“继承的森林”问题,是设计的不够合理!但是!但是!但是!不能不说,,没有一种设计是能适应所有项目(场景的),所有我感觉还是适度的用 继承 就好了,别想着哪个基类 是万能的!

2个月前 评论
Complicated

个人拙见,不喜轻喷

2个月前 评论

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!