【AI翻译】接口——软件工程中最重要的概念

本文由AI翻译

原文链接:https://blog.robertelder.org/interfaces-most-important-software-engineering-concept/
发布时间:2016-02-01 (最后更新:2016-09-29)
作者:Robert Elder


摘要

接口可以被认为是系统与环境之间的一份契约。在计算机程序中,"系统"指的是所讨论的函数或模块,而"环境"则是项目的其余部分。接口正式地描述了什么可以在系统和环境之间传递。"实现"可以被定义为系统减去接口的部分。像 Haskell 这样的语言中的接口可以极其精确,而像 Python 中的接口则可能非常不精确。所使用的接口类型会影响所产生的技术债务数量(本文提供一个数学公式)以及程序员的生产力。本文提出了一种量化和比较接口的方法。基于这些比较,你可以对某种语言或工具的使用方式做出一些观察。


概览

软件工程中最重要的概念是接口。本文讨论的不是 Java 语言中的 interface,而是软件设计中的接口,并在更小的程度上,讨论宇宙中任何地方的接口。软件开发中有许多其他重要概念,但我认为其中许多最终都与接口为何如此重要有关。在本文中,我将讨论:

  • 什么是接口?
  • 作为契约的接口
  • 什么是"模块"或"抽象"?
  • 抽象泄漏
  • 一种量化和比较接口的方法
  • 关于接口在专利和版权方面处理方式的提案
  • 为什么人们仍在使用命令行
  • 为什么 Twitter 当初选择用 Ruby/Rails 启动,后来又用 Scala 重写是正确的举措
  • 为什么 Java 和 C++ 在企业软件中如此常用
  • 一次用数学量化技术债务如何扩展,以及为何它只在大项目中变得明显的尝试
  • 为什么 Python 如此受欢迎,特别是对新手程序员而言
  • 如何有效地抄近路,同时最小化技术债务

什么是接口?

在大学里,我们学到过几个关于接口的简洁定义,我非常喜欢:

接口是系统与环境之间的契约

或者说

接口是系统与环境的交集

接口 = 系统 ∩ 环境

系统与环境交集的接口图示

当"系统"实际上是一个物理对象时,交集的定义非常贴切。上述定义非常抽象,让我们直接来看一个具体的例子:某人在键盘上打字。

在这个例子中,系统代表整个笔记本电脑,环境是人的手(以及任何喜欢踩键盘的猫)。因此,接口必须是手与计算机交互中,既不完全归属于一方,也只能归属于双方共同作用的任何部分。通常我们认为手和键盘是截然分开的,所以在这种情况下,接口的精确边界是一个哲学辩论的问题。读者可以自行决定是将整个键盘,还是仅将与手指或键盘接触的单个原子视为接口的一部分。你可能想知道这个例子如何与接口作为契约的定义联系起来:在这种情况下,"契约"是我们过去为了让大脑记住所有按键位置而花费大量精力学习的惯例。契约还有更微妙的方面,比如按住一个键不放与快速按下并释放它具有不同的含义。

这是一点不错的哲学思考,但这与编写软件有什么关系呢?嗯,编程中的接口无处不在,即使你没有意识到。如果你是 Java 程序员,你会明确地命名它们,但它们也存在于像 C 这样的其他语言中。让我们看看下面例子中 add_numbers 函数的接口:

unsigned int add_numbers(unsigned int, unsigned int);

void other_function(void){
    add_numbers(3,4);
}

unsigned int add_numbers(unsigned int a, unsigned int b){
    return a + b;
}

int main(void){
    add_numbers(9,99);
    return 0;
}

在上面的图示中,"系统"是 add_numbers 函数。你也可以正确地说 main 函数或 other_function 也可以被视为独立的系统,但为简单起见,上图将 add_numbers 函数视为一个孤立的系统。将 add_numbers 函数的调用也视为接口的一部分是合理的。请注意,我们增加了一个第四个概念:实现(Implementation)。


定义系统与环境

