电脑故障

位置:IT落伍者 >> 电脑故障 >> 浏览文章

为什么extends是有害的(一)


发布日期:2020/12/10
 

概述

大多数好的设计者象躲避瘟疫一样来避免使用实现继承(extends 关系)%的代码应该完全用interfaces写不用具体的基类事实上四人帮的设计模式的书大量的关于怎样用interface继承代替实现继承这个文章描述设计者为什么有这样的怪癖的想法

Extends是有害的也许对于Charles Manson这个级别的不是但是足够糟糕的它应该在任何的可能的时候被避开四人帮的设计模式花了很大的部分讨论用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 )

{

listadd( );

g( list )

}

[/代码]

现在假设一个对于快速查询的需求被提出以至于这个LinkedList不能够解决你需要用HashSet来代替它在已有代码中变化不能够局部化因为你不仅仅需要修改f()也需要修改g()(它带有LinkedList参数)并且还有g()把列表传递给的任何代码

象下面这样重写代码:

[/代码]

f()

{ Collection list = new LinkedList();

//

g( list );

}

g( Collection list )

{

listadd( );

g( list )

}

[/代码]

这样修改Linked list成hash可能只是简单的用new HashSet()代替new LinkedList()就这样没有其他的需要修改的地方

作为另一个例子比较下面两段代码

[/代码]

f()

{ Collection c = new HashSet();

//

g( c );

}

g( Collection c )

{

for( Iterator i = erator(); ihasNext() )

do_something_with( inext() );

}

[/代码]

[/代码]

f()

{ Collection c = new HashSet();

//

g( erator() );

}

g( Iterator i )

{ while( ihasNext() )

do_something_with( inext() );

}

[/代码]

g()方法现在能够遍历Collection的派生就像你能够从Map中得到的键值对事实上你能够写iterator它产生数据代替遍历一个Collection你能够写iterator它从测试的框架或者文件中得到信息这会有巨大的灵活性

耦合

对于实现继承一个更加关键的问题是耦合令人烦躁的依赖就是那种程序的一部分对于另一部分的依赖全局变量提供经典的例子证明为什么强耦合会引起麻烦例如如果你改变全局变量的类型那么所有用到这个变量的函数也许都被影响所以所有这些代码都要被检查变更和重新测试而且所有用到这个变量的函数通过这个变量相互耦合也就是如果一个变量值在难以使用的时候被改变一个函数也许就不正确的影响了另一个函数的行为这个问题显着的隐藏于多线程的程序

作为一个设计者你应该努力最小化耦合关系你不能一并消除耦合因为从一个类的对象到另一个类的对象的方法调用是一个松耦合的形式你不可能有一个程序它没有任何的耦合然而你能够通过遵守OO规则最小化一定的耦合(最重要的是一个对象的实现应该完全隐藏于使用他的对象)例如一个对象的实例变量(不是常量的成员域)应该总是private我意思是某段时期的无例外的不断的(你能够偶尔有效地使用protected方法但是protected实例变量是可憎的事)同样的原因你应该不用get/set函数他们对于是一个域公用只是使人感到过于复杂的方式(尽管返回修饰的对象而不是基本类型值的访问函数是在某些情况下是由原因的那种情况下返回的对象类是一个在设计时的关键抽象)

这里我不是书生气在我自己的工作中我发现一个直接的相互关系在我OO方法的严格之间快速代码开发和容易的代码实现无论什么时候我违反中心的OO原则如实现隐藏我结果重写那个代码(一般因为代码是不可调试的)我没有时间重写代码所以我遵循那些规则我关心的完全实用—我对干净的原因没有兴趣

脆弱的基类问题

现在让我们应用耦合的概念到继承在一个用extends的继承实现系统中派生类是非常紧密的和基类耦合当且这种紧密的连接是不期望的设计者已经应用了绰号脆弱的基类问题去描述这个行为基础类被认为是脆弱的是因为你在看起来安全的情况下修改基类但是当从派生类继承时新的行为也许引起派生类出现功能紊乱你不能通过简单的在隔离下检查基类的方法来分辨基类的变化是安全的而是你也必须看(和测试)所有派生类而且你必须检查所有的代码它们也用在基类和派生类对象中因为这个代码也许被新的行为所打破一个对于基础类的简单变化可能导致整个程序不可操作

让我们一起检查脆弱的基类和基类耦合的问题下面的类extends了Java的ArrayList类去使它像一个stack来运转

[/代码]

class Stack extends ArrayList

{ private int stack_pointer = ;

public void push( Object article )

{ add( stack_pointer++ article );

}

public Object pop()

{ return remove( stack_pointer );

}

public void push_many( Object[] articles )

{ for( int i = ; i < articles.length; ++i )

push( articles[i] );

}

}

[/代码]

甚至一个象这样简单的类也有问题。Tw.wiNGwIT.Com思考当一个用户平衡继承和用ArrayList的clear()方法去弹出堆栈时:

[/代码]

Stack a_stack = new Stack();

a_stack.push("1");

a_stack.push("2");

a_stack.clear();

[/代码]

这个代码成功编译,但是因为基类不知道关于stack指针堆栈的情况,这个stack对象当前在一个未定义的状态。下一个对于push()调用把新的项放入索引2的位置。(stack_pointer的当前值),所以stack有效地有三个元素-下边两个是垃圾。(Java的stack类正是有这个问题,不要用它).

对这个令人讨厌的继承的方法问题的解决办法是为Stack覆盖所有的ArrayList方法,那能够修改数组的状态,所以覆盖正确的操作Stack指针或者抛出一个例外。(removeRange()方法对于抛出一个例外一个好的候选方法)。

这个方法有两个缺点。第一,如果你覆盖了所有的东西,这个基类应该真正的是一个interface,而不是一个class。如果你不用任何继承方法,在实现继承中就没有这一点。第二,更重要的是,你不能够让一个stack支持所有的ArrayList方法。例如,令人烦恼的removeRange()没有什么作用。唯一实现无用方法的合理的途径是使它抛出一个例外,因为它应该永远不被调用。这个方法有效的把编译错误成为运行错误。不好的方法是,如果方法只是不被定义,编译器会输出一个方法未找到的错误。如果方法存在,但是抛出一个例外,你只有在程序真正的运行时,你才能够发现调用错误。

对于这个基类问题的一个更好的解决办法是封装数据结构代替用继承。这是新的和改进的Stack的版本:

[/代码]

class Stack

{

private int stack_pointer = 0;

private ArrayList the_data = new ArrayList();

public void push( Object article )

{

the_data.add( stack_poniter++, article );

}

public Object pop()

{

return the_data.remove( --stack_pointer );

}

public void push_many( Object[] articles )

{

for( int i = 0; i < o.length; ++i )

push( articles[i] );

}

}

上一篇:Sun StorEdgeTM的瞬时映像功能

下一篇:pureQuery带注释的方法风格简介