4.4 深模块
Last updated
Last updated
最好的模块是那些提供强大功能而又有简单接口的模块。我使用“深(deep)”一词来描述这种模块。为了直观地了解深度的概念,想象一下每个模块用一个矩形来表示,如图4.1所示。每个矩形的面积与该模块实现的功能成正比。矩形的上边代表模块的接口;这条边的长度表示接口的复杂性。最好的模块是有深度的:它们有很多功能隐藏在简单的界面后面。一个深模块是一个很好的抽象,因为只有一小部分的内部复杂性对用户来说是可见的。
模块深度是一种思考成本与收益的方式。一个模块提供的收益是它的功能。一个模块的成本(就系统的复杂性而言)是它的接口。一个模块的接口代表了该模块对系统其他部分的复杂性:接口越小、越简单,它引入的复杂性就越少。最好的模块是那些具有最大收益和最小成本的模块。接口很好,但更多或更大的接口不一定更好!
Unix 操作系统及其后代(如 Linux)提供的文件 I/O 机制是深接口的一个很好的例子。 I/O 只有五个基本的系统调用,带有简单的签名:
open
系统调用接收一个分层的文件名,如/a/b/c
,并返回一个整数的文件描述符,用来引用打开的文件。open
的其他参数提供了可选的信息,如文件是为读还是为写而打开的,如果没有现有的文件,是否应该创建一个新的文件,以及如果创建一个新的文件,该文件的访问权限是怎样的。read
和write
系统调用在应用程序内存的缓冲区和文件之间传输信息;close
则结束对文件的访问。大多数文件是按顺序访问的,所以这是默认的;然而,通过调用lseek
系统调用来改变当前的访问位置,能够实现随机访问。
Unix I/O 接口的现代实现需要数十万行代码,这些代码解决了以下复杂问题:
为了允许高效访问,文件如何在磁盘上表示?
目录是如何存储的,以及如何处理分层路径名以找到它们所引用的文件?
如何强制执行权限,以使一个用户无法修改或删除另一个用户的文件?
文件访问是如何实现的? 例如,中断处理程序和后台代码之间的功能如何划分,这两个元素如何安全通信?
当存在对多个文件的并发访问时,使用什么调度策略?
如何将最近访问的文件数据缓存在内存中,以减少磁盘访问的次数?
如何将各种不同的辅助存储设备(例如磁盘和闪存驱动器)合并到一个文件系统中?
所有这些问题,以及更多的问题,都由Unix文件系统的实现来处理;它们对调用系统调用的程序员是不可见的。多年来,Unix I/O 接口的实现发生了翻天覆地的变化,但是五个基本的内核调用并没有改变。
另一个深模块的例子是Go或Java等语言中的垃圾收集器。这个模块完全没有接口;它在幕后无形地工作,回收未使用的内存。在一个系统中添加垃圾收集器实际上缩小了它的整体接口,因为它取消了释放对象的接口。垃圾收集器的实现是相当复杂的,但这种复杂性对使用该语言的程序员是隐藏的。
像Unix I/O和垃圾收集器这样的深模块提供了强大的抽象,因为它们易于使用,同时隐藏了重要的实现复杂性。