我还没有定义"系统"或"环境"这两个术语,所以让我们回顾一下我的意思:系统很容易定义:它就是你正在使用的任何有用的东西。它可以是你的笔记本电脑、一个计算机程序、你家的大门,或者一小段源代码。我所说的环境可以根据系统来定义:首先,将环境视为包含整个宇宙。现在,从你认为是环境的部分中尽可能多地移除系统,但一旦再移除任何东西就会阻止环境通过其与系统的共享边界与该系统交互时,就停止。所以,如果你考虑笔记本电脑的例子,如果它的内部构造不同(但功能相同),我们仍然可以使用它,但一旦我们开始改变按键或屏幕,我们就会开始在与它交互时遇到问题。


不可能的接口

一个值得思考的问题是:"是否可能描述一个实际上无法实现的接口?"我认为可以,当你的接口描述中包含矛盾的主张时就会发生这种情况。例如,如果你定义了一个接口,断言"此函数不返回 0"和"此函数返回 0"。不存在与这两个断言都一致的实现。然而,你可以在描述接口时自由地做出你无法兑现的承诺。


定义实现

讨论接口而不提及实现是很困难的,所以让我们来尝试正式定义什么是实现:

实现是系统减去接口

实现 = 系统 \ 接口
实现 = 系统 \ (系统 ∩ 环境)

实现的图示

请注意,我从未听说过(或不记得)有其他人这样定义实现,但这似乎是对接口基于集合的定义的一个不可抗拒的扩展,并且它还有一些我稍后会讨论的好处。如果你是一个正在为考试而学习的可怜学生,你的教授可能从未听说过这个定义。如果这个定义与某些面向对象编程的分类法相冲突,我也不会感到惊讶,但即便如此,我也不打算改变它。让那些疯狂的 OOP 人士去修改他们的教科书来匹配我的定义吧。

以这种方式定义实现会引导我们得出其他合理的结论:当我们谈论物理系统上的接口时,我们通常认为"系统实现"是整个物理对象,而认为"真正的"系统实现排除了按钮、屏幕或任何其他物理部分是不自然的。这促使我们将接口视为包含尽可能少的物理系统,而更多地代表一种惯例。就好像接口只是一组承诺、保证或某种……系统与环境之间的契约!


作为契约的接口

让我们将前一个例子中 add_numbers 函数的接口视为一份契约,看看它保证了什么:

  • add_numbers 是一个存在的函数。
  • add_numbers 精确地接受两个参数,两者都是 unsigned int
  • add_numbers 精确地返回一个 unsigned int

这个函数的接口没有说明任何关于以下内容:

  • add_numbers 是否会停止。
  • add_numbers 的渐近运行时复杂度。
  • 运行 add_numbers 所需的空闲内存量。
  • unsigned int 的真正实现是什么。
  • 副作用(如分配内存、修改全局变量)。

上述 add_numbers 的接口被称为函数"原型",在早期版本的 K&R C 中,有一种更弱形式的接口描述:

unsigned int add_numbers();

将接口定义为"契约"对于编程非常方便,因为大多数编程任务仅仅是定义和要求一组公理。后置条件和前置条件都是关于某些属性或行为的保证。在两方从事商业活动之前,他们应该准备一份合同。合同规定了交付物是什么,支付多少钱,以及何时支付。其他主题如提前终止、赔偿、费用等都预先规定好。当合同被违反时,法院或仲裁员可以解决情况,但如果你忘记在合同中定义某些内容,那么意外的惊喜就更有可能发生。在计算机程序中,我们有同样的事情:模块和函数指定它们需要什么,以及(有时)它们将返回什么。违反此契约将导致编译错误、运行时错误、程序故障、构建系统或 linter 失败,甚至你的经理对你大喊大叫。我甚至可以说,将接口定义为"契约"的概念甚至不是比喻。它与商业合同真的是同一个概念,尽管商业合同通常没有那么详细。


专利、版权与接口

本节不构成法律建议,甚至可能与现有法律相矛盾,此处所有陈述均为作者的观点。

