2.4 模块和接口 - 保持整洁,保持基础接口稳定

目前构建大型系统已知方法是通过分而治之来增强抽象:将系统分解为称为模块的独立抽象。 我将模块的运行代码称为服务。 模块的规范有两件事:

  • 简化客户工作通过隐藏代码的复杂性来简化客户的工作(见上文),其次
  • 解耦客户与代码,从而使两者可以独立发展。

因此,许多人可以并行地在系统上高效地工作,而无需彼此交谈。一个真正成功的规范就像一个沙漏:该规范是一条狭窄的脖子,上面有很多客户,下面有很多代码,它可以生存数十年。示例:CPU ISA(指令集体系结构,例如x86和ARM),文件系统(Posix),数据库(SQL),编程语言(C,C ++,JavaScript)。网络中的示例,其中的接口尤为重要,因为没有权限协调模块及其客户端中的同时更改:以太网,可靠消息(TCP),Internet服务的名称(DNS),电子邮件地址,端到端安全性( TLS),网页(HTTP和HTML)。 Ousterhout的有关软件设计[^R69]的书中的许多小示例都强调了使规范比代码更小,更简单的重要性。由于规范体现了系统的多个部分(有时甚至是许多部分)所共有的设定,因此更改它的成本很高。

一种关于解耦的主流观点是,规范是客户端与服务之间的合同

  • 客户同意仅依赖规范描述的行为; 作为回报,客户只需知道规范说明中的内容,就可以依靠代码实际按照规范说明进行操作。
  • 该服务同意该模块将遵守规范; 作为回报,它可以在内部做任何喜欢的事情,并且不必交付规范中未规定的任何内容

许多人回避“规范”,而赞成“合同”,也许是因为他们认为规范太正式了。

通常将模块的规范称为接口,我也会这样做。 不幸的是,在通常情况下,接口是编译器或加载器处理的是非常不完整的规范,仅提供数据类型和名称(如果幸运的话)甚至会提供操作的参数,而不是提供操作如何操作状态。 甚至对状态的良好描述也经常丢失。

模块接口的功能与业务实体之间的合同具有相同的目的:通过简化和标准化通信来降低交易成本。 但是,与公司一样,有时您必须向内看,因为您不了解规范、规范不完整、或者只是不相信代码会真正满足要求。

模块边界不仅仅将其代码与客户端解耦; 它也可以使执行与资源消耗解耦。 如果接口是异步的,则任何一方都不会等待对方,因此无论客户端在做什么,模块都可以继续运行,反之亦然。 而且,它可以独立于客户端来管理其消耗存储和其他资源的方式。 因此,模块服务可以充当自治代理。 规格中如何显示? 典型的规范说明模块的内部状态足以描述其返回的结果,而完整的规范还描述了模块如何使用与客户端共享的任何资源。 由于自主模块不共享资源,因此其规范更加简单,包含该模块的系统将更加可靠并且更易于更改。 分布式事务是一个有趣的示例。

2.4.1 类与对象

模块上非常流行的变体将规范和代码附加到通常称为对象(object)的数据项上。您选择了一组称为方法的例程,这些例程采用与第一个参数相同的数据类型,并将其规范打包为一个规范,这里称为classpec(在Haskell中称为类型类,在Smalltalk中是协议,在Windows中是接口)。 Java(C ++和Python中的概念或抽象基类)。 classpec的代码是一个类(class),一个字典,将每个方法名称映射到其代码。 具有足够方法的类可以满足多个规范。 附加了类的正确类型的对象是该类(和classpec)的实例。 使用静态类型,您可以将类附加到类型而不是对象。

例如,classpec Ordered T可能具有方法eqlt,它们采用T类型的两个值,返回Bool,并满足部分订单的公理。 如果x是Ordered T的实例,则x.eq(y)调用eq方法(也许更漂亮的x == y); 要运行它,请在x的类中查找eq以获取该方法的代码,并以(x,y)作为其参数进行调用。 如果编译器知道该类,则可以进行查找。 这不是魔术,这些想法即使没有语言支持也可以起作用。 例如,在C语言中,方法查找是显式的:对象x是具有指向字典的类字段的结构,这是另一个具有eq指向方法代码的结构的结构,然后编写x.eq(y)x->class->eq(x,y)

