data-directed与message passing,及OO

SICP第二章提到了data-directed programming和message passing,及它们的关系。正好最近客户那边重构代码天天提data driven,而且message passing这个词一下让我想到了Smalltalk和OO,所以就把头绪理一下。

考虑一个非常常见的场景:你有两个不同的数据结构,它们其上各自有一组类似但不相同的操作。显然这些操作是针对于各自的具体数据类型的。现在的要求是:

  1. 提供一组通用操作作为这两个数据结构的外部接口。
  2. 要考虑将来加入新的具体数据类型对系统的影响。
  3. 要考虑将来加入新的操作对系统的影响。(感觉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那道题,一目了然。

私以为经验的重要性大致等同于知识,但二者皆低于思想。

favor composition over inheritance?

无数设计书都叫嚣着这个标题,是为什么呢?

我觉着是因为大多数OO语言里,继承关系是不能在运行时动态改变的,但composition就可以啊,君不见那么多的Java getters/setters。

但如果继承关系能在运行时改变呢??比如Python这样的,且不说把基类换掉这种极端做法了,就说运行时可以override父类方法,我觉得就避免了Java中继承带来的问题啊。这样的话,Strategy模式还有意义么?有么??没有么??

说的抽象一点,如果语言本身不够灵活,你就只能利用它有限的一点灵活度,来尽量避免一些僵硬的设计。但如果语言足够灵活呢?我倒不是说这一定是好事。因为语言灵活,实现一个设计就会有好多灵活的方案,尤其像老潘这样喜欢玩儿数据和代码互换的,准保写出代码来让人丈二和尚。

所以说Java的方案是:语言僵死,然后拿一堆设计模式来进补。。。这样倒也好,适合工业大规模开发哈。但这样的话Java程序员就特别容易把事情想死,以为天下就只有这一种办法了。所以如果你是个Java程序员,你一定要小心脑电路栓塞或者冠状思想硬化这样的毛病。我见过的不在少数。

我现在特别庆幸我是先学动态语言,后学设计模式。。。

感觉Decorator用了closure思想

总感觉Decorator模式像SICP里面讲的那个closure(数学意义上的closure),尤其是painter那一节。

Decorator们被用来“修饰”一些对象的行为,或者说给一些对象扩展功能。这本身不新鲜。重要的是它们进行扩展的方式:它们本身与这些被修饰/被扩展对象是同一类型——一般是共同实现某个接口或继承某个超类;一般来说它们的构造函数里接受一个和它们同类型的对象,并“修饰”之。修饰的方式一般是override某个方法,在此方法中调用被修饰对象的overriden方法并对其行为添油加醋,形成新的行为。

这里面一个非常关键的思想,就是被修饰对象经修饰以后,成为了一个新的对象,仍然属于“同一对象族系”。于是这个对象可以继续被修饰。。。靠,这明明就是closure思想嘛:一组对象,其上定义的一组操作的返回值仍然是同类型对象,可继续将同一组操作应用于其上。所以这些操作可以任意组合,形成各种各样不同的效果。数学中的例子比如实数集合和四则运算操作。

Decorator模式的典型例子就是Java标准库里的reader/writer及input/output stream系列。

关于虚函数,多重继承,Python中的super

继承和多重继承(http://en.wikipedia.org/wiki/Multiple_inheritance)是language features,是面向对象技术为了使程序向现实贴近的一个手段。不过在面向对象理论中多重继承也不是必须的,基本上所有可以用多重继承实现的,都可以通过单继承来work around。这句话更多来自于实践而不是严格的理论。

从继承的实现角度来说,当子类对象被初始化时,实际上有一个父类对象也被初始化并且组合到子类对象中。这种来自继承的组合和普通的组合的不同之处主要在于,我们是仅仅需要使用父类提供的功能,还是说也需要使用父类提供的接口。

有继承就有多态。C++为了实现多态引入了late/dynamic/runtime binding(Connecting a function call to a function body is called binding):编译时,编译器不知道通过一个基类引用调用的某函数,具体调用的到底是那段代码。所以传统的非OOP编译器的early/static binding机制就行不通了。C++中实现late binding的手段是虚函数。Python和Java中所有的函数/方法实际上都是虚的。所以说虚函数只是实现多态的一个(并不唯一的)技术手段。

具体的实现机制是,编译器为每一个包含虚函数的类(包括通过继承得来虚函数的类)都生成一段分配空间并初始化VTABLE(就是所谓的虚函数表)的代码。于是在运行的时候内存中就会有这样的VTABLE,它们包含了该类中所有虚函数的实现代码的地址。这是非常合乎逻辑的,因为同一个具体类的所有对象,其虚函数实现都是一样的,所以一个类只需要一个虚函数表。

接下来,每当编译器在源代码中看到new一个对象的时候,都会做一些小偷小摸的事:它会生成一些额外的代码,在对象中在分配空间给一个指针成员变量VPTR(对程序员是不可见的!!!),通常被放在对象内存布局最开始的地方,也就是this指针指向的地址。这个VPTR指向本对象的类所属的VTABLE。这部分代码被十分自然的放在构造器中,所以运行时new一个对象出来,对象被初始化后,这个VPTR就已经ready好了。明白了吧!这就是为什么如果程序员不写构造器,编译器会搞个默认构造器出来。Java中也是这样。

但是VTABLE的地址是在运行时才available的。初始化VPTR这段代码必须在运行时获得本对象的类型信息才能知道正确的VTABLE是哪个!所以编译器确实是插入了类型信息到对象中的。在运行时的时候可以获得。题外话:这也是RTTI的本质。

前面的说法有一个hole,就是如果一个对象多重继承怎么办?这样对于同一个对象就会有多个VTABLE。最直觉也是最家常的做法,就是搞多个VPTR出来,也是都放在对象头部。但这时就需要pointer fixups即thunks(http://en.wikipedia.org/wiki/Virtual_table),即当用不同的类型去引用该对象时,使用不同的VTABLE。

接下来的事情就简单了:当通过一个基类指针调用虚函数时,不管对象的具体类型是啥,总能找到正确的VTABLE,通过其找到被调用函数的正确地址,并调用之。

从语言使用者的角度来看,virtual只是一个关键字。使用者实际上不需要知道虚函数是怎么实现的。但由于虚函数调用会有额外的开销,有心的程序员会去研究它的实现并且在适当的时候才使用。

所谓纯虚函数,就是只有声明没有实现的函数。包含至少一个纯虚函数的类就是抽象类。

多重继承引入了二义性问题。最典型的菱形问题http://en.wikipedia.org/wiki/Diamond_problem。

C++使用虚函数来解决多态调用,使用了虚基类来解决菱形问题。

Python则规定了一个特别的method resolution order来摒除二义性:http://en.wikipedia.org/wiki/Diamond_problem。

Python中的super http://fuhm.net/super-harmful/ 

程序语言的类型系统

类型系统的本质是,赋予计算机中那些莫名二进制数据一些意义。它定义了这些东西代表了啥,并且规定了在特定类型上能做哪些操作。编译器/解释器可以检查对象的类型,也可以不检查;可以在编译时检查,也可以在运行时检查。

  1. object/value 对象/值
    • a. 程序员在程序空间中可操纵的东西
    • b. 对应一些内存空间
    • c. 下文中的“对象”和“值”可互换。一般我会用“对象”
  2. class 类
    • a. 类只不过是用户定义类型罢了
    • b. 实例的类型就是对应的类
    • c. OO中的继承经常基于类。但Self和JavaScript则是基于prototype,比较另类
    • d. 用户定义类型是一个语言feature。要提供这个feature,不一定非得提供类机制。比如JavaScript就是用函数定义不同的对象,用prototype实现继承(代码重用)。
  3. type 类型
    • a. 每一个对象都有一个或多个类型(面向对象语言一般都支持一个对象有多个类型,尤其是用户定义类型即class)
    • b. 类型这个东西在某些语言中,程序员可以操纵,即它们也是对象。在另一些语言中则不是
    • c. 前一种情况的例子包括Python, Java。不过Java只为操纵类型提供了很弱的支持,并且primitive类型无法操纵(虽然有wrapper class)。我勉强把Java归为这一类。Python中的类型则是第一级(first-class)公民。你可以写return int这样的语句
    • d. C属于后一种情况。你可以传递一个类型为某struct的值,而不是这个struct本身。
    • e. 当类型也是对象的时候,会引发一个新的问题:由于对象必有类型,所以类型对象(包括类)也有类型,对吧?(Java里的类不是对象,但可以用对象表示)
    • f. 那么类型的类型是什么呢???Python里有个type类,可谓是元类型。事实上Python允许你通过创建type类的instance创建新类型。哦my god。使用这种方式创建出的类型和用户定义类型class应该是没啥区别。所有类型对象,包括class,都是type class的实例。实例的类型当然就是对应的class了,所以Python中,类型的类型就是这个type class。
    • g. 接着说Python。type class也是个class啊,class在Python里也是第一级公民,也是对象。(咱就说new-style class吧,不要讨论old-style的了。)那type class的类型是什么呢?所有的class的类型都是type class,所以它自己也不例外。。。我们会在Java中看到类似的情况。
    • h. Java里面的类不是对象,本质上是不可操纵的。Java使用java.lang.Class的实例来表示class。你可以通过这些实例获得一些类型信息,还可以用它们创建出对应它们所表示class的实例。对应每一个类,都可以获得对应的java.lang.Class实例。当然对于java.lang.Class这个类本身,也可以获得其对应的java.lang.Class实例。

Debugging Web Application

For now it seems no magic how Eclipse(WTP) did it.

Eclipse与搜狗输入法冲突

刚开始我以为只是快捷键(所有的ctrl shif组合)冲突,后来发现不将搜狗设为默认输入法后,Eclipse启动时僵死的毛病也好了。我用的3.0beta,同事用的2.0版就没这些问题。

Java accessors considered evil

先引经据典一把,Effective Java里面Joshua在item 12里提到了public field的坏处:

1. 你放弃了将该field的值限制在一个范围里的能力

2. 你放弃了当该field被改变时,做出反应的能力

典型的结果是这些field不具备线程安全性。当然上述情况发生的前提是这些public field是nonfinal或者final但mutable。

我第一个闪出的念头是,这是不是getters/setters存在的原因?你可以把它们写成跟listener似的,一旦要修改相应的field就检查其值是否合法或者干点什么别的勾当。

我一直隐约的觉得getters/setters有什么不对的地方。比如Python里就不这么干(
http://dirtsimple.org/2004/12/python-is-not-java.html)。

然后搜了搜找到这篇,说的挺有道理:

http://www.javaworld.com/javaworld/jw-09-2003/jw-0905-toolbox.html

about Java ClassLoader

http://www.panix.com/~mito/articles/articles/classloader/j-classloader-ltr.pdf
http://www.javaworld.com/javaworld/javaqa/2003-06/01-qa-0606-load.ht

回头再来搞。

分页共1页 1