在上一节中,我表示我会将接口字面上视为两个实体之间的"商业合同",并且我强调我不认为这是一个比喻。我相信这种解释既能满足计算机科学家的关切,也能满足旨在保护创意作品的法律专业人士的关切。

接口应该可以申请专利吗?使用本文中包含的定义,即接口是系统与环境之间的契约,我认为接口不应该可以申请专利,而且到目前为止,现有的判例法似乎与我意见一致。但是请记住,"接口"这个词非常通用,并且其使用方式常常与我在本文中定义它的方式不同。

接口应该受版权保护吗?使用本文中包含的定义,即接口是系统与环境之间的契约,我确实认为接口的"源代码"应该受版权保护。此外,接口的受版权保护方面不应超出它们开始涵盖使接口如此特殊的那些方面的点。版权应该只涵盖媒介(源代码或手写副本),而不涵盖保证或约束。如果接口的任何保证或约束变得与媒介的任何部分不可分割,那么媒介的那些部分应该被取消版权资格。我将提出了一个简单的测试,可以用来确定某物受版权保护:

如果你考虑一组你希望受版权保护的接口属性,对于任何可以想象的以任何方式成功使用该接口的第三方软件,总应该可以构建某个直接替换品,该替换品声明并实现了相同的接口,并被第三方软件成功使用而无需对第三方软件进行任何修改,并且不侵犯任何版权。如果每个可能的直接替换品都会导致侵权或要求第三方软件被修改或功能倒退,那么所选的受版权保护属性集就过于激进,必须减少。

我相信上述测试也适用于测试可专利性。请注意,此测试仅确定某物受版权保护或可专利。它对于最终确定它是否受版权保护或可专利没有任何定论。最后,上述测试只是我的观点,不要将其与实际法律混淆。

关于上述测试,需要指出的重要一点是,在一种语言中可以被认为是接口一部分的任何标准,在另一种语言中可能不是接口的一部分。例如,在 Java 中,函数声明的顺序不影响程序执行。如果你想当然地说"文件中的函数顺序永远不重要",那么如果你考虑以下 Python 程序,你就是错误的:

def foo():
        print("asdf")

def foo(abc):
        print(abc)

foo("lol")

考虑到接口的法律方面,促使我回头看了一下著名的 Oracle vs. Google 案。提供的链接包含了对软件开发者会感兴趣的案件细节,所以我的分析将以此为基础。总而言之,根据我所看到的,我找不到理由不同意有利于 Oracle 的判决结果。这并不是说我支持它,因为我能找到的公开案件细节相当稀少。

我认为大多数软件开发者担心的是,该案的结果可能会开创一个先例,允许版权或专利涵盖接口中会导致我提出的上述测试失败的部分。

该案的结果取决于地区法院的裁定,即"API的'结构、顺序和组织'受版权保护"。如上所述,我认为这没有问题,只要"结构、顺序和组织"的定义不会导致上述测试失败。以下是链接文章中的几个关键引述:

"地区法院的结论是'只有一种方式来编写'与 Java 交互的声明。如果这是真的,那么使用相同的声明将不受版权保护。然而,除了三个 API 包外,Google 并未否认它可以编写自己的 API 包来访问 Java 语言。"最后,"Google 承认它逐字复制了声明。"

看来地区法院在得出接口的内在独特性质不受版权保护的结论上做出了正确的决定,但 Google 也承认"逐字"复制了声明。如果"逐字"可以被理解为包括字面上的复制粘贴,包括像空白和注释中的拼写错误这样的非功能性方面,那么我认为将此视为版权侵权是非常合理的。接口的不受版权保护性不必阻止接口的个人艺术表达受版权保护。

