SICP第二章提到了data-directed programming和message passing,及它们的关系。正好最近客户那边重构代码天天提data driven,而且message passing这个词一下让我想到了Smalltalk和OO,所以就把头绪理一下。
考虑一个非常常见的场景:你有两个不同的数据结构,它们其上各自有一组类似但不相同的操作。显然这些操作是针对于各自的具体数据类型的。现在的要求是:
- 提供一组通用操作作为这两个数据结构的外部接口。
- 要考虑将来加入新的具体数据类型对系统的影响。
- 要考虑将来加入新的操作对系统的影响。(感觉OO老是故意回避这个问题。事实上接口变动是很正常很频繁的。当然我设计模式学艺不精,请指正。)
按照OO和设计模式的思路,我们应该使用adapter设计模式。Adapter要实现某抽象数据类型,而这个抽象数据类型就是外部接口。client code仅使用此抽象数据类型,这样第一个要求就满足了。至于新加入的具体数据类型,大不了再加adapter就是,对现有代码无需做改动。不过加入新的操作就很麻烦了,每个具体数据类型都要改,抽象数据类型也要改。当然如果新加入的操作对每个具体类型都一样,可以放进抽象数据类型里。
在语言的实现方面,这需要是一个dynamic dispatch/binding的机制。Java和C++都采用了虚函数表,将对于抽象数据类型的操作在运行时映射到具体数据类型。Smalltalk和Python则采用了完全的动态也就是运行时查找,基本就是大查哈希表。
好吧现在我们剥掉OO那个外壳,回到Scheme的朴素思路。还是得有个抽象数据放在这儿当接口,在Scheme中没有object这个把数据和操作绑一起的东西,这个抽象数据其实就是一组通用操作。但现在语言不再自觉地替我们做这些dispatch,或者说将抽象映射到具体的工作了,我们得自己动手了。所以首先最弱智的我们可以用那一手generic operations with explicit dispatch:我们定义通用操作,然后在在其中显式的判断具体数据类型,再调用具体的操作。这一手确实把client code隔离开了,也就是满足了第一条,但显然每加一个具体数据类型都得在每个通用操作里加一个判断分支,没有满足第二条。这就是书中说的not additive。不过如果加一个操作的话还是挺方便的哈,老代码基本不用改。
第二手是用data-directed。其实质就是把那些判断也就是dispatch拿出来放一表或者说registry里。此表以同一具体数据类型的不同操作为列,以不同具体类型的相同操作为行(看SICP 181页或者自己画一个)。表中放的是某一具体数据类型的具体操作。通用操作不再充斥着写死的判断语句,而是接受操作名称作为参数,并从具体数据中获得类型信息,然后查表取得针对具体数据类型的操作并执行之。这样dispatch就成隐式的了,就是一个查表的过程。
所谓的data driven跟这个差不多就一个意思:把代码和数据严格分开,用数据驱动程序,新具体数据类型加进来的时候代码就不用改了。这样的代码就是additive的。详见ESR的Unix编程艺术和Wikipedia。
最后说message passing。它与generic operations with explicit dispatch是相对的。GOED是在operations里包含dispatch,其实质是,每一个operation其实就是data-directed里面的那个表里的一行,根据传入的数据类型自行解决dispatch问题。MP则是,把这个表分割成列,每个data type根据传入的operation自行解决dispatch问题。
在Scheme里表现出来就是,将数据表示为过程(带状态的过程,也就是闭包),其参数是operation。过程内部显式判断operation名称,再做或者调用具体操作。这其实已经是典型的OO特征了。显式判断operation名称就相当于调用实例方法啊!!!相比data-directed,显然是写死了。像Java这种OO语言呢里那些方法名不就是写死的么?当然动态语言除外。Python有个dict大家都知道。所以Scheme这里(object 'method)就相当于Java里object.method()啊!只不过这里dispatch是我们自己写的!而且这个dispatch不是动态的。
哈,object果然就是闭包!
然后SICP里为了提供Scheme风格的数据抽象,对MP也写了个apply-generic函数。据此我们可以再把那些通用操作写出来。比如
(define (real-part z) (apply-generic 'real-part z))
当新的具体数据类型加入时,我们需要写新的过程即可。但加入新操作就费劲了。注意MP和上面OO的局限多么的一致!其实它们的内在就是一样的!
基本思想是,如果我们把数据(具体数据类型)和代码(操作)视作正交,那么GOED和MP各自按住了一个,或者说基于其中一个做模块化,针对另一个做dispatch。所以遇到它们dispatch的那个东西要增加或者减少时,它们划分好的模块就被切了,怎么整都不爽。(想想那个表在按列切的时候多了一行,或按行切的时候多了一列,等于跨了已划分好的模块。)data-directed则是按照这两个维度做模块化,切得更细,需要两个坐标(具体数据类型,操作)才能找到目标。用这个思想看2.76那道题,一目了然。
私以为经验的重要性大致等同于知识,但二者皆低于思想。