在类中添加方法将产生一个子类,该子类继承了父类方法(对于classpec同样如此)。 因此,Ordered T是仅具有eq方法的Equal T类的子类。Ordered T的实例也是Equal T的实例。子类为添加的方法提供代码,并且它也可以替换或覆盖父类方法。 子类应满足父类的类,这是其客户可以依靠的。 这样,当在期望父类的代码中使用子类对象时,就不会感到惊讶。 确保这一点的一种简单方法是不覆盖父类方法,并防止添加的方法接触父类的私有数据。 Java中的final修饰符强制执行此操作,但是继承通常不执行。 打破父类抽象非常容易,因为

  • 通常规范是非常不完善的
  • 实际上现有技术水平很难证明正确性
  • 最多可以保证方法名称和类型一致。

这里有两种截然不同的想法:隐藏(抽象)和重载(对于多个事物使用相同的名称)。

  • 该类执行通常的抽象工作,描述对象的行为并向客户端隐藏其代码。
  • 该类为其方法提供重载,从而可以轻松地使用该类的本地名称来调用它们,但与具有相同规格或密切相关的其他类中的方法名称相同。

当不同的重载方法确实满足相同的规范时,这两件事可以一起工作,但是当它们不满足时,它们可能会成为错误的丰富来源,因为没有机械的方法可以分辨。 当同一个团队同时拥有父类和子类时,这可能没问题,但是如果父类拥有许多独立客户,这是非常危险的,因为没有一方可以确保子类满足其规格。 当心继承。

OrderedEq示例很不寻常,因为与大多数对象不同,它们没有状态(数据)。 通常,对象的代码状态是一组命名字段,例如,具有xy字段的Point类。 状态可以是不可变的,以便像pt1.add(pt2)之类的操作返回一个新对象,或者它可以更改,以便file.write(i,b)将文件的字节i替换为b。 由于对象是具有抽象状态的模块,因此如果代码状态与规范状态相同,则它实际上并没有完成其工作。 特别是,如果字段xsetx方法,则是一个不好的信号; 该对象看起来像是昂贵的记录或结构。

许多语言以各种令人困惑的方式嵌入类。 它有助于将它们分为两个主要类别。

基于对象:对象托管类,如Smalltalk或Java。 当然,要知道调用哪种方法有意义,程序员需要知道一个表达式的类实现什么类。 在Smalltalk中,程序员必须对此进行跟踪(如果发现错误,则会出现““method not understood”错误)。 在Java中,表达式的类型会静态地告诉您,但是对象的类可能是该类型的子类,因此编译器不知道该方法的实际代码(除非是final)。

基于类型的:如Haskell或大多数语言中的内置方法(如“ +”)一样,一种类型托管该类。 例如,对于给定的适当代码,IntegerOrdered提供的代码的宿主:integerEq 对应 ==integerLF 对应 <。如果类型具有所有方法的代码,则该类型可以承载多个类。 因为方法代码是类型的一部分,所以除非类型是参数,否则编译器会知道它。 在这种情况下,类字典是运行时参数。

2.4.2 层与平台

2.4.2-1

典型的系统有很多模块,当模块规格更改时,您需要知道谁依赖它。 为了简化此过程,请将相关模块放到一个中,一个团队或供应商可以运送一个单元,而客户可以理解。 该层仅公开选定的接口,不允许较低的层在较高的层中调用例程。 因此,一层是一个大模块,通常是其宿主的客户端,位于其下方的单个层,在其上方的一层或多层是其客户端。 因此,尽管下面的乌龟示例有一些例外,但术语“层”还是有意义的。 有时,一个层是几个主机的客户端。 例如,每一层都是CPU硬件及其下一层的客户端。

