[译] 公开接口与发布接口

英文原文来自 Public versus Published Interfaces,作者 Martin Fowler


备注:快速翻译(机翻+人工校对,没有精修),质量不高,一般阅读可以,不适合传播,谢绝转载。

软件设计的发展趋势之一是将接口与实现分离。其原理是将模块分为公共(public)部分和私有(private)部分,这样就可以在不与其他模块协调的情况下改变私有部分。此外,还有更进一步的——公开(Public)接口和发布(Published)接口之间的区别。这种区别很重要,因为它影响到如何使用接口。

公开与发布

假设我正在用现代模块化语言编写应用程序——为了使事情更加具体,我们假设这个语言是Java。因此,我的应用程序由多个类(和接口)组成,每个类都有一个公开(Public)接口。这个类的公开接口定义了一组方法,系统中的任何其他类都可以调用这些方法。

当我在增强一个公开(public)方法时,我意识到它的一个参数是多余的——我不需要在方法中使用它(可能是我可以通过其他途径获得这个值,或者我只是不需要它了)。此时,我可以把这个值从方法签名中删除,让这个方法更清晰,并有可能为其调用者节省工作。因为这个方法是公开的,系统中的任何类都可以调用它。我是否应该删除该参数?

在这种情况下,我认为是应该的,因为有好处,而且不难。虽然这个方法可能在任何地方使用,但我可以用搜索工具轻松找到用户。如果我有一个适用于Java的新式重构工具(详见www.refactoring.com),我可以通过简单的菜单点击来完成——然后工具会自动更新所有的调用者。所以,改变一个公开(public)方法并不是什么大事。

然而,如果我把这个软件作为一个组件放到网上,而其他我不认识的人开始在它的基础上构建应用程序,事态就会迅速改变。如果我现在删除这个参数,当我升级时,其他人的代码都会被破坏。现在我必须做一些更复杂的事情。我需要生成少一个参数的新方法,但保留旧方法——可能是重新编码旧方法来调用新方法。我把旧方法标记为废弃(deprecated),假设大家会把代码搬过来,我可以在下一两个版本中修改它。

这两种情况是完全不同的,然而在Java语言中却没有任何东西可以表述其中的区别——在其他一些语言中这一点也存在差距。然而,公开(public)和发布(published)的区别比更常见的公开-私有的区别更重要。

关键的区别在于能够找到并修改使用接口的代码。对于一个已发布(published)的接口,这是不可能的,所以你需要一个更复杂的接口更新过程。接口用户要么是调用者,要么是子类或实现接口的类。

关于发布的建议

认识到公开和发布之间的区别,这个区别会导致一系列重要的后果。

不要将接口视为发布,除非它真的是

如果你需要改变一个接口,并且可以找到并改变所有的用户,那么就不用费劲得去经历所有的转发和废弃的过程。做出改变并更新用户即可。

不要在团队内部发布接口

我曾经向某人建议修改一个公开(public)方法,他反对,因为它已经被发布(published)。真正的问题是,虽然团队里只有三个人,但每个开发人员都把自己的接口当作发布(published)给另外两个人。这是因为团队采用了一种强势的代码所有权形式,每个模块都被分配给一个程序员,只有这个程序员可以修改模块的代码。我很认可代码所有权——它鼓励人们监控他们的代码质量——但是像这种强势代码所有权模式会引起问题,因为它迫使你把人际接口当作发布(published)的。

我鼓励一种较弱的所有权模式,即一个人负责模块,但其他人可以在必要时进行修改。这让其他开发者可以做一些事情,比如修改对变更了的的方法的调用。(你也可以使用集体代码所有权——任何人都可以修改任何东西,以避免内部发布。) 这种所有权通常需要一个支持并发写入器的配置管理系统(比如CVS),而不是使用悲观锁。

在没有某种形式的内部发布(published)的情况下,能运行多大的团队是有限制的,但我还是倾向于更少的发布(published)。换句话说,先假设不需要发布接口,然后如果发现会导致问题,再进行调整。

尽量减少和推迟发布

因为发布将导致变化周期较慢,限制发布频率。这使得公开和发布之间的语义差别成为一个问题。建议最好的是声明一些模块为接口,然后劝告软件用户不要使用其他模块,即使他们可以看到它们。让这些接口尽可能的薄。在开发周期中尽可能晚一点发布,给自己留出时间完善接口。其中一项战略是在发布给大众之前,与组件的一个或两个用户紧密合作——这些用户足够友好可以接收剧烈的变化。

尽量以做加法的方式来做变更

除了区分发布接口和公开接口外,我们还可以识别两种类型的接口变化。一般来说,变更可以改变一个接口的任何方面。但是,有一些更改只会造成对接口的添加,比如添加一个方法。添加不会破坏任何一个接口的客户端——现有的客户端使用老的方法是没有问题的。因此,当发生变更时,可以考虑是否可以将其构造为增加项。例如,如果需要从方法中删除一个参数,与其改变方法,不如尝试添加一个没有参数的新方法。这样一来,得到的是一个加法,而不是一般的改动,而客户端仍然是兼容的。

如果外部有自己的接口实现,添加仍然会引起问题。如果发生这种情况,即使添加一个方法也会破坏替代实现。因此,一些组件技术,如COM,使用不可变的接口。对于不可变的接口,一旦发布了,就必须保证不能改变它。如果想改变这个接口,你必须创建第二个接口,然后组件可以随意支持这个接口。这不是理想的方案,但肯定有它的优点。

我希望看到公开(public)和发布(published)的区别能更多地出现在语言和平台中。同样有趣的是,环境并不倾向于提供设施来演进接口。有些可以废弃即将被删除的方法:Eiffel将其作为语言的一部分,Java也是如此(但作为内建文档的一部分)。我还没有看到有人在方法中添加标记,警告实现者将要添加的东西,或者容许以一种临时的方式向接口添加东西。这是软件平台中普遍存在的问题。到目前为止,平台还没有充分理解软件应该是“软”的,因此需要设施来允许变更。近年来,我们已经在这个方向上有了长足的进步,有了组件打包系统,但这些只是开始。

敖小剑
敖小剑
新时代农民工 * 中年码农

我目前研究的方向主要在Microservice、Servicemesh、Serverless等Cloud Native相关的领域,全职从事Dapr开发,欢迎交流和指导。