Java在Interface方面的缺陷 2004-10-31

来源:互联网 发布:知乎账号解封 编辑:程序博客网 时间:2024/06/11 04:17

OO设计中有一个重要原则:结构(mechanism)与策略(Policy)分离。Interface是实践这一原则的重要途径。

从概念上将,Interface用来扮演使用者与实现者之间的契约(contract)。我们使用Interface来定义一组Abstract Operation(method)的集合,用实现类实现这些Interface,调用者通过访问这些Interface的实现类所提供的相应服务。调用者并不关心接口是由谁实现的,它看到的只有Interface本身。

                         Contract
|-----------| Realize |-------------|       
|  Class A  |-------->| Interface A | Access |------|
|-----------|         |-------------|<-------|Caller|
                      |+Operation1()|        |------|
|-----------| Realize |+Operation2()|
|  Class B  |-------->|-------------|
|-----------|

在定义Interface Specification的时候一定要基于上述Interface的意义来进行。而Java的Interface设计就存在着缺陷。

在Java中,一个Class可以从另外一个Class继承,同时可以实现多个Interface。所以,一个Method既可以从base class那里继承过来,也可以是要实现的Interface Method。而Java的Method Signature由Method Name和Parameter List组成。这就意味着Java无从区分来自于Class继承体系的method和Interface实现体系的method。这会造成很大的困惑。

例如,我们从Vendor A和Vendor B处各购买了一套Java包,我们不妨称之为Package A和Package B。在Package A中,有一套Container的继承

体系,在Package B中有一套通过实现IContainer构成的一套Container体系。如下图所示:

现在我们想增加一个新的容器类PowerVector,对于它,我们即想让其相容与Package A的Container架构,又想让其相容于Package B的Container体系。于是,我们让PowerVector从Vector类继承,同时让其实现IContainer。正如图中所画的那样。

这是一个再自然不过的方法。但问题是,PowerVector从Vector(Container)继承了size方法,同时要实现IContainer的size方法。这个来自两个供应商的两套体系价构的方法在Java中有着完全相同的签名,但却包含完全不同的含义。在Package A的Container体系中,size方法用来查询Container的容量(capability);在Package B中,size方法却被用来计算Container所存放的Item的数量。

而使用Java,由于size方法的签名完全相同,所以在PowerVector中,你只有两种选择,继承或改写(override)Vector的size方法。如果是继承,那么通过IContainer的size方法就得到了错误的结果。如果为了迎合IContainer而改写,则通过Package A的Container体系来访问size方法时,同样是错误的。

你或许会说:这个很简单,把其中一套体系的size方法改变签名不就行了吗?这是一个没有动脑的建议。想想看,由于Package A和Package B来自于两个不同的供应商,它们在开发各自的Package时,根本不会考虑别人是否会如何命名。而在自己的体系中,已经有大量的类在使用自己体系的定义。修改任何一个都会很麻烦。更何况,你拿到的很有可能是编译后的包,根本就无从改起。

如果你是个Java高手,你或许可以通过一些特殊的架构或方法解决掉这个问题。但那些方法都不是最自然的方法。而一个设计充分的语言,应该让设计师或程序员能够用最自然的方法表现自己的意图。

我们上面所给出的例子并不是一个特殊的情况。而是一种经常会出现的case。只所以Java设计出现了这样的问题,是因为Java语言的设计师根本没有深刻理解在这一问题上的OO思想。

这是因为,虽然Java仅仅允许但继承,但却允许实现多个接口,那么一个Class所拥有的Operation(Method)就会来自于多条线。而每一条线都有可能由不同的人,不同的公司来设计,虽然对于Class或Interface的签名,由于Sun建议了Package的命名规则,所以,可以避免签名冲突,但对于method的签名,按照Java的method signature规则,产生冲突的可能性是很大的。而相同签名的method,很有可能具有很大差异的含义。

从本质上说,当如果一个Class继承了另外一个Class,同时实现了一个或多个Interface,或者,仅仅实现了多个接口时,如果这些Interface和Class具有相同签名的method(按照Java的签名原则),这些method应该被看作具有不同签名的method。应该对其分别看待。比如:

// 下面的代码即不是Java code也不是C++ code,而是一种伪code
public class Base{
  public int foo() { return 0; }
}