我对这个案件的了解仅来自于我能在网上读到的内容,但在我看来,Google 创造了 Java 源代码的逐字副本,而这些副本恰好包含了接口。Google 自己似乎也认为他们使用 Java 需要获得许可,因为在 2010 年之前,Google 曾寻求与 Sun 达成许可协议以授权使用 Java。在 Sun 被 Oracle 收购后,许可谈判失败了。Google 曾寻求许可协议但未果,却继续使用"逐字"复制代码,这一事实似乎对他们的案子没有帮助。我怀疑 Google 的律师可能知道他们的案子很弱,所以他们试图使用与接口不应受版权保护这一非常合法的说法相关的辩护,并希望接口的源代码表示和更哲学的概念会变得混淆,从而让他们赢得官司。


什么是"模块"或"抽象"?

当我想起"模块"时,我想到的是儿童的"形状分类器"玩具,你可以将不同形状的积木放入由正方形、圆形、三角形等组成的孔中。

我认为这种表示方法非常适用,因为它清楚地强调了模块边界的重要性,以及它如何与其环境的其余部分交互。此外,上面立方体的接口对外部世界如何与内部事物交互施加了非常强的约束。你不能绕过接口,所以如果你想与它交互,你必须通过它向你暴露的方式进行。最后,立方体里什么都没有,但我们实际上不在乎,因为重要的不是内在的东西(抱歉了,立方体),而是暴露给世界的接口。

我非常喜欢的另一个例子是细胞及其膜:细胞表面的特征,如转运蛋白和受体蛋白,只允许细胞外基质中的某些东西根据非常具体的规则影响细胞质内发生的事情:

细胞膜图示

在本文的语境中,我将把"模块"和"抽象"视为同一个概念。这些词的字典定义当然不相同,即使在编程语言之间,这些概念也有不同的含义。我感兴趣的关键属性是,它们都可以被视为我们本文中一直使用的术语"系统":抽象和模块可以被认为是由一个接口和一个实现组成的。你可以将 C 中的单个函数视为一个模块,Python 中的"模块",Java 中的类或包。任何具有某种外部呈现的接口和某种"隐藏"实现的东西。请注意,实现的"隐藏性"可以由语言规则强加,甚至可以仅由程序员的约定来强加。


抽象泄漏

据我所知,抽象泄漏的想法可以追溯到 Joel Spolsky 的一篇文章。文章中有一些关于特定抽象泄漏的好例子,但我想补充一个我自己的:在编程中,"map"的概念非常普遍,它代表一个由键和值对组成的数据结构。map 保证的一个重要约束是所有 map 键必须是唯一的:尝试向给定键写入新值要么导致错误,要么覆盖该键的先前值。结果绝不会有重复的键。一个极其常见的程序员需求是希望遍历 map 的所有键。由于键的排序不一定是 map 提供的保证,你可能会想知道当你遍历它们时键会是什么顺序?嗯,顺序是没有定义的,因为 map 接口不提供任何排序保证。因此,任何排序都被认为是可接受的,但在实践中,键很可能会以某种方式排序。它们为什么会排序呢?嗯,排序恰好是组织数据的一种有效方式。它可以使检查预先存在的键等事情变得更容易。

遍历排序数据与遍历随机数据可能会产生非常不同的结果。例如,如果你正在尝试在列表中找到最小的数字:

min = null;
list = map.getMapKeys();
for (item in list){
    if ( min == null ){
        min = item
    }else if (item < min){
        min = min;   /*  这行有 bug */
    }
}

如果你的数据是升序排列的,else if 分支将永远不会执行,即使你进行随机输入测试,你的程序也永远不会发现这一行的问题。这是一个巨大的问题,因为如果你将 map 实现换成另一个不返回排序键的实现,那么你的代码将突然开始运行有 bug 的代码路径。到那时,你已经完全忘记了这段代码,它隐藏在一个巨大的单体项目中。

我将为本文后续使用提出我自己对抽象泄漏的定义:

当实现有可能以接口中未约定的方式影响环境时,就存在抽象泄漏。

使用这个定义,似乎几乎每个抽象都是有泄漏的,因为在接口中指定每一个环境影响只在最严格的数学系统中才实用。对于物理系统,你可能也可以在这里与哥德尔不完备定理建立联系。大多数抽象都是有泄漏的这个想法并非没有根据,因为这基本上就是 Joel Spolsky 在他的"抽象泄漏定律"中所暗示的:

"所有非平凡的抽象,在某种程度上都是有泄漏的。"

那么,如果每个抽象都有泄漏,为什么还要谈论它呢?问题只在环境的某一部分开始依赖于源自所讨论的系统的这些未指定的环境影响之一时才会出现。这些是每个人都在谈论的有问题的抽象泄漏。

这具有深远的影响,不仅对于偶然的 bug,而且在安全领域也是如此。有一个与物理系统安全相关的著名短语,其中来自系统的意外影响以危及其安全的方式泄漏到环境中:旁道攻击。将此与所有抽象都有泄漏的主张相结合,将得出以下结论:

每个密码系统的物理实现都容易受到旁道攻击。

鉴于我们上面讨论的内容,将这个想法扩展到不仅包括物理实现,还包括模拟实现,也是不无道理的。


量化和比较接口

正如我们上面看到的,C 中的接口指定了诸如返回类型和可以传递给函数的参数数量之类的事情。但是 Python 中的接口指定了什么?请注意,我使用的术语"接口"与本文的用法一致,这可能比你以前读过的关于 Python 中"接口"的任何文献都更通用。

def add_numbers(a,b):
        return a + b

print(add_numbers(3,1))
print(add_numbers("abc","def"))

在 Python 中,你不必在函数接口上指定类型。这样做的好处是使函数的定义和调用更容易,因为需要指定的信息更少,缺点是可供提前检查的约束更少(以检测可能的编程错误)。

我认为在比较和量化接口的不同特性方面,可以从你可以通过它们发送信息的多种方式入手。这可以针对特定接口进行,也可以从给定编程语言中可以指定的所有接口的角度进行。它对于比较同一语言中特定接口的安全性也可能有用。对于 C 中的 add_numbers 示例,让我们考虑一下我们可以通过接口发送多少信息,以及通过抽象泄漏绕过接口发送多少信息:

通过 C 接口的信息

特性描述 可能的状态数
参数1类型 1 (unsigned int)
参数2类型 1 (unsigned int)
返回值类型 1 (unsigned int)
参数1值 2^('unsigned int'中的位数)
参数2值 2^('unsigned int'中的位数)
返回值 2^('unsigned int'中的位数)

绕过 C 接口的信息

特性描述 可能的状态数
全局变量状态 (全局变量数) * (全局变量状态数)
文件系统 文件系统状态数
消耗的CPU时间 无界
堆状态 堆状态数
许多其他…

以下是可以通过 Python 接口 add_numbers 传达的信息数量:

通过 Python 接口的信息

特性描述 可能的状态数
参数1类型 几乎无限
参数2类型 几乎无限
返回值类型 几乎无限
参数1值 几乎无限
参数2值 几乎无限
返回值 几乎无限

绕过 Python 接口的信息

特性描述 可能的状态数
全局变量状态 (全局变量数) * (全局变量状态数)
文件系统 文件系统状态数
消耗的CPU时间 无界
堆状态 堆状态数
许多其他…

现在,如果你看一下我们可以在 Haskell 中描述的接口类型(感谢 James Hudon 的审阅,因为我几乎不懂 Haskell):

add_numbers :: Int -> Int -> Int
add_numbers 3 4 = 7

main = print (add_numbers 3 4)

使用上述 Haskell 代码,接口 add_numbers 可以接受以下信息:

通过 Haskell 接口的信息

特性描述 可能的状态数
参数1类型 1 (Int)
参数2类型 1 (Int)
返回值类型 1 (Int)
参数1值 1 (值 3)
参数2值 1 (值 4)
返回值 至少 2^30

绕过 Haskell 接口的信息

特性描述 可能的状态数
消耗的CPU时间 无界
CPU/内存缓存效应 无界
可能还有其他…

对于给定语言中的特定接口,你可以量化几件不同的事情:

  • 通过接口传达信息的独特方式数量
  • 通过抽象泄漏绕过接口传达信息的独特方式数量