通常,您是在平台上构建系统,该平台是为不同客户提供服务的大层,并且来自其他组织。 常见的平台是基于操作系统平台(Windows或Linux;接口是内核和库调用)构建的浏览器(接口是通过JavaScript访问的文档对象模型)或数据库系统(接口是SQL)。 硬件平台(Intel x86或ARM;接口是ISA)。 通常ISA是这张照片中的最低层,但它始终是乌龟:硬件是建立在栅极和存储单元上的,而存储单元是建立在晶体管上的,而晶体管是根据电子遵循量子力学定律的。 这是所有海龟的示例:

类别 具体
应用 Gmail
web 框架 Django
数据库 、浏览器 BigTable 、Chrome
操作系统 Windows 10
虚拟机 VMware
指令集 x86
CPU AMD Ryzen 7 2700X
门、 存储 TSMC 7 nm Micron MT40A16G4
晶体管 7 nm finFET LPDDR4X-4266

名人的哔哔声。Microsoft Windows发出声音(通常称为哔哔声)以通知用户各种情况。 其中一些情况发生在系统的低级别。 一方面,他们引入了层来控制依赖关系,其中大约有50个。 然后有人通过类似于名人铃声的方式想到了名人哔哔声。 但是,当然,名人哔哔声是需要进行数字版权管理的有价值的财产,需要在45级完成。这意味着在10级哔哔声的代码调用到45级。当签入工具拒绝此操作时,开发人员感到困惑——他们不明白他们在做什么错。

2.4.3 组件

重用代码段就像从别人的故事中摘取句子,然后尝试撰写杂志文章。 —鲍勃·弗兰克斯顿(Bob Frankston)[^ Q28] 阅读代码要比编写代码难。 —Joel Spolsky[^ Q73]

经过设计可在多个系统中重用的模块称为组件。 显然,找到一个可以满足您需要的组件比自己建造一个组件要好(不要重新发明轮子),但是有一些陷阱:

  • 您需要了解其规格,包括其性能
  • 您需要确信其代码实际上符合规范并会得到维护。
  • 如果它不能完全满足您的要求,则必须填补空白。
  • 您的环境必须满足组件所做的假设:组件如何分配资源,组件如何处理初始化,异常和故障,组件的配置和自定义方式以及所依赖的接口。

构建真正可重用的组件的成本是构建可以在一个系统中发挥出色作用的模块的成本的几倍,而且通常没有商业模型可以支付此成本。因此,广告中的组件可能无法满足您对可靠,可维护的系统的需求,尽管如果可靠性不是至关重要的(例如,对于近似软件),仍然可以。

有两种方法可以防止陷入这些陷阱之一:

  • 将模块的代码复制并粘贴到您的系统中,然后进行必要的任何更改。这通常对于小型组件而言是正确的选择,因为它避免了上面列出的问题。缺点是难以跟上 错误修复或改进。
  • 坚持通常称为平台的超大型组件。 只有少数几个要学习,它们封装了许多艰苦的工程工作,并且由于拥有可行的业务模型而存在了很长一段时间(因为编写自己的数据库或浏览器是不切实际的)。 维护良好的库也可以是比整个平台小的安全组件的来源。

2.4.4 放开系统-不要隐藏,给客户以权力

抽象的目的是隐藏代码的工作方式,但是它不应阻止客户端使用其主机的全部功能。抽象可以抢占其客户可以做出的决定;例如,其缓冲I/O的方式可能会使设备无法以其全部带宽运行。如果它是一个普通模块,则客户端总是可以侵入其中,但是,如果它是隔离其客户端的操作系统,或者您想继续进行错误修复,则不是一个明智的选择。另一种选择是精心设计,它不会隐藏功能,但可以让客户访问所有潜在的性能。调度程序激活就是一个例子;它们不如线程方便,但可以让客户端控制调度和上下文切换。同样,信号量和监视器无法控制等待监视器锁或条件变量的进程的调度这一事实,使Exokernels进一步将这一想法付诸实践,将OS平台的大部分代码移动到了一个OS中,客户端可以在此OS上进行更改。通常被称为缺点的方法实际上是一种优势,因为它使客户可以自由地提供所需的计划。

公开抽象功能(并使其可扩展)的另一种方法是通过对客户端提供的函数进行回调或通过以特定于应用程序的指令集编写的程序来对其进行编程。 有很多这样的例子:

  • SQL查询语言,一种功能指令集。
  • 显示列表和更多精致的GPU程序。
  • 软件定义的网络
  • 二进制修补。