public interface IFoo{
  int foo();
}

public class Derived extends Base implements IFoo
{
  // 还存在着一个继承自Base的foo method
  int IFoo::foo() { return 10; } //需要指明这是IFoo的foo mothed的实现
}

这样,如果我们有这样一段code,

Derived derived = new Derived;
derived.foo(); // 调用的是Base::foo
IFoo ifoo = (IFoo)derived;
ifoo.foo(); // 调用的是Base::IFoo::foo

另外一个例子:
public interface IFoo1{
  int foo();
}

public interface IFoo2{
  int foo();
}

public class Foo implements IFoo1, IFoo2
{
  //需要分别指明IFoo1和IFoo2的foo mothed实现
  int IFoo1::foo() { return 0; }
  int IFoo2::foo() { return 10; }
}

Foo obj = new Foo;
IFoo1 foo1 = (IFoo1)obj;
foo1.foo(); // 调用的是Foo::IFoo1::foo
IFoo2 foo2 = (IFoo1)obj;
foo2.foo(); // 调用的是Foo::IFoo2::foo

对于这种解决方案,随之产生的一个问题是,如果我们在一个class内实现了一个Interface,而我们基于这个class的引用来访问这个class的instance时,我们如何访问这些interface methods?

我对答案就是:不允许访问,因为,这些方法的存在是因为这个class实现了方法所在的接口,这些方法的实现定义了契约履行的行为,也就是说他们是契约被执行过程的一部分。既然接口是调用者和服务提供者之间的契约,那么访问它们的途径当然也应该通过接口。这是完全符合Interface的语义的。如果你真想访问这些interface method,可以把它们包装为class method。

由于接口允许扩展其它接口,就会产生这样的问题:

public interface IFooBase{
  int foo();
}

public interface IFoo1 extends IFooBase{
  void draw();
}

public interface IFoo2 extends IFooBase{
  void move();
}

public class Foo implements IFoo1, IFoo2 { ... }

这种情况下,IFoo1和IFoo2都从IFooBase那继承了foo方法,如果我们在class Foo中分别为这两个foo进行实现,就会存在问题,比如:

Foo obj = new Foo;
IFooBase foo0 = (IFoo1) obj;
foo0.foo(); //???到底调用的哪个实现??

所以,对于这种情况,我们必须规定,如果两个或多个接口都扩展了某个接口,并且随后这些接口被同一个Class实现的时候,你不能分别为其实现,而是要直接指定base interface的方法实现。以上例为例,其实现应该为:

public class Foo implements IFoo1, IFoo2 {
  int IFooBase::foo() { return 0; }
  void IFoo1::draw() { ... }
  void IFoo2::move() { ... }
}

对于这种情况,还有一种表现形式:

public class Base implements IFoo1{
  int IFoo1::foo() { return 0; } // 这里,我们只需要指定IFoo1即可
  void IFoo1::draw() { ... }
}

public class Derived extends Base implements IFoo2{
  void IFoo2::move() { ... }
  // 对于IFoo2::foo(),由于在Base中已经实现,
  // 我们可以继承其实现,也可以改写它。
  // 但在改写的时候,我们使用什么名字呢?
  // IFoo1::foo()? IFoo2::foo()? 还是IFooBase::foo()
}

这种情况下,随意指定范围会引起很大的困惑。为了避免这种问题,我们可以做出这样的规定:当我们在实现一个接口的时候,对于每一个method,我们都指定其定义所在的Interface,而不是扩展后的Interface。按照这个规定,我们上例的实现应该为:

public class Base implements IFoo1{
  int IFooBase::foo() { return 0; } // 这里,我们必须指定IFooBase
  void IFoo1::draw() { ... }
}

public class Derived extends Base implements IFoo2{
  void IFoo2::move() { ... }
  // 对于IFooBase::foo(),由于在Base中已经实现,
  // 我们可以继承其实现,也可以改写它。
  // 但在改写的时候,我们仍然必须使用IFooBase::foo()
}

上述的一切就形成了一个完整的Interface解决方案,按照这种方案,设计师及程序员就可以自然、直观、正确的使用接口进行OO的架构设计。同时也期望Java能够在未来的版本中解决掉这个缺陷。