从编程语言的角度,你也可以观察到:

  • 语言让你在多少或多 little 信息通过接口方面有多大的限制性
  • 语言为你提供了哪些工具来防止绕过接口的通信。

如果将同类型的分析扩展到其他接口,例如你可以更改目录的图形用户界面:

文件夹图形界面

通过 GUI 的信息

特性描述 可能的状态数
点击文件夹1 文件夹1在屏幕上占用的像素数 * 点击次数
点击文件夹2 文件夹2在屏幕上占用的像素数 * 点击次数
悬停在文件夹1上 文件夹1在屏幕上占用的像素数
悬停在文件夹2上 文件夹2在屏幕上占用的像素数
悬停/点击事件之间的时间 无限
常见键盘事件 常见组合键数量
GUI屏幕区域 用于GUI显示的像素数

绕过 GUI 的信息

特性描述 可能的状态数
隐藏的UI功能 无限
非标准键盘快捷键 按钮2在屏幕上占用的像素数
其他意外的UI功能

如果你回顾在命令行上使用 cd 执行的相同更改目录任务:

通过命令行接口的信息

特性描述 可能的状态数
可能输入的目录数 无限

绕过命令行接口的信息

特性描述 可能的状态数
环境变量 无限

对于通过 GUI 和命令行发送的信息,实际上还有另一条我没有包含在上述表格中的数据:信号中的噪声量。如果你考虑精确重复一系列键盘敲击(逐键)与一系列鼠标移动(逐像素)的难度,你会注意到从鼠标移动或点击获得的数据中的误差总是远大于键盘敲击。GUI 通过使其接受的语义更加不具体来弥补这一点。你能想象如果"确定"和"取消"按钮的可点击区域只有 1 像素宽吗?此外,当你考虑到不同能力个体的错误率如何变化时,这种分析会变得更加复杂。

现在我已经回顾了一种量化和比较接口的可能方法,我将从这些例子和我自己的个人经验中做一些推断:

  • 人类倾向于偏爱那些对它们接受的信息不是很具体的接口,特别是当他们不熟悉该接口时。
  • 对它们接受的信息不是很具体的接口容易被滥用。
  • 接受大量信息的"万能"接口被视为强大,但常常被滥用。
  • 当通信变得繁琐时,人类倾向于绕过接口进行通信。
  • 通过抽象泄漏绕过接口进行通信非常容易产生不希望的意外。

泄漏的和特定的接口

我将根据上一节的分析做出很多观察,所以为了清晰起见,我将定义几个术语:

当接口在系统和环境之间的任何通信中都容易被忽略时,就存在一个泄漏的接口。

如果一个接口具有相对较少的可能输入和输出,那么它就是特定的

有关泄漏接口的更多详细信息,请参阅关于抽象泄漏的部分。关于我所说的特定接口的一个好例子是分段定义的函数,仅为非常少量的输入定义。

如果你能有意义地量化接口的"泄漏性"或"特定性",我认为值得定义一个谱系,其中非常特定和非泄漏的接口在一端,而非特定和泄漏的接口在另一端:

接口谱系图

可能会有合理的论据将谱系中的任何一项向右或向左移动,但你明白我的意思。请注意,你可能可以将此分为两个谱系:一个用于接口允许"泄漏"抽象的程度,一个用于接口的特定程度,尽管总的来说这两个概念似乎是相关的。根据我的经验,我还会提出的另一个相关性是,来自此谱系"非用户友好"端的工具的"错误"频率较低,当它们确实发生时,它们更可能是由确认(validation)失败引起的。对于谱系的"用户友好"端,错误更频繁,更可能是验证(verification)错误。


技术债务的渐近复杂度

我将以一个主张开始本节:

项目中的大部分技术债务源于对抽象泄漏的不当依赖,或对具有难以预见后果的极其不具体接口契约的依赖。

