为什么说Java中继承是有害的 |
|||||
大多数好的设计者象逃避瘟疫一样来幸免 使用实现继承(extends 关系) 。实际上80%的代码应该 彻底用interfaces写,而不是通过extends 。“JAVA设计模式”一书 详尽 阐述了 怎么用接口继承 接替实现继承 。这篇文章 形容设计者为何会这么作 。 Extends是有害的; 兴许关于Charles Manson这个级别的不是,然而足够 蹩脚的它应该在任何可能的时候被避开 。“JAVA设计模式”一书花了很大的 部分 探讨用interface继承 接替实现继承 。 好的设计者在他的代码中,大 部分用interface,而不是具体的基类 。本文 探讨为何设计者会这样 取舍,而且也介绍一些基于interface的编程 根底 。 接口(Interface)和类(Class)? 一次,我 加入一个Java消费者组的会议 。在会议中,Jams Gosling(Java之父)做 发动人讲话 。在那令人难忘的Q&A 部分,有人问他:“假如你再一次 构造Java,你想转变什么?” 。“我想放弃classes”他 答复 。在笑声平息后,它解释说,真正的问题不是由于class 本身,而是实现继承(extends 关系) 。接口继承(implements关系)是更好的 。你应该尽可能的幸免实现继承 。 失去了灵便性 为何你应该幸免实现继承呢?第一个问题是明确的 运 器具体类名将你固定到特定的实现,给底层的转变添加了 毋庸要的 困苦 。 在目前的 麻利编程 步骤中,核心是并行的设计和开发的概念 。在你 详尽设计程序前,你开始编程 。这个技术不同于传统 步骤的 模式----传统的 模式是设计应该在编码开始前 实现----然而许多 顺利的 名目已经 证实你 可以更 快捷的开发高 品质代码, 有关于传统的 循序渐进的 步骤 。然而在并行开发的核心是主张灵便性 。你只能以某一种 模式写你的代码 甚至于最新发现的需要 可以尽可能没有 苦楚的合并到已有的代码中 。 胜于实现你 兴许需要的 特色,你 惟独实现你明确需要的 特色,而且适度的对 变迁的 包容 。假如你没有这种灵便,并行的开发,那 几乎不可能 。 关于Inteface的编程是灵便 构造的核心 。为了 注明为何,让我们看一下当 使用它们的时候,会 产生什么 。考量下面的代码:f() { LinkedList list = new LinkedList(); //... g( list ); } g( LinkedList list ) { list.add( ... ); g2( list ) } 现在, 假如一个关于 快捷 查问的需要被提出, 甚至于这个LinkedList不 可以解决 。你需要用HashSet来 接替它 。在已有代码中, 变迁不 可以 部分化,由于你不只仅需要 批改f()也需要 批改g()(它带有LinkedList参数),而且还有g()把列表传递给的任何代码 。象下面这样重写代码:
这样 批改Linked list成hash,可能只不过 方便的用new HashSet() 接替new LinkedList() 。就这样 。没有 其余的需要 批改的地方 。 作为另一个例子,
比较下面两段代码:
和
g2() 步骤现在 可以遍历Collection的派生,就像你 可以从Map中得到的键值对 。事实上,你 可以写iterator,它产生数据, 接替遍历一个Collection 。你 可以写iterator,它从测试的框架或者文件中得到信息 。这会有 硕大的灵便性 。 耦合 关于实现继承,一个更加 要害的问题是耦合---令人 烦躁的依赖,便是那种程序的一 部分关于另一 部分的依赖 。全局变量提供经典的例子, 证实为何强耦合会引起麻烦 。例如,假如你转变全局变量的类型,那么全部用到这个变量的函数 兴许都被影响,所以全部这些代码都要被 审查,变更和再一次测试 。而且,全部用到这个变量的函数通过这个变量 彼此耦合 。也便是,假如一个变量值在难以 使用的时候被转变,一个函数 兴许就不正确的影响了另一个函数的行为 。这个问题卓著的 潜藏于多线程的程序 。 作为一个设计者,你应该 奋力最小化耦合关系 。你不能一并 肃清耦合,由于从一个类的对象到另一个类的对象的 步骤调用是一个松耦合的 模式 。你不可能有一个程序,它没有任何的耦合 。然而,你 可以通过 恪守OO 规定,最小化 定然的耦合(最主要的是,一个对象的实现应该 彻底 潜藏于 使用他的对象) 。例如,一个对象的实例变量(不是常量的成员域),应该总是private 。我意思是某段 时代的,无例外的,不停的 。(你 可以间或有效地 使用protected 步骤,然而protected实例变量是可憎的事)同样的缘由你应该不用get/set函数---他们关于是一个域公用只不过使人感到过于复杂的 模式( 只管返回 润饰的对象而不是 根本类型值的 拜访函数是在某些状况下是由缘由的,那种状况下,返回的对象类是一个在设计时的 要害 形象) 。 这里,我不是书 怄气 。在我自己的工作中,我发现一个直接的 彼此关系在我OO 步骤的严格中间, 快捷代码开发和方便的代码实现 。无论什么时候我违反 核心的OO 准则,如实现 潜藏,我 后果重写那个代码(普通由于代码是不可调试的) 。我没有 工夫重写代码,所以我遵照那些 规定 。我关怀的 彻底有用?我对 清洁的缘由没感兴趣 。 懦弱的基类问题 现在,让我们 利用耦合的概念到继承 。在一个用extends的继承实现系统中,派生类是十分密切的和基类耦合,当且这种密切的衔接是不 期冀的 。设计者已经 利用了外号“ 懦弱的基类问题”去 形容这个行为 。 根底类被认为是 懦弱的是,由于你在看起来安全的状况下 批改基类,然而当从派生类继承时,新的行为 兴许引起派生类浮现 性能 混乱 。你不能通过 方便的在隔离下 审查基类的 步骤来分辩基类的 变迁是安全的;而是你也必须看(和测试)全部派生类 。而且,你必须 审查全部的代码,它们也用在基类和派生类对象中,由于这个代码 兴许被新的行为所 攻破 。一个关于 根底类的 方便 变迁可能招致整个程序不可操作 。 让我们一同 审查 懦弱的基类和基类耦合的问题 。下面的类extends了Java的ArrayList类去使它像一个stack来 运行:
甚至一个象这样 方便的类也有问题 。思量当一个消费者 均衡继承和用ArrayList的clear() 步骤去弹出堆栈时:
这个代码 顺利编译,然而由于基类不晓得关于stack指针堆栈的状况,这个stack对象目前在一个未定义的状态 。下一个关于push()调用把新的项放入索引2的位置 。(stack_pointer的目前值),所以stack有效地有三个元素-下边两个是垃圾 。(Java的stack类正是有这个问题,不要用它). 对这个令人 讨厌的继承的 步骤问题的解决 步骤是为Stack 遮蔽全部的ArrayList 步骤,那 可以 批改数组的状态,所以 遮蔽正确的操作Stack指针或者抛出一个例外 。(removeRange() 步骤关于抛出一个例外一个好的候选 步骤) 。 这个 步骤有两个缺陷 。第一,假如你 遮蔽了全部的东西,这个基类应该真正的是一个interface,而不是一个class 。假如你不用任何继承 步骤,在实现继承中就没有这丝毫 。第二,更主要的是,你不 可以让一个stack 支撑全部的ArrayList 步骤 。例如,令人 郁闷的removeRange()没有什么作用 。唯一实现无用 步骤的 正当的 路径是使它抛出一个例外,由于它应该永远不被调用 。这个 步骤有效的把编译 舛误成为运行 舛误 。不好的 步骤是,假如 步骤只不过不被定义,编译器会输出一个 步骤未找到的 舛误 。假如 步骤存在,然而抛出一个例外,你惟独在程序真正的运行时,你 威力够发现调用 舛误 。
|