分类: 服务器与存储
2012-03-27 12:46:18
漂亮代码的示例通常有着良好的边界(well-bounded),只解决问题的某个局部且易于理解。本章的示例是版本控制系统Subversion中的一 个接口svn_delta_editor_t,它不仅用来表示两个目录树之间的差异,还可以用来将某个目录树转换为另一个目录树。
Subversion在服务器端的仓库(repository)里存放着根目录的所有版本,仓库可被视为一个无限大的修订号的数组(revision array):
这个目录树模型有一些非常吸引人的功能:
易读性:要想找到文件/path/to/foo.txt的第n个版本,可以直接跳到版本n,然后沿着目录树搜索下去,直到/path/to/foo.txt。
写操作不会妨碍读操作:当写操作在创建新的节点时,直到最后节点被链接到版本数组时,读操作才能看到新的目录树。
用版本来标识目录树的结构:在不同的版本中,每棵目录树的结构都将被保存。文件和目录的重命名、增加以及删除操作都将成为仓库历史记录的一部分。
除了服务器端的仓库,Subversion还包含一个客户端:工作副本(working copy)。 用户可以检出(check out)某个目录树的副本并在本地对其进行修改。Subversion最常见的操作就是在服务器/客户两端传输目录树之间发生的变化:或者从工作副本到仓 库,或者从仓库到工作副本。同样还有些其它操作,如把一个分支的融合到另一个分支。这些操作都需要一个好的方法来表达两棵树之间的差异。
如何表达两棵树的差异?如果把变化的路径作为字符串来传输的话,那传输的顺序应该是什么?深度优先、宽度优先、随机 顺序还是字母顺序?目录与文件是否使用不同的命令?最重要的是,如何判断一个表达差异的命令是否属于一个(包含所有修改操作的)整体操作的一部分?由于 Subversion的整个目录村操作都是用户可见的,如果编程接口没有设计好的话,之后肯定需要编写大量脆弱的代码来进行弥补。
为此,Jim Blandy提出了解决方案:增量编辑器接口。
一个文本增量表达一个文件在两个不同版 本之间的差异。在Subversion中一个文件的“文本”被认为是二进制数据,无论文件是普通的文本,还是音频数据、影像或者其他的东西。文本增量被表 示为固定大小的窗口,每个窗口中包含一块二进制数据,即一个大的文本增量可以分成多个窗口,这样内存的使用量只和单个窗口成正比,而不必是整个增量的大 小。
在处理“Commit”命令时,客户端检查工作副本数据,生成一个目录树增量来描述将要提交的修改,之后通过网络把目录树增量通过一系列请求发送给服务器
端。服务器端接收到这些请求并生成一个目录树增量,它应该与客户端的目录增量是一致的。最后服务器处理这个增量,并向文件系统提交一个恰当的事务。
“Update”命令与“Commit”命令所做的操作基本相同,但顺序相反,它把服务器端的目录树增量发送给客户端。
有时增量会很巨大,我们无法把整个增量作为一个简单的数据结构传输,所以我们并不这么做,而是定义了一套标准的接口。消费者必须实现这些接口,并传递给生产者。这个接口里包含多个回调函数。生产者在遍历目录树并生成增量时,会调用(消费者提供的)接口中的回调函数,使得消费者产生与生产者这边一样的目录树增量。(Jim Blandy给出了这个解决方案。)
接口里的回调函数包括:delete_entry、add_file、add_directory、open_file、open_directory等。在部分的回调函数都包含一个void*类型的名为Baton(接力棒)的参数,它主要用来传递当前的运行环境,比如下面的这些操作:
open_root () :为根目录生成一个baton root;
open_directory (root, "foo") :为“foo”生成一个baton f;
open_directory (f, "foo/bar") :为“foo/bar”生成一个baton b;
add_file (b, "foo/bar/baz.c")
(博主:生产者通过接口中的回调函数使消费者产生目录树增量的方式,让我联想到配钥匙时,根据原钥匙的轮廓产生出钥匙的副本。)
所有的回调函数都返回一个svn_errot_t*,它是一个指向一个表示错误的对象的指针。回调函数还接受一个apr_pool_t*的参数,它是用于分配和释放对象的内存池。
这个接口名为svn_delta_editor_t,下面是它的(C语言)定义:
typedef struct svn_delta_editor_t { /** 将root_baton设置为一个表示根目录变化的baton */ svn_error_t* (*open_root)(void *edit_baton, apr_pool_t* dir_pool, void **root_baton); /** 删除parent_baton的子目录path */ svn_error_t* (*delete_entry)(const char* path, void* parent_baton, apr_pool_t* pool); /** 在parent_baton里增加一个新的子目录path,并把child_baton设置为这个新的子目录 */ svn_error_t* (*add_directory)(constchar* path, void* parent_baton, apr_pool_t* dir_pool, void** child_baton); /** 准备对parent_baton的子目录path进行修改,并把该目录信息传递给child_baton */ svn_error_t* (*open_directory)(const char* path, void* parent_baton, apr_pool_t* dir_pool, void** child_baton); /** dir_baton已经完成修改,这个baton所使用的资源都可以释放 */ svn_error_t* (*close_directory)(void* dir_baton, apr_pool_t* pool); /** 在parent_baton中增加一个文件path,并在file_baton中存储新文件的信息 */ svn_error_t* (*add_file)(const char* path, void* parent_baton, apr_pool_t* file_pool, void** file_baton); /** 在parent_baton中打开一个文件path进行修改,并在file_baton中存储该项文件的信息 */ svn_error_t* (*open_file)(const char* path, void* parent_baton, apr_pool_t* file_pool, void**file_baton); /** 在文件file_baton上应用文本增量。回调函数应返回一个文本增量窗口处理器handler,在收到后续的文本增量时,将在文本增量上调用*handler,handler_baton为*handler被调用时传递进去的参数 */ svn_error_t* (*apply_textdelta)(void* file_baton, apr_pool_t* pool, svn_txdelta_window_handler_t* handler, void** handler_baton); /** 文件file_baton已经处理完,可以释放相关资源 */ svn_error_t* (*close_file)(void* file_baton, apr_pool_t* pool); /** 所有的增量处理已经完成 */ svn_error_t* (*close_edit)(void* edit_baton, apr_pool_t* pool); /** 编辑器的驱动器决定退出,编辑器将在必要时执行清理工作 */ svn_error_t* (*abort_edit)(void* edit_baton, apr_pool_t* pool); } svn_delta_editor_t;
对于生产者,它的代码大概是这样的:
Karl Fogel认为这种接口设计的漂亮在于:
1、虽然没有显式地表示出来,但增量编辑器通过baton之间的关系迫使编辑按照深度优先的顺序来进行。这使得接口的用法和行为是更可预测的。
2、整个编辑操作中通过baton隐式地传递了运行环境。baton可以包含一个指向父目录baton的指针,同时还能包含一个指向编辑baton的指针。baton对象是可以被销毁的,这样就为编辑中的各个部分提供了作用域。
3、接口中的不同子操作有着清晰的边界。这提供了很大的灵活性:如果用户只想知道版本间大概的区别,而不用知道文件内部的细节,则可以选择不调用apply_textdelta。
有时在目录树增量编辑时会有些附属操作,比如根踪提交的目标,以便向用户报告更新或者提交的进度。Subversion曾考虑过组合编辑器的解决方案:
/** 将editor_1和editor_2组合成新的new_editor,每次调用某个函数new_editor->fun时,先调用editor_1->fun,接着调用eidtor_2->fun。 */ void svn_delta_compose_editors(const svn_delta_editor** new_editor, void** new_edit_baton, const svn_delta_editor_t* editor_1, void* edit_baton_1, const svn_delta_editor_t* editor_2, void* edit_baton_2, apr_pool_t* pool); 这种方案也是挺漂亮的:它的行为是可预测的,并且使代码保持很好的整洁性,不同的编辑器可以组合而不干扰彼此。但是最后它被认为是过度抽象的,因为附属的行为并不需要像目录树增量编辑那样的频繁。过度地调用附属行为容易误导阅读代码的程序员。
最后Subversion取消了这种通用的组合方式。当需要的时候他们手动地构造复合编辑器。比如一个取消操作编辑器:
/** 将editor和edit_baton设置为包装了wrapped_editor和wrapped_baton的取消操作编辑器 * 当编辑器的每个函数被调用时,都将使用cancel_baton来调用cancel_func */ svn_error_t* svn_delta_get_cancellation_editor( svn_cancel_func_t cancel_func, void* cancel_baton, const svn_delta_editor_t* wrapped_editor, void* wrapped_baton, const svn_delta_editor_t** editor, void** edit_baton, apr_pool_t* pool);除了取消操作,Subversion还使用类似的手工组合方式实现了条件调试输出、进度报告、事件通知以及目标统计等。
:工作于非盈利组织Civic Commons,致力于开源软件的开发。Subversion的作者。代表作有:《Producing Open Source Software》、《Open Source Development with CVS》等。