【问题标题】:Mandatory class recompilation when dependency changes依赖项更改时强制类重新编译
【发布时间】:2019-05-03 16:28:01
【问题描述】:

上下文:

我是一名 Java 程序员,正在阅读 Bob 大叔敏捷软件开发。关于 ISP 接口隔离原则,有一个我理解为的论点:

让我们拥有:

interface Service {
  function doA();
}
class ServiceImpl implements Service {...}
class ServiceClient {
  // ServiceImpl is injected; eg either through constructor or setter
  private Service service;
  function useOnlyDoA() {
    service.doA();
  }
}

现在,如果接口服务发生变化,例如添加方法doB(),然后添加所有依赖类,例如ServiceClient 必须重新编译,即使他们不使用添加的方法! (真的吗??)

  1. Java 是这样吗?
  2. c++ 是这样吗?
  3. 对于其他哪些语言是正确的,对于哪些不是?

我坚信关于 java 如果ServiceClient 在包中,例如client.jar,接口Serviceservice.jarServiceImplimpl.jar,那么client.jar如果不使用Service接口的新方法就不用重新编译重建,可以使用以及service.jarimpl.jar 的新版本。在我看来,事情在java中是这样发展的。 它是其他语言,例如 c++ 还是其他一些语言?

可能在 c++ 中更多的东西被破坏了,例如 https://stackoverflow.com/a/4033900/1423685

说清楚:

我不是要重新编译实现更改接口的类(这很明显,它的类必须实现新添加的方法)。 但是我问我是否必须重新编译使用这个接口的类 ServiceClient,即使类没有使用新添加的方法。可能在 c++ BC 中发生更改,并且客户端确实必须重新编译,但在我看来,在 java 中不是。

编辑:

我在 java 中实现了一个测试。 4 罐:

  • interface.jar - 包含接口public interface Service
  • implementation.jar - 包含public class ServiceImpl implements Service
  • 上面两个罐子变了
  • 即使上面改变了,下面的两个罐子也没有改变
  • interfaceclient.jar - 依赖于interface.jar,包含类ClientOfServiceService 作为构造函数参数,在doA() 方法中使用此服务
  • application.jar - 依赖于以上所有 jars。主方法中的 App 类创建 ServiceImpl 实例并将其作为 arg 传递给 ClientOfService 的构造函数,然后在 ClientOfService 上调用一个方法,该方法从 Service: public static void main(String[] args) { Service service = new ServiceImpl(); ClientOfService clientOfService = new ClientOfService(service); System.out.println("App.main() :: calling clientOfService.doWorkCallingDoAFromService"); clientOfService.doWorkCallingDoAFromService(); System.out.println("App.main() :: end of main"); } 调用方法 doA()

更改interface.jar和implementation.jar后App中的main运行成功(我添加了一些未使用的方法并删除了一个旧方法)。

所以挑战是如何改变接口(当然不改变doA() 方法声明)和实现来阻止它成功运行?可能吗?如果是这样,怎么做?

【问题讨论】:

  • 请注意,ISP 不仅仅是重新编译。未使用的接口方法可能需要客户端不需要的传递依赖项。

标签: java c++ oop interface solid-principles


【解决方案1】:

是的,如果您更改接口,则需要重新编译用 Java 和 C++ 实现它的类。这有几个原因,其中包括:

  • Java 编译器需要检查实现接口的类是否仍然兼容(实现接口的类必须实现其所有方法)。
  • C++ 编译器可能需要更改类的技术布局(例如 vtable 如果 doB() 是虚拟的),并验证具有相同签名但在另一个基类中的另一个成员函数是否未被新隐藏创建了一个。

ISP 的全部工作就是避免这种情况。所以如果doB()实际上与这个接口无关,你最好选择一个只有doB()的隔离接口,并且只更改/重新编译需要实现它的类。

编辑:同样的原则适用于使用接口的类。一些参数当然可能依赖于实现:

  • 对于 C++,为 using 类生成的代码需要依赖正确的接口定义,以便它可以对 vtable 布局做出正确的假设并生成正确的代码。这就是使用类也需要重新编译的原因。
  • 对于 Java,对接口方法的调用确实可以依赖于显式名称和运行时解析。我不是 java 专家,所以这里有一个更正:对接口的一些更改实际上是 allowed by the Java Specifications,无需重新编译。但有些更改需要重新编译。

【讨论】:

  • 我进行了澄清编辑。假设 doA 和 doB 必须由同一个类实现。问题是,如果将它们分成 2 个不同的服务,是否会改变不依赖于 doB 的服务的客户/客户?当然,除此之外,这更具可读性,并且不将 doB 暴露给不需要它的人本身就很好,但这不是我的疑问。
  • @bastiat 我已经编辑以解决您的补充问题。希望这会有所帮助:-)
  • @bastiat 在您进行第二次编辑后,我在回答中添加了一些备注。
【解决方案2】:

对于其他哪些语言它是正确的,对于哪些不是?

这实际上是特定于实现的特征,而不是语言特征。

将接口上的成员访问编译为函数表的内存地址或数组索引的语言实现通常需要重新编译。这就是大多数静态语言编译器的实现方式。大多数 Java 和 C 实现都属于这一类。

编译成员访问字典查找的语言实现不需要重新编译(因此它使用函数名进行查找,而不是替代索引)。这就是大多数动态语言编译器的实现方式。例如,如果另一个文件中的类更改了其接口,大多数 Python 实现不需要重新创建 .pyc 文件。

所以主要的区别在于静态语言和动态语言。当然也有例外。在某些静态语言中可以确保将任何新添加附加到函数表中,因此使用旧索引的现有代码仍然可以工作。对于大多数静态语言的库作者来说,如果语言实现允许,只以保持二进制兼容性的方式更改接口是一个重要的考虑因素。

【讨论】:

  • 谢谢回复。 java是静态的还是动态的? c++是静态的还是动态的?我可以通过哪些方式更改 java 中的接口破坏二进制兼容性?
  • C++ 和 Java 都是静态语言。尽管 JVM 确实具有动态语言功能,但这些在 Java 本身中使用的并不多,主要由在 Java 上运行的其他动态语言使用。请参阅this page 了解在不破坏二进制兼容性的情况下修改 Java 程序的方法。
猜你喜欢
  • 1970-01-01
  • 2018-09-27
  • 1970-01-01
  • 2022-09-13
  • 2015-07-04
  • 1970-01-01
  • 2013-12-09
  • 2013-12-23
  • 2018-08-13
相关资源
最近更新 更多