Archive for 2011年4月

无毒副作用

2011/04/21

每当提到如何『评价』面向对象技术时,很多人的直接反应就是上升到世界观层次。说它让软件设计和客观世界更好的一一对应云云。我非常怀疑这种思考方式。客观世界是多维度的,面向对象的软件系统只能选取一个维度进行描述,同时为兼顾其它维度要做各种 leaky 的妥协。因此,对一个客观场景总有很多个可能的面向对象『设计』存在,每种『设计』都不能被认为是错的,甚至不能被认为是差的。这种一对多的情景很难让我信服面向对象技术比其它手段能更『自然地』描述世界。对我来说『自然地』只有一种标准 —— 容易取舍的单一选择。

面向对象技术没有脱离面向过程和命令式( imperative )的思维。有经验的 C++ 、Java 、Objective-C 程序员在阅读和编写代码时能熟练到近乎反射地把 this 或者 self 作为一个参数来思考(比如 0->func() 这样的调用为什么在某些情况下是可行的),或者把函数 override 作为函数指针的另一种形式(比如 0->func() 为什么在某些情况下是不行的)。这里没有任何世界观的升华。面向对象真正的便利仅限于消除负面副作用( negative side-effects )。

举个例子来说明什么叫负面副作用。假设在程序中有个二维的『单位向量( normalized vector )』,其方向任意,长度必须为 1 。这个数据通常表式为 x 分量和 y 分量。作为单位向量,必须满足 x 和 y 的平方和始终为 1 ,这种条件叫做对象数据的不变式( invariation )。不小心打破不变式,比如把 x 和 y 都设成了 1 (平方和变成了 2 ),这种行为就叫负面副作用。在数据暴露给所有算法代码的编程方法中,不变式是通过文档由程序员人工保证的。面向对象方法认为人工保证容易出错,所以通过隐藏对象的数据,只允许有限的成员函数访问它们来保证不变式。这样需要精心维护的涉及不变式的代码仅限于少量成员函数。类似的例子还有如何表示一个钢体(内部没有复杂结构,各部分的相对位置不会变化的物体)在三维空间的位置和姿态。钢体本身的形状一般用一组顶点表示,它在三维空间的位置和姿态用一个 4 x 4 的变换矩阵表示。熟悉 3D 几何的人知道,4 x 4 矩阵可以表示拉伸、平移、旋转、甚至扭曲等任意的变换。但是对于钢体来说,只有旋转和平移是有物理意义的。所以必须对这个数据结构中的 4 x 4 矩阵做出限制,即钢体对象的不变式。

从另一个角度来看,面向对象保证不变式的方式未必是理想的。首先,人工约束不变式并非那么难。Linux kernel 核心数据结构的不变式就是通过活跃的讨论和 code review 保证的。其次,人工约束不变式可以让设计者天然地保持警觉,把核心数据结构的数量最小化。最后,不变式可以通过合理的设计数据结构来减小甚至消除。比如上面单位向量的例子里,可以把单位向量用一个夹角 —— 即它和坐标系的某个轴的夹角来表示。这样的表示法不再需要额外维护不变式。类似地,在钢体的例子里用一个平移向量和一个旋转向量代替 4 x 4 矩阵表示位置和姿态,可以减少要维护的不变式。

这两个例子都说明,不变式来自数据结构的信息冗余。当然,表示复杂的对象很难做到完全消除信息冗余。若底层 framework 的 API 直接提供 4 x 4 矩阵运算或者 (x, y) 二维向量运算,我们就要考虑在这样的系统上构建对象的简便性和复杂度,未必要舍近求远用平移、旋转和夹角来表示。同时,保持冗余信息也是提高性能的手段,很多操作都需要 x / y 分量或者 4 x 4 矩阵,如果不直接存储它们,就要在每次需要的时候临时计算。所以造成不变式的信息冗余又来自两处:一是性能优化的需要;二是支持我们构建对象的底层系统中可以被直接操作的模块粒度不够小。这就像现实中我们没法构建一个无需仓库的物流,也无法用纳米亚原子直接打造机器。