当一个项目开始时,只有一两个模块,你需要做的工作来指定一个好的接口契约是 O(1)。如果你设计一个糟糕的接口,你将创造的技术债务也是 O(1),所以在花时间把接口契约做好上没有太多回报。但是随着模块数量线性增加,模块间通信的最坏情况数量根据 O(N^2) 增加。因此,如果你制定了糟糕的接口契约,这些糟糕接口契约的最坏情况调用次数将根据 N^2 扩展(如果每个模块都与每个其他模块通信)。

技术债务增长图

在上面的图表中,你可以看到最初避免创建定义良好的接口工作量较少,然而,这种优势很快就被超越,因为模块间通信问题将以模块数量的多项式速率发生,而创建良好接口规范所需的工作量与模块数量成线性关系。多项式成本来自于考虑握手问题,在最坏情况下,每个模块都与每个其他模块通信。显然,普通项目的通信需求扩展速率将小于 O(n^2),但肯定会超过 O(n)。还有一个因素欺骗性地将快速增长推向未来:人类的记忆。当你刚开始时,即使你有 20 个模块,你可能也能记住它们都做什么,所以模糊的函数名和深奥的惯例就是你需要的全部契约。一旦项目变得足够大,以至于你忘记了这些,或者你引进了其他人,多项式成本总是占主导地位。


为什么人们仍在使用命令行?

当你问这个问题时,人们通常会给出几个不同的答案,但没有一个是我认为最重要的:

  • 它非常强大和灵活。
  • 命令行使用更少的资源。
  • 使用命令行让你更了解底层的工作原理。

我们仍在使用命令行的最重要原因是自动化!很难高估自动化任务带来的生产力提升。如果我需要启动一个包含 100 台服务器的集群,你会登录到每台服务器并通过点击一堆 GUI 来手动安装你的软件栈吗?即使你想自动化点击 GUI 的任务,你也需要某种文件存储来记住如何以及在哪里点击。某种充满灵活……命令的文件。

我将与关于量化和比较接口的部分建立的另一个普遍关系是,即使我们可以通过自动点击和屏幕抓取器来自动化事物,这种类型的通信是为人类设计的,因此它暴露了一个非常不具体的接口,不允许你非常精确。结果是你的自动点击器很可能会因为一个窗口在它不期望的时候移动,或者也许颜色或字体改变而卡在一个屏幕上。GUI 有太多的变量。使用命令行,一切都更加精确,你通过一个非常狭窄的不容错的接口进行通信,这就是为什么许多人类不喜欢它但其他计算机程序喜欢它的原因。

当然,也有 GUI 的不精确通信成为一种美德的情况。例如,在做图形艺术工作时,你通常不关心指定每个单独的像素阴影和颜色,但你确实希望某些东西被指定给每个像素。在这种情况下,当你移动光标时,你手的任何运动产生的噪声实际上成为最终产品的有意义的信息内容。


选择正确的语言

如果你阅读了关于技术债务的渐近复杂度的部分,你可能会得出这样的印象:你应该总是用具有非常具体接口契约的语言开始一个项目,比如 Haskell 或 Java。这根本不是我想传达的信息。如果你正在决定为特定项目使用哪种语言,我认为这个问题可能会有所帮助:

项目需求变更的可能性有多大?

如果你正在创业,答案几乎肯定是"非常大",特别是如果你正在从头开始构建一个小型产品并且仍在建立产品市场契合度。如果你已经确切地知道需求是什么,例如,如果你正在构建一个编译器,或基于国际标准的东西,那么你可能会回答"不太可能"。

如果你对这个问题的回答是"非常大",那么你可能想选择一种不会让你浪费大量时间指定接口契约的语言,因为当需求改变时,它们很可能会对你不利。毕竟,这个阶段的目标不是得到需求的完美实现,而是得到完美的需求,这样你就可以开始最终的实现。一个例外是,如果你的 MVP 实际上包含一个可能拥有数百个模块的巨大系统。如果已经有很多人参与构建软件,那么良好的接口契约将是必要的,以防止他们互相干扰。

如果你对这个问题的回答是"不太可能",那么你应该从具有非常强接口契约的语言开始。开始会更费力,但在第 1523 天添加新功能也会更容易。唯一的例外是,如果你正在编写一些小的东西(比如几百行)。

