【问题标题】:Can interfaces evolve with time?接口可以随着时间发展吗?
【发布时间】:2015-12-26 09:03:23
【问题描述】:

从灵活性的角度来看,接口很棒。但是,如果一个接口被大量客户端使用。在保持旧方法完整的同时向接口添加新方法将破坏所有客户端的代码,因为新方法不会出现在客户端中。如下图:

public interface CustomInterface {

    public void method1();

}

public class CustomImplementation implements CustomInterface {

    @Override
    public void method1() {
        System.out.println("This is method1");
    }
}

如果在稍后的某个时间点,我们向此接口添加另一个方法,所有客户端的代码都会中断。

public interface CustomInterface {

    public void method1();

    public void method2();

}

为避免这种情况,我们必须在所有客户端的代码中显式实现新方法。

所以我认为接口和这个场景如下:

  1. 曾经编写过的界面就像在石头上雕刻一样。他们很少被认为,并且期望改变。如果他们这样做了,他们会付出巨大的代价(重写整个代码),程序员应该为此做好准备。
  2. 继续上面的观点,是否可以编写经得起时间考验的接口?
  3. 如何在您期望将来有更多功能的接口中处理这种情况?这就是预期约束所有客户的合同的变化。

编辑:Default 方法确实是对许多人在回答中提到的 Java 接口的一个很好的补充。但我的问题更多是在代码设计的背景下。以及如何强制客户端上的方法实现是接口的固有特征。但是接口和客户端之间的这种契约似乎很脆弱,因为功能最终会发展。

【问题讨论】:

  • 阅读建议:框架设计指南(它是 .net,但它也是 Java 程序员的好读物)有一节关于在类或接口之间进行选择。正如其他答案所述,他们也谈论使用类,因为它们比接口灵活得多,并且可以让您在不破坏客户端代码的情况下对其进行改进。

标签: java interface


【解决方案1】:

Java 8 中以接口中的默认方法的形式引入了该问题的一种解决方案。它允许在不破坏现有代码的情况下向现有 Java SE 接口添加新方法,因为它为所有新方法提供了默认实现。

例如,被广泛使用的Iterable接口(它是Collection接口的超级接口)添加了两个新的默认方法——default void forEach(Consumer<? super T> action)default Spliterator<T> spliterator()

【讨论】:

  • 好的,我会调查的。但是从设计的角度来看,在 Java 8 之前,接口是否应该是不可变的?也就是说,一旦与客户签订了合同,就不应更改。
【解决方案2】:
public interface CustomInterface {
    public void method1();
}

public interface CustomInterface2 extends CustomInterface {
    public void meathod2();
}

除了默认方法之外,您还可以使用继承属性,如上所示,新接口将具有所有以前的方法以及新方法,并在您需要的情况下使用此接口。

【讨论】:

  • 但这不是我想要的。因为使用上面的代码,客户端代码仍然会中断。
  • 因为方法 2 仍未在客户端代码中实现。
  • 好吧,在你的方法中,假设我们有 25 个实现 interface1 的类 A-Z,由于某种原因,你必须添加只能由 5 个类 A-E 使用的新的肉类,但不是其余的类。因此,通过您的方法,您必须在 A-Z 的所有类中实现肉类方法 2,没有选项。但是通过使用继承,您只能在所需的类中进行更改,而不是在其他类中进行更改。这样您最终可以节省大量时间。
【解决方案3】:

Java 8 引入了方法的默认实现。这些实现驻留在接口中。如果在一个已经被许多类实现的接口中创建了一个具有默认实现的新方法,则不需要修改所有类,只需要修改那些我们希望新定义的方法的实现与默认一个。

现在,较旧的 Java 版本呢?在这里,我们可以有另一个接口来扩展第一个接口。之后,我们要实现新声明的方法的类将被更改为实现新接口。如下图。

public interface IFirst {
    void method1();
}

public class ClassOne implements IFirst() {
    public void method1();
}

public class ClassTwo implements IFirst() {
    public void method1();
}

现在,我们希望声明 method2(),但它只能由 ClassOne 实现。

public interface ISecond extends iFirst {
    void method2();
}

public class ClassOne implements ISecond() {
    public void method1();
    public void method2();
}

public class ClassTwo implements IFirst() {
    public void method1();
}

这种方法在大多数情况下都可以,但它也有缺点。例如,我们想要 ClassTwo 的 method3()(并且只有那个)。我们需要一个新的接口IThird。如果稍后我们要添加应该由 ClassOne 和 ClassTwo 实现的 method4(),我们将需要修改(但不是 ClassThree,还有 implemented IFirst)我们需要同时更改 ISecondIThird .

