• 《Unix程序设计艺术》是一本讲解Unix文化的书。作者——开源运动的发起者Eric Raymond将多年沉浸于Unix经验一一道出,对于后来者理解程序设计中一些不变的东西大有裨益。该书的第二部分讲解Unix程序在设计上的一些考虑。在我看来,这里所讲的一些设计原则,更多是站在程序的外部进行观察,比如建议程序与外部接口文本化,比如将程序的内部信息以恰当的形式暴露出来以便外部观察等等。

    这本书的第五章讨论的是文本化,也就是以文本作为存储或传输的格式。

    在我的编程经历中,很早就开始同文本格式打交道。我参与的第一个Java应用,其协议采用了SOAP,也就是XML,也就是文本格式。在随后的几年中,我一直在与XML为伍,无论是配置文件,还是传输协议,所以,文本格式在我心中成了一种理所当然的选择。记得有段时间在一个实验室测试,当时的目的就是在测协议接口,大家经常干的一件事,就是把接收或发送的包打印在屏幕上,以便快速发现问题。幸运的就是我们的传输协议是文本格式,所以,我们省去了开发额外工具或是数字节的困扰。别以为这是开玩笑,那时候,部门的另外一个项目组在同一个楼和我们一同做着测试,他们的协议是二进制的。每次看到他们一个一个数字节,我心存恐惧的同时暗自窃喜着。

    那个系统分为业务系统和管理系统两个部分,为了让二者关联起来,我们需要开发了一个通信协议。就在我们几个人在外测试的阶段,我们一个很勤快的同事把这个部分开发出来,他采用了Java的序列化作为通信协议。当他离开部门之后,我们试图修改这个部分,发现这个部分已非我们能力所能控制。一方面,他的代码给人一种满天遍野的感觉,另一方面,任何对于服务器端的修改,必须要同时修改客户端,因为传输的是二进制,所以,如果不同时对两端进行修改,我们无法保证通信的正确。原本独立发展的两端不明不白的给连接到了一起。

    最近的一个关于文本化的例子来自于我现在开发的算法中的持久化部分。在最初的Matlab版本中,我采用了文本作为存储格式,到了C移植的时候,负责移植的同事原本的想法是偷了个懒,存成二进制格式,这样一条记录一下子就写进去了,读也是。事实证明,他懒惰的目的完全没有达到,一个小错误就够他忙活上半天,因为不直观,他又也要数字节才知道问题所在。一而再,再而三,他决定放弃了,对我说,看来选择文本方式存储是个好主意。

    选择文本格式,对开发人员来说,是一种享受。

  • 2006-02-18

    有状态的程序

    Tag:向上走

    我现在工作的一个程序是读入Video(或连续帧图像),然后逐帧进行处理。当前帧处理的结果需要依赖于之前的处理结果,所以,这个程序的状态逐步累加的。记得刚开始开发这个程序的时候,由于程序不稳定,经常出现崩溃的现象,为了找到原因所在,我们只有选择从头开始运行,虽然可以从中间开始运行,但由于状态累加的不同,bug很可能无法重现。那是一段相当逍遥的日子,因为当时的程序是Matlab版本,处理一帧图像就要好几秒的时间,而且崩溃通常发生在数百之后,出现一个错误,通常要花几个小时来重新运行,所以,我们会在相当长一段时间内无所事事,就这样,大家有了很多时间聊天,增进了彼此的感情,而且领导绝对没有理由说我们不工作。为此,领导特意给我增加了一台机器,美其名曰提高工作效率。

    后来,我良心发现,认为这种工作方式着实不怎么样,于是,我决定开发程序的持久化功能。简而言之,就是把程序运行过程中的数据存起来。这样的话,一旦程序崩溃,我们就可以利用保存起来的数据,重建原有的状态。我知道,熟悉J2EE开发的人对持久化这个词有着特别的感情,实际上,本质上是完全一样的,存储而已。

    在《管窥OS——进程透明化》中,我曾经讨论过持久化的问题,持久化的目的就是为了恢复。实际上,对于那些问题的思考主要源自我在这个程序所写的持久化部分。持久化的关键在于数据的持久化,但并不是所有的数据都需要持久化,在应用这个层次上,我们需要保留的数据是那些需要积累的状态,因为其它的数据或者可以通过它们恢复出来,或者只是一些临时数据。在我们的应用中,用来积累状态的主要是一些全局变量。再有就是保存的时机,每当一帧处理完毕的时候,都会把相应的信息持久化,这也符合我讨论过的原子操作之后,只不过,这个原子操作的规模大了一些。在我们的应用中,有一个有趣的细节,因为涉及到图像操作,持久化的过程中要存储一些图像,如果存储为JPG图像,恢复过来就会有些问题,这是因为JGP属于有损压缩,也就是存进去的矩阵和读出来的矩阵会有所差异,虽然在视觉上几乎看不出来,不得已,存放格式选择了BMP。

    我不喜欢编写有状态的程序,因为它会带来很多的问题。记得从前编写服务端应用的时候,我们的应用希望能够做集群,从而带来更好的水平扩展性。分析的结果,发现大部分请求几乎可以在不做任何改动的情况下进行扩展,因为它不会对服务端状态有任何改变,只是一来一回。所有的问题都来自我们转发异步请求,因为我们必须在服务端记录请求的信息,这样的话,对于其它服务器处理之后的应答,我们才能进行正确的匹配,进行后续处理。就是因为保存了这些信息,扩展的复杂度一下就上来了,因为一旦做了集群,一种可能的结果是,一台机器发起的请求,应答回到了另外的机器上,这种情况下,我们仍需要进行正确的匹配。这种情况与单机处理截然不同,所以,需要进行额外的设计,而这一切的罪魁祸首就是程序中的状态。

    在宏观层面上,应用的性质决定了它是否有状态,这是程序员无法决定的,但是我们可以通过设计,将状态降至一个可以接受的程度。在微观层面上,我们可以通过自己的代码,将状态限制在一个很小的范围之内,让我们编写的大部分函数都属于无状态的。我已经见识过太多本来可以无状态的有状态函数,结果是牵一发而动全身,由于状态带来的副作用。之所以这样的代码经常存在,仅仅因为这种代码编写起来很方便,直接访问全局变量,省去了声明参数之苦。程序员是应该懒惰,但不是这么个懒法,因为这只是一时之快,后面则是无尽的痛苦在等待。本来应用的折磨已经够受了,程序员们何苦为难自己呢!

    当然,有状态的程序并不那么可怕,毕竟,身经百战的我们有着兵来将挡的本领。

  • 2005-12-30

    IoC与DI

    Tag:向上走

    一个朋友发了封mail问了几个问题,其中的一个是关于IoC和DI的:
    Inversion of Control和Dependency Injection 是什么关系,我认为两个词代表的是同一个意思,只是两种不同的表示,对吗?

    下面是我对这个问题的一些理解。
    准确的说,IoC和DI并不相同,这一点从字面上就可以看出,否则,它们可以叫一个名字。^_^

    理解IoC,我们需要知道Control是什么,它又是怎样被Inversion的。其实,IoC是用来说明“程序库”和“框架”区别的最好证据。在使用程序库的时候,控制权是掌握在我们手中的,我们编写的代码调用程序库的实现,完成相应的功能,想想我们使用JDK的情况。使用框架的时候,控制权则掌握在框架手中,我们的代码最终是由框架调用,一个常见的例子是Servlet,我们编写的Servlet代码是放在整个Servlet的框架中,由Web容器进行调用。这就是差异所在。我们更习惯于自己掌控一切,因此,对框架掌握控制权的这种情况,我们用“Inversion”来形容,这也是Martin Fowler在那篇给DI正名的文章中提到,所有框架都是IoC的原因。

    Spring的核心容器是一个框架,所以,我们可以说它是IoC,但是就如前面所说,每个框架都有IoC,所以,仅仅用IoC是不足以说明一切的。Spring核心容器完成的是组件组装的过程,这是它和其它普通框架区别最为显著的地方。如果说用IoC描述这个框架,那么,这里所指的Control实际上是组件的组装过程。

    站在Spring核心容器的层面上看,它完成组装过程是把组件所依赖的零部件给组件安装上去。站在单个组件层面上看,它所需要的零部件是由外部给它安装的,这个过程就像是把“Dependency”这管药水用注射器“Injection”到组件的身体中去,所以,我们称之为“Dependency Injection”。

    完成组件组装的容器也不只是注入一种形式,还有一种常见的方式是“Dependency Lookup”,即每个组件自己去查找自己所需要的内容。至于到哪去找,也有不同的实现方式,有固定到某个地方(比如使用静态方法),有把查找点通过DI的方式注入进来等等。

    Martin Fowler的文章已经很清楚的解释了IoC和DI这两个概念,我们只需要去细细品味。

  • 2005-10-23

    EJB设计马后炮

    Tag:向上走

    在without EJB之后,EJB在开明人士的眼中已经快成了人人喊打的过街老鼠。我不妨也干一回落井下石的事,从设计的角度,品评一下EJB。鉴于without EJB嘹亮的声音,这篇blog也算是一篇读后感,故称马后炮。

    谈到设计,最先在头脑中出现的Robert Martin的《敏捷软件开发》,这本Jolt大奖作品应该是给我冲击最大的几本书之一,它为我打开软件设计的大门,对我而言,它最有价值的部分并不在于设计模式,而在于设计原则。相对于设计模式而言,设计原则是一些meta的知识,掌握了设计原则,理解模式也就是很自然的事情了。

    提起EJB,人们首先会想到它叫得最凶的组件的概念。在经典的J2EE架构中,EJB往往是单独部署在一台应用服务器上,所以,谈及EJB,它的远程访问也是必不可少的。我就曾经设计过一个EJB,目的只是为了使用它的远程模型,省却自行开发通信协议的烦恼。既要承担业务组件的职责,又要扛起远程通信的大旗,EJB在这一点上很显然是一个部分承担了多个职责,这显然是违反单一职责原则的。或许你编写EJB只是作为一个业务组件,但你不能说我用它做远程通信就是错的,尽管这不是什么标准做法,谁让EJB这么能干呢!其实,为了讨论方便,我还少提了一点,EJB往往还有线程模型在其中,这只是让它能干的事更多,进一步违反单一职责的纪律而已。

    “声明性”为简化程序结构提供了一条金光大道,只要你做好自己该做的事情,一些额外的服务可以通过声明附加进去,让程序的威力得到加强。EJB在这方面做了一些事情,提供了包括事务管理、线程管理和安全性在内的一些常用服务。但是,这些服务远不足以满足人们日益增长的物质文化需要。当你冀望于按照自己的想法增加一些服务时,你才会发现,报国无门啊!EJB很好的做到不必修改,但其不能扩展的实际情况实在有愧于框架的美誉。这显然是违反了开放—封闭原则。

    在这个敏捷横行年代,如果做设计的时候不考虑测试的问题,那这个设计师一定会被视为老古董,因为他连测试优先都不知道。测试驱动开发的“开发”可不只是编码,那是要从软件开发的源头算起的“开发”,身在开发过程之中的设计自己也不能免俗了。如果你像我一样经历过在应用服务器上调试一个bug,你就知道我为什么心甘情愿地奔向测试驱动开发的怀抱,那是一段让人不堪回首的往事,本来三两分钟就可以搞定的问题,耗去了两个小时。而这一切的一切,起因就是EJB。

    考虑EJB问世时,敏捷尚未成风,我们还可以原谅它对于可测试性缺乏思考,那么违反单一职责原则和开放—封闭原则实在是制订标准的人对于这个问题思考不足,想来设计这些东西的人也并非等闲之辈,由此我只能得出一个结论:官僚主义害死人的。

  • 2005-08-11

    虚假的泛型

    Tag:向上走

    一位网友引发了一次关于泛型数组创建的讨论大胃给出了自己的回答。我也想就这个问题讨论一下,因为这也是我曾经遇到过的一个问题。

    简而言之,在Java中,下面这段代码行不通。
    public static T[] create(T t) {
        return new T[100];
    }

    在讨论这个话题之前,我们必须知道的一个事实是Java的泛型并不是我们常说的那种真正的泛型。下面是一个简单泛型程序:
    public class Generic {
        public static void main(String[] args) {
            String s = transition("hello");
        }

        public static T transition(T t) {
            return t;
        }
    }

    如果把编译之后的结果反编译过来,一切的魔术就将揭开:
    public class Generic {
        public static void main(String args[]) {
            String s = (String)transition("hello");
        }

        public static Object transition(Object obj) {
            return obj;
        }
    }
    从这里我们不难看出,编译器将从前版本中需要的手工转型给自动化了。

    既然要达到一种可用的自动化,一个很重要的标准就是不能把原本做得很好的事情给破坏了。泛型在于把类型作为参数,而我们前面已经看到了,交给虚拟机的class文件里根本没有类型信息,也就是说,Java的泛型都是编译器上的工作。除非用户明确要求怎么做(比如强制转型),否则能够通过编译器的在类型问题上一定是清清楚楚、明明白白的,不能有任何含糊。

    在回头看最初的问题便不难解释了。我们预期创建泛型数组,但实际上,因为类型信息将会在编译阶段丢失,那么到了运行时,不知类型,何谈创建。在类型问题上想蒙混过关,谈何容易。因此,这样的代码是无法编译通过的。由此可以看出,这个问题本身与数组没有太大的干系,完全是类型问题在作祟。

    说好听点,是为了安全,实际上,只是不想给人留下口实而已。关于这个话题,《Generics in the Java Programming Language》7.3节进行了更为细致的讨论,你会看到如果在类型上不较真将会出现怎样的遗漏。

    Java泛型如此选择有其历史原因。作为Java泛型的雏形泛型Java形成之初,势单力孤,尚无力改变Java的历史进程,因此,它只有选择在Java编译器上动手脚,而不敢动Java虚拟机毫分,因此,Java的泛型就注定了其虚假的本质。事实上,这也大多数新思想介入Java时遇到的问题,比如AspectJ。运用同样的反编译技术,我们不难发现,Tiger中大多数的新语法都是在不伤及虚拟机的情况下加入。Annotation是一个例外,它所带来改变class文件,这也使得Java泛型的努力显得如此不伦不类:要么都不改,要改就改得彻底一些。

    如果说C++的泛型带来的是一场思维革命,那Java的泛型至多只是一种简化代码编写的手段而已。不过这也符合Tiger的口号:简化开发。