前面提到用非面向对象技术消除副作用并非难如登天。另一方面,复杂系统的信息冗余不可能完全消除决定了面向对象技术多少是有用的。这里的结论是不要把一种消除副作用的便宜之计吹捧成改变世界观的灵丹妙药。特别是在一叶障目之后,容易忽视背后的更本质更有效的手段。很多吹捧面向对象技术的人往往喜欢把系统的核心数据结构设计的非常庞大,信息非常冗余。从编写复杂的保持不变式的代码中获得不真实的成就感。这种过度吹捧在面向对象以外也有例子。并行化技术最大的负面副作用是不同线程同时修改一份数据,即 race condition 。消除这个副作用有几个方法,其一是被诟病的 lock 技术。Lock 的问题在于为了试图保证数据的不变式引入了时序上需要额外保证的不变式。在《多核与锁》里有所讨论,这里不过多涉及。

接下来是两种直接保证并行处理中数据不变式的手段。一个方法是只用栈变量,因为栈是每个线程独立的。更精确的说是只使用 per-thread 数据,因为有些 per-thread 数据其实在全局堆里,只是通过 per-thread 的引用来管理,而且后面马上说到有些系统的 per-thread 数据不是基于栈。这方面有一个最近成为热门的例子 —— GPU 运算。OpenGL 的 GLSL( shader language ),CUDA 和 OpenCL 的编程语言处理的数据都仅限于当前 GPU core 使用的 per-thread 变量。另一种方法是根本不用变量,那就是一小撮人推崇备至的函数式编程( functional programming )。在《并行计算的解药》里我讨论过,前一种手段里分割输入数据才是关键。而后一种,我认为根本是步子迈的有点大,扯着了。对这两种手段的评价,都只能限于消除副作用的影响,解决问题的真正关键在于更高层次的数据分割。

一项技术技术是否能被主流软件开发社区采用,不在于世界观和哲学那类东西,而是更实际的 —— 在消除副作用的同时保持原有的思维方式;至少增加的额外思考不能超过熟练程序员的反射弧。要求改变世界观的技术一开始就失败了一半。

土气

2011/04/07

自从成为 Mac 用户之后,我一直非常想把 Firefox 从系统中清除出去。原因有三个:第一,我基本不用 add-on ,偶尔重度使用的几个功能也都有 Safari extension 或者 Chrome extension ;第二,从 code base 的角度说,我对 Webkit 的信任度大于 Firefox ;第三,Firefox 太『土气』。前两条我认为尚属个人感觉。这第三条从道理上说主观的不能再主观,可我相当肯定这种感觉。和 Safari 相比,Chrome 经常给我欠修饰,不圆润的感觉,但绝对没有『土气』。可看到 Firefox 就和看到上世纪 80 年代风格的服饰一样,而且是那种绝不会借复古卷土重来的土气。

最近发现一例,是对正在下载图片的处理(注意这里的例子是正在下载但还没有结束也没有彻底下载失败的图片)。Safari 是直接留白(强调一下,Safari 对彻底下载失败的图片的处理并非留白)。

Firefox 的处理是从上世纪不变的黑框加『圆、方、三角』图标。

这两种效果有什么区别?不用讲太多道理,第一感觉就是 Firefox 的效果太『土气』。我和同事曾经比较过一些应用的界面。一个可拖动的分割线( draggable separator )是单像素宽的黑线还是多个像素宽的伪 3D 效果,这点区别就能决定一个界面是整洁还是凌乱(猜猜哪种让界面更整洁)。这种感觉不易量化,但绝对客观存在。不管有什么理由,『土气』就是一种缺陷,就意味着你得花额外的精力从其它方面弥补。

Firefox 的『土气』带来了什么收益?它告诉了用户一个在今天的网络状态下持续不会超过两秒钟的状态(为了抓上面那张图我费老了劲了,真想找个让网速临时慢下来的工具)。去掉这个状态的可见性不会令用户丝毫不便,刻板的显示这个状态给用户带来的只有一闪而过的『视觉污染』。就这个例子来说,Firefox 的设计不仅仅是土气的问题,它忽视了 UI 设计的一个基本原则,不要浪费用户的注意力。用户的『注意力』是稀缺资源,这种知道不知道两可的程序内部状态无需让用户费心。这是 Firefox 对拨号时代的遗产长久不加审视的体现。我不敢说从这个例子可以总结出什么一般规律,但是我怀疑那些表面上看着土气的设计也总是隐藏着对可用性的某种深层次的忽视。