在编程方面很少有“灵丹妙药”。在接口的情况下,最好不要改变它们。在现实生活中并非总是如此。这就是为什么建议接口只提供“契约”(必须具备的功能)并尽可能使用抽象类的原因。

【讨论】:

    【解决方案4】:

    未来的界面更改不应该破坏任何一直在工作的东西——如果是这样,那就是一个不同的界面。 (不过,它可能会弃用某些东西,并且在弃用后的整个周期内,可以接受抛出未实现的异常是可以接受的。)

    要向接口添加东西,最简洁的答案是从中派生一个新接口。这将允许使用实现新行为的对象以及期望旧行为的代码,同时让用户适当地声明和/或类型转换以访问新功能。这有点烦人,因为它可能需要 instanceof 测试,但它是最强大的方法,而且您会在许多行业标准中看到它。

    【讨论】:

      【解决方案5】:

      接口是开发人员和客户之间的合同,所以您是对的 - 它们是一成不变的,不应更改。因此,接口应该只公开(=需求)类绝对需要的基本功能。

      List 接口为例。 Java 中有许多列表的实现,其中许多随着时间的推移而发展(更好的底层算法、改进的内存存储),但列表的基本“概念”——添加项目、搜索项目、删除项目item - 不应该也永远不会改变。

      所以,对于您的问题:您可以使用抽象类,而不是编写类实现的接口。接口基本上是纯抽象类,从某种意义上说,它们不提供任何内置功能。但是,可以将新的非抽象方法添加到客户端不需要实现的抽象类中(覆盖)。

      以这个抽象类(=接口)为例:

      abstract class BaseQueue {
          abstract public Object pop();
          abstract public void push(Object o);
          abstract public int length();
          public void clearEven() {};
      }
      
      public class MyQueue extends BaseQueue {
          @Override
          public Object pop() { ... }
      
          ...
      }
      

      就像在接口中一样,每个扩展 BaseQueue 的类都在合同上绑定了实现抽象方法。但是clearEven() 方法不是抽象方法(并且已经带有一个空实现),因此客户端不必强制实现它,甚至不会使用它。

      这意味着您可以利用 Java 中抽象类的强大功能来创建非契约绑定方法。以后可以根据需要向基类添加其他方法,只要它们不是抽象方法即可。

      【讨论】:

      • 这相当于使用default,除了使用抽象类而不是接口由于Java 禁止多重继承而引入了一大堆令人头疼的问题。但我对此表示反对,因为 clearEven() 的空实现实际上违反了该方法的实际合同,特别是它“清除偶数”(无论可能是什么)。更好的方法是抛出 UnsupportedOperationException,但这仍然是一个糟糕的做法。 BaseQueue
      • @SáT 将继承限制为单一类型是 OO 编程中非常合理且有用的设计原则。所以我倾向于不同意抽象类仅仅因为它们不支持多重继承而难以实现。您正在比较接口和抽象类,这就是为什么您发现抽象类不如接口灵活的原因。但两者在代码架构上的用途非常不同,不应该进行比较。虽然我同意你的第二点和第三点。
      【解决方案6】:

      我认为您的问题更多是关于设计和技术,所以 java8 的答案有点误导。这个问题早在 java8 之前就已经知道了,所以还有一些其他的解决方案。

      首先,解决问题没有绝对免费的方法。界面演变带来的不便的大小取决于库的使用方式以及您的设计的深思熟虑。

      1) 如果您设计了一个界面并忘记在其中包含强制方法,那么任何技术都无济于事。更好地规划您的设计,并尝试预测客户将如何使用您的界面。
      示例:想象Machine 接口具有turnOn() 方法但错过了turnOff() 方法。在 java8 中引入一个默认为空实现的新方法将防止编译错误,但不会真正有帮助,因为调用一个方法将没有任何效果。提供工作实现有时是不可能的,因为接口没有字段和状态。

      2) 不同的实现通常有共同点。不要害怕在父类中保留通用逻辑。从这个父类继承你的库类。这将强制库客户端也从您的父类继承他们自己的实现。现在,您可以在不破坏所有内容的情况下对界面进行小幅更改。
      示例:您决定在界面中包含 isTurnedOn() 方法。使用基本类,您可以编写有意义的默认方法实现。不是从父类继承的类仍然需要提供自己的方法实现,但是由于方法不是强制性的,所以对他们来说很容易。

      3) 升级功能通常是通过扩展接口来实现的。没有理由强制库客户端实现一堆新方法,因为它们可能不需要它们。
      示例:您决定将stayIdle() 方法添加到您的界面。它适用于您的库中的类,但不适用于自定义客户端类。由于此功能是新功能,因此最好创建一个扩展 Machine 的新接口,并在需要时使用它。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-09-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-11-02
        • 2018-12-01
        相关资源
        最近更新 更多