第十五课 - 面向对象编程(3) - Scheme 实现(2)
self
很多时候,一个对象需要在一个 procedure 中调用自己的另一个 procedure,但它没有相关的 reference,这时候就需要一个变量 self,它始终指向这个对象自己,从而达到调用自己的 procedure 的目的。
首先,需要为每个 procedure 增加 self 引用,然后就可以利用这个 self 去调用自己的 procedure:
(define (make-person fname lname)
(lambda (message)
(case message
((WHOAREYOU?) (lambda (self) fname)
((CHANGE-NAME)
(lambda (self new-name)
(set! fname new-name)
(ask self 'SAY (list 'call 'me fname))))
((SAY)
(lambda (self list-of-stuff)
(display-message list-of-stuff)
'NUF-SAID))
(else (no-method)))))
接着,需要修改 ask
(define (ask object message . args)
(let ((method (get-method message object)))
(if (method? method)
(apply method object args)
(error "No method for message" message))))
从以上代码中,可以体会到将寻找 procedure 和调用 procedure 的逻辑抽象到 ask 中,也能让我们很方便地做这种额外的改动。
object typing
在面向对象系统中,常常需要知道某对象的类型,从而构建对不同类型对象的处理逻辑。其中最简单的一种方法就是在对象中添加一个 procedure
(define (make-person fname lname)
(lambda (message)
(case message
((WHOAREYOU?) (lambda (self) fname))
((CHANGE-NAME)
(lambda (self new-name)
(set! fname new-name)
(ask self 'SAY (list 'call 'me fname))))
((SAY)
(lambda (self list-of-stuff)
(display-message list-of-stuff)
'NUF-SAID))
((PERSON?)
(lambda (self) #t))
(else (no-method)))))
(define someone (make-person 'bert 'sesame))
(ask someone 'person?)
> #t
这种方法简单,但弊端也很明显,如果我们 (ask someone 'professor),就会得到 no-method,但我们同样可以利用类似 ask 抽象的方式解决这个问题:
(define (is-a object type-pred)
(if (not (procedure? Object))
#f
(let ((method (get-method type-pred object)))
(if (method? Method)
(ask object type-pred)
#f)))))
(define someone (make-person 'bert 'sesame))
(ask someone 'professor?)
> #f
Inheritance
Internal object
继承 (inheritance) 是面向对象系统中重要的一员,它可以将系统中的个体按层级抽象,将不同个体的共同特征单独抽象,使得代码往模块化更进一步。基于之前的面向对象系统设计,我们可以在子类实例内部创建一个父类的实例,当子类中找不到与 message 相对应的 procedure 时,将 message 传递给内部的父类实例,从而实现局部变量和 procedure 的继承,以 professor 和 person 为例:
(define (make-professor fname lname)
(let ((int-person (make-person fname lname)))
(lambda (message)
(case message
((LECTURE) ...) ; new method
((WHOAREYOU?
(lambda (self)
(display-message (list 'Professor lname))
lname))
(else (get-method message int-person))))))
(define e (make-professor 'eric 'grimson))
执行最后一句 define,我们在全局环境上创建一个 professor 实例,此时环境模型如下图所示:
当执行 professor 特有的 procedure 时,可以得到如下环境模型图:
当执行 person 特有的 procedure 时,可以得到如下环境模型图:
环境模型图中展现出整个继承的过程,值得回味。
Delegation
拥有 internal object 能够将子类共用的局部变量和 procedure 抽象到父类中,但有时候子类的某个 procedure 常常是父类的某个 procedure 的改进版本,为了避免重复父子类中相似 procedure 中的共同逻辑,我们需要 delegation,来实现子类对父类 procedure 的调用。
首先,构建一个 delegate procedure
(define (delegate to from message . args)
(let ((method (get-method message to)))
(if (method? method)
(apply method from args) ; from becomes self
(error "No method" message))))
; 对比 ask
(define (ask object message . args)
(let ((method (get-method message object)))
(if (method? method)
(apply method object args) ; object becomes self
(error "No method for message" message)
delegate 与 ask 非常相似,唯一的不同在于 delegate 是从 to 身上找到 method,然后执行的时候用 from 当作 self 传入,举例:子类实例拿父类的方法应用到自己身上,而不是父类的示例身上。
继续之前的例子,创建一个 arrogant-professor,它的 SAY procedure 会在自己说的每句话之后加上 obviously,利用 delegate 实现如下:
(define (make-arrogant-professor fname lname) ; subclass
(let ((int-prof (make-professor fname lname))) ; superclass
(lambda (message)
((SAY)
(lambda (self stuff)
(delegate int-prof self
'SAY (append stuff '(obviously)))))
(else
(get-method message int-prof))))))
(define e (make-arrogant-professor 'big 'gun))
(ask e 'SAY '(the sky is blue))
> the sky is blue obviously
(ask e 'LECTURE '(the sky is blue))
> therefore the sky is blue
调用 SAY 时,arrogant-professor 实例如愿在自己说的话后面加上 obviously,然而调用 professor 实例的 LECTURE 时,并没有如愿。仔细看一下 make-professor 的源码:
(define (make-professor name)
(let ((int-person (make-person name)))
(lambda (message)
(case message
((LECTURE)
(lambda (self stuff)
; bug (delegate int-person self 'SAY
; bug (append '(therefore) stuff))
(ask self 'SAY
(append '(therefore) stuff))))
(else (get-method message int-person))))))
原因在于arrogant professor 内部的 professor 实例内部调用 SAY 时,使用的并不是 arrogant-professor 本身的 SAY,而是 professor 实例内的 SAY,因此 obviously 没有被加在每句话之后。因此稍加改动就能实现我们最初的目的。本例也能体会出,在面向对象系统设计过程中,在重用 procedure 过程中的一些微妙的变化。
Multiple Inheritance
假设系统中有新的类 Singer,它没有父类,它的 constructor 如下所示:
(define (make-singer)
(lambda (message)
(case message
((SAY)
(lambda (self stuff)
(display-message
(append stuff '(tra lala))))
((SING)
(lambda (self)
(ask self 'SAY '(the hills are alive))))
(else (no-method)))))
这时候如果有一个新的类,它既是 Arrogant Professor 又是 Singer,暂且称它为 SAP, 这时候就出现 multiple inheritance,沿用之前的设计,我们可以在 SAP 实例内部创建一个 Arrogant Professor 实例和一个 Singer 实例:
(define (make-s-a-p fname lname)
(let ((int-singer (make-singer))
(int-arrognt (make-arrogant-prof fname lname)))
(lambda (message)
(find-method message int-singer int-arrognt))))
(define (find-method message . objects)
(define (try objects)
(if (null? objects)
(no-method)
(let ((method (get-method message (car objects))))
(if (not (eq? method (no-method)))
method
(try (cdr objects))))))
(try objects))
当一个类继承两个类时,就需要决定先从哪个父类寻找 procedure,我们甚至也可以让每个父类都执行对应的 procedure。这些都是设计面向对象系统的需要做的一些决定。