修补程序首先在Informer(用于OS内核的检测工具)中进行;它检查了所提议的机器代码补丁以确保安全。[^R28]随后,当源不可用时,使用二进制修改工具来检测和优化二进制文件。[^R82] , [^R60]可以像伯克利数据包那样检测网络代码。 过滤并执行软件故障隔离(SFI)[^R92]。您也可以使用其他语言修补源代码。

这种可编程性最初仅适用于非常特定的应用程序,但在它成为一台功能完善的计算机之前,通常涉及更多用途。 然后循环可以再次开始。[^R64]

隐藏秘密也并不意味着代码应该是秘密的。 诸如Linux,Apache,gcc,EMACS和llvm之类的开源系统的成功展示了让很多人阅读代码并做出贡献的价值。 这对于安全性尤其重要,因为当攻击可能来自任何地方并且有强大的分析工具时,通过模糊性进行的安全性无法正常工作。 但是,很多人并不能代替全面的测试和代码验证。

2.4.5 鲁棒性-离散,弹性部分

鲁棒性原则:严以律己,宽以待人。——乔恩·波斯特尔[^Q65]

需要围绕规范的强度进行权衡。 规范越强,对客户的承诺就越多,并且很难编写代码。 较宽松(较弱)的规范承诺较少,并且更易于编码。 但是,如果您想构建一个强大的系统,最好是对客户更保守,对服务更自由。 保守的客户尝试仅依靠规范的基本功能(通过猜测它们是什么,或研究其他成功的客户)。 如果该服务显示为不稳定(即未正确实施整个规范),则该客户端仍将正常工作。 自由派服务人员会竭尽所能地预见客户的错误并适应错误。 尽管存在不可避免的错误和误解,但最终的系统更有可能正常工作。 这适用于标准配置,通常具有许多不必要的功能,这些功能实际上并没有起作用。

2.4.6 标准

提问:当您越过具有国际标准的流氓时,您会得到什么?

回答:某人为您提供了您无法理解的报价。——Paul Mockapetris[^54]

被广泛接受或通过公认的过程达成一致的规范称为标准。 有时它们是非常成功的:以太网,IBM PC,USB,TCP / IP,HTML,C,C ++,Java脚本,PostScript,PDF,RSA和Linux是明显的例子。 值得注意的是,所有这些最初都是由一个人或一个小组设计的,而不是由公认的流程设计的。 实际上,大多数标准委员会最初制定的标准最终都是scrap花一现,因为这些承诺委员会是政治动物,倾向于驱逐优秀工程师,并通过包括每个人的想法而采取最少抵抗的道路。 示例:OSI网络,R74IPsec网络安全性,IPv6,XML,UML,ATM网络。

政府为满足自身需求而制定的标准很可能会失败。美国国防部至少有两个主要的计算标准失败,即Ada编程语言和用于多级安全的Orange Book标准。

另一方面,对于以临时形式开始并在委员会中成功继承的标准来说,这是很好的,该委员会负责无聊的任务,以一种保守的,向后兼容的方式发展它以满足新的需求,这通常就是这种情况。 由于大多数新想法都失败了,因此对失败的标准过分地对待也不公平。

关于标准的重要一点是:已经存在了一段时间并且已知具有良好实现的标准为您提供了一个稳定的构建基础。但是健壮性意味着坚持使用最简单,使用最频繁的部件是明智的; 其余部分很可能设计或实施不当。如果没有有效的代码,请远离它。

»X.509。有时设计不当,过于复杂的委员会标准确实成立。 令人惊讶的例子是用于数字签名证书的X.509标准,这是OSI网络中唯一幸存的部分。 真是一团糟,每当一个房间里的两个X.509专家问一个问题时,我至少都会得到两个答案。

»WEP。有时候,出于政治原因,委员会的一些成员故意或由于误导了标准而弄乱了标准。支持Wi-Fi安全的有线等效保密(WEP)标准,外汇示例,缺陷众所周知 给加密协议方面的专家,但是他们没有被咨询或被忽略.[^R10]