【译】再见,面向对象编程(一)

栏目: 后端 · 前端 · 发布时间: 7年前

内容简介:Charles Scalfani原文:我使用面向对象语言编程已经十几年了。我是用的第一个OO语言是C++,然后是Smalltak,最后是.NET和Java。

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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Go Web 编程

Go Web 编程

[新加坡]Sau Sheong Chang(郑兆雄) / 黄健宏 / 人民邮电出版社 / 2017-11-22 / 79

《Go Web 编程》原名《Go Web Programming》,原书由新加坡开发者郑兆雄(Sau Sheong Chang)创作、 Manning 出版社出版,人名邮电出版社引进了该书的中文版权,并将其交由黄健宏进行翻译。 《Go Web 编程》一书围绕一个网络论坛 作为例子,教授读者如何使用请求处理器、多路复用器、模板引擎、存储系统等核心组件去构建一个 Go Web 应用,然后在该应用......一起来看看 《Go Web 编程》 这本书的介绍吧!

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具