这是一本关于CLOS编程的书籍. 当然, 因为CLOS既典型又一般, 所以也适合对于OOP感兴趣的普罗大众.
许多计算机程序创建对象
并操纵它们. 有时这些对象表示真实世界的东西. 一个交通模拟程序需要表示机动车, 行人, 交叉路口, 和红绿灯. 在其他一些情况下, 程序操纵表示抽象的对象, 例如操作系统工程中的缓冲区, 窗口, 和进程.
Common Lisp Object System (CLOS) 支持被称为面向对象编程的编程风格, 这使得创建和操纵对象变得容易起来. CLOS鼓励软件开发者创建基于对象的结构和行为来描述各种对象的类的工作模型. 经常的情况是, 工作模型包含相互关联的类, 它们类似但并不完全相同. 例如, 窗口系统通常需要为了各种不同的目的支持各种不同种类的窗口. 有的窗口可能具有边框, 有的窗口可能具有标签, 还有的窗口可能兼具边框和标签. 窗口系统的设计可能想要容纳各种窗口的类.
CLOS使得表示类之间的关系变得容易, 并且它提供了继承 (或者说共享) 结构和行为的灵活手段. 继承允许应用程序的设计和实现高度模块化, 并消除了维护诸多近乎相同的代码片段的需要.
CLOS程序的要素是类, 实例, 通用函数 (generic function), 和方法. 以上这些要素都不能单独考虑, 因为其目的都在于以有用且可预测的方式与其他要素进行交互. 我们首先呈现这些要素最重要的方面, 并检视这些元素之间的关系. 然后, 我们描述CLOS是如何与Common Lisp集成的, 专注于类 (class) 和类型 (type) 的共同基础.
编写CLOS程序的第一步是定义一种新的数据结构类型, 其被称为类. 一个类是一个Common Lisp类型. 每个具有该类型的对象都是这个类的一个实例. 一个给定类的每个实例都和该类的其他实例一样拥有相同的结构, 行为, 和类型.
我们或许想要定义一个叫做month
的类型, 其拥有表示一月, 二月, 等等的实例. 或者, 我们可能要定义一个叫做window
来表示出现在显示终端屏幕上的窗口. 当我们需要创建一个新的窗口时, 我们就作成了该类的一个新实例. 图2.1展示了一个拥有三个实例的类.
我们可以使用通常的Common Lisp的类型函数来查询一个实例的类型. 尽管(某个固定的类的)所有实例都可知拥有相同的类型, 但是它们每个都拥有单独的identity. 这与Common Lisp的模型相兼容, 其中两个对象可以拥有相同的类型和相同的结构 (例如具有相同内容的两个数组), 但是却是两个不同的对象, 每个都拥有其自己的identity. [译注: 对于我这样一个Schemer而言, 我会想到eq?
和equal?
, 不过Common Lisp中也有类似的函数.]
我们已经说过一个类的所有实例都具有相同的结构. 这种结构是以槽的形式出现的. 一个槽拥有一个名字和一个值. 一个槽的名字描述了其所模拟的特征, 而值描述了槽在某个给定时间的状态. 这个状态信息可以通过accessor读取和写入.
CLOS提供了两种槽: 局部槽和共享槽. 对于局部槽, 每个实例都存放着其自身对于这个槽的值. 对于共享槽, 所有的实例对于这个槽都共享着同一个值. 既然局部槽用的更多, 所以我们现在就专注于局部槽. 我们在局部槽和共享槽
中讨论了共享槽.
名为window
的类可能有着叫做x-position
, y-position
, width
, height
的局部槽. 这些状态信息描述了, 对于任意给定窗口而言, 其大小和坐标. 图2.2展示了window
类的两个实例的槽的名字和值.
注意到相同的类的两个实例有着相同的命名槽之集. 换言之, 它们有着相同的结构. 然而, 每个实例都维护着自己对于其局部槽的值, 即每个实例都有着自己的状态.
CLOS允许你从其他类中构造一个类出来, 这些作为组件 (component) 的类被称为这个新类的超类. 新类既继承了其超类的结构 (槽), 也继承了其超类的行为.
这种编程风格适合处理建模相互关联的各种对象的任务. 例如, 我们或许想要各种不同的窗口. 除了朴素的窗口之外, 我们或许还需要带有标签的窗口和带有边框的窗口. 窗口的新种类类似于既有的window
类, 但是它们具有额外的特性. 图2.3展示了两个新的类, window-with-label
和window-with-border
, 其建立在既有的类window
之上.
为了从诸多组件中构造一个类, 你需要在类的定义中列出这些类. 这些类被称为新类的直接超类. 在图2.3中, 每个箭头都从一个类指向一个其直接超类. 实际上, 一个类不仅构建于其直接超类的基础之上, 还要包括这些超类的每个直接超类, 循此反复. 一个类的超类就是(前一句话提及的)所有其组件类. 术语子类是超类的逆. 现在我们将此术语应用于window
类:
window
是window-with-border
的一个直接超类.window
是window-with-label
的一个直接超类.window-with-border
是window
的一个直接子类.window-with-label
是window
的一个直接子类.window-with-label
的一个实例和window-with-border
的一个实例. 类window-with-label
继承了其超类window
的四个槽, 并且还有一个叫做label
的槽. 类似地, 类window-with-border
继承了window
的所有槽, 并具有一个名为border-width
的槽. 因此, 一个窗口的基本结构只被定义了一次 (通过类window
) 并被许多窗口种类继承.槽在内存中的顺序依赖于实现, 通常对于程序员而言并不可见.
程序和用户通过使用通用函数来施行实例上的操作. 对于调用者而言, 通用函数表现得就和正常的Lisp函数一模一样, 函数调用的句法是完全等同的. 当你调用函数时, 你无需知道这个函数是一个正常的函数还是一个通用函数.
从概念上说, 一个通用函数执行了某种高层次的操作, 例如刷新窗口
. 对于不同种类的窗口, 这项操作可能需要不同的工作; 尽管对于简单窗口而言, 工作可能就是直接清空, 对于带边框的窗口而言, 清空之后还必须重绘其边框. 高层次的目标刷新窗口
必须针对不同种类的窗口而以相应的不同方式实现. 换言之, 每种窗口都需要一个适合于自身的实现.
当我们比较正常函数和通用函数的运作方式时, 我们发现了语义上的不同之处. 一个正常的Lisp函数既描述其接口, 也描述对于其所执行的操作的实现. 正如图2.5所示, 当一个正常的Lisp函数被调用时, Lisp系统定位并执行实现该函数的单一的代码体.
一个通用函数仅描述其接口. 对于一个通用函数的实现并不存在于一处, 而是散布于一集方法. 尽管一个正常的函数在各种调用下的实现都是相同的, 通用函数的实现的确会有所不同, 依赖于其参数的类.
考虑刷新三种窗口的任务. 我们可以定义一个称为refresh
的通用函数, 其可以用来刷新各种窗口. 不管窗口的类如何, 接口是一样的. 然而, 每个窗口的类都需要稍微不同的对于refresh
的实现. window
的实例是会被直接清空; 即屏幕上由这种窗口覆盖的区域会被变为空白. 对于window-with-border
的实例, 窗口会被清空而边框将被重绘. 类似地, 对于window-with-label
的实例, 窗口会被清空而标签将被重绘. 图2.6展示了一个通用函数可以拥有多个单独的实现.
当refresh
被调用时, CLOS会确定参数的类并选择针对该类的合适实现. 每个实现可以由一个方法构成, 也可以由数个方法构成. 确定应该调用什么过程并调用它们的过程被称为通用分派(generic dispatch). 每当一个通用函数被调用时, 该过程都会自动发生.
在refresh
这个例子里, 通用分派只使用一个参数 (即窗口) 来选择实现. 在多方法 (Multi-Methods)
一节中, 我们将展示CLOS的通用分派也可以利用不止一个参数来选择实现.