svn: 没有演进历程信息
引入了默认方法以启用接口演进。 如果向后兼容性是不可替代的,则仅限于向接口添加新方法(这是它们在JDK中的唯一用法)。 但是,如果希望客户端更新其代码,则可以使用默认方法逐步演化接口而不会引起编译错误,从而使客户端有时间将其代码更新为新版本的接口。
这个小型系列的第一部分说明了默认实现如何允许在不破坏客户端代码的情况下添加,替换和删除方法。 我愚蠢地宣布,“以后的文章将探讨替换整个接口的方法”,同时也不会破坏客户端代码。
好吧,您现在正在阅读这篇文章,不幸的摘要是:
我无法使其工作。
为什么? 泛型。
到底为什么 你真的想知道吗 好吧,那么请继续阅读,但是这篇文章的其余部分实际上只是对我如何成为障碍的描述,因此不要期望太多。 (大激励,是吗?)
总览
在描述我尝试过的方法和失败的方法之前,我先定义要解决的问题。
问题陈述
这就是我们要做的:
假定您的代码库包含一个接口,您的客户端可以用所有可以想象的方式使用该接口:它们具有自己的实现,使用其实例调用您的代码,并且您的代码返回此类实例,当然他们将其用作参数的类型和返回值。
现在,您要实质性地更改接口:以无法用对单个方法的更改来表示的方式对其进行重命名,移动或修改。 (但是从提供一个版本到另一个版本的角度来看,这两个接口仍然是等效的。)
您可以执行此操作,发布包含更改的新版本,并告诉您的客户端修复其导致的编译错误。 如果他们的代码与您的代码高度耦合,那么他们可能必须在单独的分支中执行此操作以花一些时间在代码上,但这就是生活,对吗? 不过,您真是个好人,因此,与其要求费时的一天,不如让他们有机会随着时间的推移(例如,直到下一个版本)逐渐更改其代码,而没有任何编译错误。
(请注意,这是接下来所有内容的主要要求。我首先忽略了这是否是个好主意。我只是想看看自己能走多远。)
我认为甚至有机会实现这一目标的唯一方法是定义一个过渡阶段,在该阶段中,新旧版本的接口都将共存。 因此,我们真正需要的是一种通用的逐步方法,该方法如何将实现,调用者和声明从一个接口转移到另一个接口。
想法
在发布这篇文章时,我对它的工作方式有一个具体的想法。 基本上与我在方法中使用的方法相同。
不断发展的接口方法
使用默认方法添加,替换或删除接口的单个方法非常简单,通常包括三个步骤(在某些情况下更少):
- 新版本:库的新版本发布,其中界面定义是过渡性的,并结合了旧的和新的所需轮廓。 默认方法可确保所有外部实现和调用仍然有效,并且在更新时不会出现编译错误。
- 过渡:然后客户有时间从旧大纲过渡到新大纲。 同样,默认方法可确保适应的外部实现和调用有效,并且可以进行更改而不会产生编译错误。
- 新版本:在新版本中,该库删除了旧轮廓的残差。 鉴于客户端明智地利用了自己的时间并进行了必要的更改,因此发布新版本不会导致编译错误。
如果您对这些步骤的详细说明感兴趣,可以阅读我的早期文章 。
改进界面
对于这种情况,这种方法似乎也很有意义,所以我坐下来进行了探讨。
如果整个接口发生变化,则要稍微复杂一点,因为在方法仅具有调用者和实现的地方,该接口也是一种类型,即可以在声明中使用。 这使得必须区分三种使用接口的方式:
- 内部使用 ,您在其中拥有实现和使用接口的代码
- 已发布的使用 ,您拥有实现,但客户端调用了代码
- 外部使用 ,其中客户端拥有实现和使用接口的代码
起作用的部分采用与演化方法相同的方法:
- 新版本:使用新界面发布新版本,以扩展旧版本。 让所有内部代码实现并使用新接口。 所有已发布的代码将使用旧接口声明参数类型,并使用新接口返回类型。 如果必须转换实例,则可以使用适配器来完成。 现在忽略参数化类型,此更改将不会导致客户端代码中的编译错误。
- 过渡:发布后,客户端更改其代码。 从旧接口的实现(已更改为实现新接口的实现)和您已发布的代码返回的实例开始,它们可以开始声明新类型的实例,更新将它们传递给它们的方法的参数类型,等等。上。 如有必要,可以暂时使用适配器通过新接口与旧实例进行交互。
- 新版本:发布一个删除旧界面的版本。
与不断发展的方法相同,新接口中的默认实现允许客户端代码停止明确实现旧接口,从而可以在第二个版本中将其删除。 此外,旧接口上的便捷asNew()
方法可以调用适配器以使其自身适应新接口。
我掩饰了一些细节,但我希望你相信我,这是可行的。 现在让我们回到泛型…
障碍
提出的方法中的关键部分是已发布的代码。 它由您的客户调用,因此第一个发行版必须以兼容的方式对其进行更改。 并且由于所有内部代码都需要新接口,因此它必须从Old
到New
。
没有泛型,它可能看起来像这样:
在已发布的代码中将“旧”转换为“新”
// in version 0
public Old doSomething(Old o) {// 'callToInternalCode' requires an 'Old'callToInternalCode(o);return o;
}// in version 1 the method still accepts 'Old' but returns 'New'
public New doSomething(Old o) {// 'callToInternalCode' now requires a 'New'New n = o.asNew();callToInternalCode(n);return n;
}
好的,到目前为止很好。 现在,让我们看看泛型的外观。
在已发布的代码中将“旧”转换为“新” –泛型
// in version 0
public Container<Old> doSomething(Container<Old> o) {// 'callToInternalCode' requires a 'Container<Old>'callToInternalCode(o);return o;
}// in version 1
// doesn't work because it breaks assignments of the return value
public Container<New> doSomething(Container<Old> o) {// 'callToInternalCode' requires a 'Container<New>'// but we can not hand an adapted version to 'callToInternalCode'// instead we must create a new containerNew nInstance = o.get().asNew();Container<New> n = Container.of(nInstance);callToInternalCode(n);return n;
}
因此,使用已发布的代码层从旧界面适应新界面通常不起作用,原因至少有两个:
- 由于Java中泛型的不变性,返回值的所有分配都将中断:
不变性打破分配Container<Old> old = // ... // works in version 0; breaks in version 1 Container<Old> o = published.doSomething(old);
- 不能将同一
Container
实例从已发布传递到内部代码。 这导致两个问题:- 创建一个新容器可能很困难或不可能。
该死的…
从一开始,我就感到仿制药会很麻烦-回想起来,这实际上很明显。 当涉及类型时,泛型怎么可能不是问题。 因此,也许我应该先尝试解决难题。
可能绕行
在将我的头撞在墙上一段时间之后,我仍然没有找到解决这个问题的通用方法。 但是我想出了一些可能有助于解决特殊情况的想法。
通配符
您可以检查已发布的内部代码是否充分利用了通配符(请记住PECS )。 您也可以建议客户如何使用它们。
根据情况,这可能会产生解决方案。
专用接口,类,实例
根据具体的代码,可以提供使用旧接口的已发布接口,类或实例的新版本。 如果可以通过让客户端选择使用依赖于旧接口的接口,类或实例还是依赖于新接口的接口,类或实例的方式来处理代码,则各个实现不必进行转换。
但这可能会将旧界面推回内部代码,而内部代码刚刚更新为仅使用新接口。 听起来也不好。
容器适配器
您可以在已发布的代码中为与旧接口一起使用的容器提供适配器。 这实际上将允许您在这些容器上调用asNew()
。
(出于一个不相关的原因,我目前正在为某些JDK集合进行此类转换。下一版本的LibFX将包含它们;如果您好奇,可以在GitHub上查看演示。)
算了!
这一切又是为了什么? 为了防止客户创建分支,在将所有内容合并回master之前花一些时间在那里修复问题吗? 算了!
在这一点上,这是我对此事的看法。 只要您只处理单个方法,接口的演变就很顺利,但是当您要替换整个接口时,这似乎会很痛苦。 因此,除非有充分的理由介绍所有这些复杂性,否则我将以困难的方式进行操作,然后让客户对其进行分类。 还是根本不做。
而且,如果您只是重命名或移动界面,无论如何,大部分甚至全部工作都可以通过简单的搜索替换来完成。
反射
我们重申了如何将默认方法用于发布,过渡和发布三部分的界面演化。 尽管这对单个方法有效,但我们发现它无法替换整个接口。 主要问题是参数类型的不变性使我们无法使用已发布的代码作为适应层。
即使我们看到了一些解决该问题的方法,也没有一个好的解决方案脱颖而出。 最后,看起来不值得麻烦。
我有事吗 还是整个想法愚蠢? 为什么不发表评论!
翻译自: https://www.javacodegeeks.com/2015/04/interface-evolution-with-default-methods-part-ii-interfaces.html
svn: 没有演进历程信息