4.3 用原型编程
具有类似我们在本章中介绍的委托机制的基于对象的语言被称为 基于原型的语言 (prototype),例如 Self,JavaScript 和 AmbientTalk 等等。这些语言擅长什么?如何使用原型编程?
4.3.1 单例和特殊对象
由于对象可以无中生有地创建(即,用类似于 OBJECT-DEL
的对象字面表达式创建),所以自然地可以创建只包含一个实例的类型的对象实例。与基于类的语言需要一个特定的设计模式(称为单例(Singleton))相反,基于对象的语言非常适合这种情况,也适合创建“特殊”对象(下面会详细介绍)。
我们先来考虑布尔值的面向对象表示和简单的 if-then-else
控制结构。有多少种布尔值?只有两个:真和假。所以我们可以创建两个独立的对象, true
和 false
来表示它们。在像 Self 和 Smalltalk 这样的纯面向对象的语言中,像 if-then-else
, while
等这样的控制结构在语言中不是基本指令。相反,它们被定义为某些对象的方法。我们来考虑 if-then-else
的情况。我们可以给一个布尔值传两个 thunk(译注,无参数的 lambda,即 (lambda () ...)
),一个真 thunk 和一个假 thunk;如果布尔值 是 true,它会调用真 thunk;如果它 是 false,它会调用假 thunk。
(define true
(OBJECT-DEL root ()
([method ifTrueFalse (t f) (t)])))
(define false
(OBJECT-DEL root ()
([method ifTrueFalse (t f) (f)])))
怎么能使用这些对象?举个例子:
(define light
(OBJECT-DEL root
([field on false])
([method turn-on () (set! on true)]
[method turn-off () (set! on false)]
[method on? () on])))
> (-> (-> light on?) ifTrueFalse (λ () "灯开了")
(λ () "灯关了"))
"灯关了"
> (-> light turn-on)
> (-> (-> light on?) ifTrueFalse (λ () "灯开了")
(λ () "灯关了"))
"灯开了"
对象 true
和 false
是布尔值的唯二表示。任何依赖某个表达式为真或假的条件机制都可以类似地定义为这两个对象的方法。这就是动态分发!
Smalltalk 中的布尔值和控制结构就是这么定义的,不过,由于 Smalltalk 是基于类的语言,它们的定义更加复杂些。用你最喜欢的基于类的语言来试试看。
我们再来看一个基于对象语言的实用例子:特殊(exceptional)对象。先来回顾一下普通点对象的定义,一般是调用工厂函数 make-point
创建的:
(define (make-point x-init y-init)
(OBJECT-DEL root
([field x x-init]
[field y y-init])
([method x? () x]
[method y? () y])))
假设我们要引入一个特殊的点对象,它的特殊性在于坐标是 随机的 ,每次访问都会改变。我们可以简单地定义 random-point
为一个独立的对象,其 x?
和 y?
方法执行计算而不是访问存储的状态:
(define random-point
(OBJECT-DEL root ()
([method x? () (* 10 (random))]
[method y? () (-> self x?)])))
请注意, random-point
没有声明任何字段。当然,因为在 OOP 中我们依赖的是对象的接口,两种表示可以共存。
4.3.2 通过委托共享
上面讨论的例子突出了基于对象的语言的优点。现在让我们看看实际使用中的委托。首先,委托可以用来分解对象之间的 共享行为 。考虑这种情况:
(define (make-point x-init y-init)
(OBJECT-DEL root
([field x x-init]
[field y y-init])
([method x? () x]
[method y? () y]
[method above (p2)
(if (> (-> p2 y?) (-> self y?))
p2
self)]
[method add (p2)
(make-point (+ (-> self x?)
(-> p2 x?))
(+ (-> self y?)
(-> p2 y?)))])))
创建的所有点对象都具有相同的方法,因此这些行为可以移至公共的父对象(通常称为原型)中,以实现共享。所有的行为都应该移到原型中吗?如果我们想要允许点的不同表示,比如前面的随机点(它根本不含任何字段!),就不该这么做。
因此,我们可以定义 point
原型,它提取了 above
和 add
方法,它们的实现对所有点都是一样的:
(define point
(OBJECT-DEL root ()
([method above (p2)
(if (> (-> p2 y?) (-> self y?))
p2
self)]
[method add (p2)
(make-point (+ (-> self x?)
(-> p2 x?))
(+ (-> self y?)
(-> p2 y?)))])))
如果使用的语言支持抽象方法的话,
point
中这些选择器(accessor) 方法可以定义为抽象(abstract) 的。Smalltalk 就可以这么做,这种方法被调用的话就会抛出异常。
请注意,作为一个独立的对象, point
没有意义,因为它给自己发送自已也不理解的消息。但它可以作为原型,其他点可以扩展之。比如用 make-point
创建的普通点,包含字段 x
和 y
:
(define (make-point x-init y-init)
(OBJECT-DEL point
([field x x-init]
[field y y-init])
([method x? () x]
[method y? () y])))
也可以是特殊的点:
(define random-point
(OBJECT-DEL point ()
([method x? () (* 10 (random))]
[method y? () (-> self x?)])))
正如我们所说的,这些不同类型的点相互合作,它们都理解 point
原型中定义的消息:
> (define p1 (make-point 1 2))
> (define p2 (-> random-point add p1))
> (-> (-> p2 above p1) x?)
8.90016724570533
同样,我们可以用委托来共享对象之间的 状态 。例如,考虑一组共享相同 x 坐标的点:
(define 1D-point
(OBJECT-DEL point
([field x 5])
([method x? () x]
[method x! (nx) (set! x nx)])))
(define (make-point-shared y-init)
(OBJECT-DEL 1D-point
([field y y-init])
([method y? () y]
[method y! (ny) (set! y ny)])))
所有由 make-point-shared
创建的对象共享同一个父对象 1D-point
,由它决定 x
坐标。如果改变 1D-point
,自然会反映到所有子对象上:
> (define p1 (make-point-shared 2))
> (define p2 (make-point-shared 4))
> (-> p1 x?)
5
> (-> p2 x?)
5
> (-> 1D-point x! 10)
> (-> p1 x?)
10
> (-> p2 x?)
10
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论