过去有很多关于 Twitter 如何开始使用 Ruby on Rails,后来因此遇到一些扩展问题的讨论。他们后来转向使用 Scala。有些人可能会声称这代表了一次失败,正确的决定是始终选择 Scala。我不相信这是真的。Twitter 本身的想法极其简单,所以有许多潜在的竞争者,他们最初的主要目标是获得足够的市场份额以占据主导地位。他们需要不惜一切代价尽可能快地增长。这意味着尽可能快地迭代功能,以找出人们真正想要他们构建的产品。扩展问题不是失败的症状,而是成功的症状。Twitter 实际"产品"的愿景已经清晰,剩下的就是去构建它。从开发者的角度来看,这是每个程序员都梦寐以求但从未经历过的涅槃般的境地:当你的老板说"用你最喜欢的语言,以你想要的任何方式从头开始重写这个糟糕的代码,以便以后更容易开发。"当你有了一个较弱的参考实现时,从头开始重写东西并不像弄清楚你实际上需要构建什么产品来创办一家火箭飞船公司那样费力。不幸的是,大多数公司只把这种转换看作是满足程序员强迫症的非必要成本,并浪费大量时间试图扩展一些根本不打算扩展的东西。


为什么 Python 如此受欢迎?

在关于泄漏的和特定的接口的部分,我讨论了如何根据接口对抽象泄漏的倾向性以及接口定义的具体程度对接口进行分类。我还指出了一个事实,即被认为更"用户友好"和"高效"的语言通常是处于这个谱系中高度泄漏和非特定的一端的语言。

我声称 Python 如此受欢迎的原因在于它是一种优秀的入门语言,因为它极其精简的接口契约。这也是为什么随着项目规模的增加,Python 变得难以维护的同样原因。

Python 在科学界也非常受欢迎,对于从事数值计算实验的人来说也是如此。实验的本质要求你不断地迭代你正在构建的东西的设计,因此更具体的接口会减慢实验过程。


为什么企业软件通常是 Java/C++?

我声称原因与上一节"为什么 Python 如此受欢迎?"中的原因完全相反。在关于泄漏的和特定的接口的部分,我讨论了与不同类型接口相关的权衡。Java 和 C++ 中的接口比 Python 或 Ruby 等其他语言中的接口更偏向谱系的特定端。C++ 和 Java 仍然可能是泄漏的,当然还有像 Haskell 这样更具体的语言,但 Java 和 C++ 似乎在可扩展性、用户友好性和迭代时间之间取得了平衡。这些语言还为程序员提供了灵活性,让他们可以根据项目约定来决定他们希望接口有多泄漏。一个例子是你可以如何根据项目需要将变量或函数设为私有、公有或受保护。


如何高效地抄近路

如果你从本文中应该带走一件事,那就是:如果你必须在项目中抄近路,请在实现内部进行,并用一个非常好的接口将其包裹起来。你可能会想,如果实现足够糟糕,那么该实现中的问题可能会泄漏到系统的其他部分,但它们不应该!如果它们泄漏了,我会称之为糟糕的接口设计!为了清晰起见,我将在这里明确列出我所说的"接口"是什么意思:

  • 函数原型
  • Java 的 interface
  • 公共类方法
  • 公共成员变量
  • C/C++ 中的头文件 (.h)
  • RESTful API 端点
  • URL 路由
  • "模块"或"包"的公开可见方面
  • 数据库模式 (DDL)
  • 许多其他…

结论

正如你所看到的,"接口"的概念是一个极其重要的概念,具有各种深远的影响。有法律后果、生产力后果,以及你可以与其他系统设计方面建立的许多非常哲学的联系。问任何有经验的程序员他们对接口的看法,你很可能会听到滔滔不绝的讲述。

最后感谢 James Hudon 对本文提供的一些反馈和修正。

posted @ 2025-06-11 12:04  ffl  阅读(328)  评论(0)    收藏  举报