有一个小故事,说有人问一个科学家,如果地球文明将要毁灭,只有一句话可以传给后人,那他最想告诉后代的是什么。那个科学家回答:宇宙万物都是由原子组成的。分析学的哲学基础是还原论,原子论可以说是数千年来还原论最辉煌的胜利。原子论也清楚的向我们揭示了分析学的奥秘:千变万化仅是事物的表象,分解之后它们都由同质的基元构成。在分解的过程中,问题的规模越来越小,问题的数目似乎越来越多,但当问题空间因为某种原因"塌缩"的时候,分解后的子问题出现大量的重叠,整个问题的复杂性出现了本质性的降低。FFT和动态规划算法中所采用的也正是这种从异质到同质的解决方案。
分解之后,我们希望得到的子系统是低耦合的,那么最好是完全不相关的,我们希望得到的子系统是高内聚的,那么最好是不可分的,在数学上,我们称之为正交。还原论最完美的载体是线性世界,而线性代数(或更广义的群论)说了,线性系统完全由其正交的特征向量所构成的"核"(kernel)来刻画,那大体上软件系统应利用少数可重用的模块来构建。但线性代数又说了,特征向量的选择方式是无穷多的,而且完全等价, 那大体上软件系统的分解方式也是多种多样的, 多数很难分出优劣。 线性代数还说, 特征向量个数仅由系统的维度(系统复杂性的一种度量)来决定, 那大体上软件系统无论怎么分解, 总有一个复杂性的下限。过分简单的架构仅能支持过分简单的应用。线性代数没有明说,但潜在的表达着,特征向量的地位是平等的,所以在高内聚,低耦合的基础上,软件分解的原则中至少还要增加一条:对称性, 以维护系统整体结构的平衡。
很可惜,现实世界中发现了越来越多的非线性现象,以致于非线性研究本身已经成为了一门独立的学科。不过,古老的教诲仍然有效,分解可以帮我们找回系统的线性。在微积分所描绘的极限情形中,外力产生了加速度,然后加速度产生了速度,因与果就这样实现了分离。(有人说,重整化方法在微观世界的成功正是因为在极度纠缠的临界情况下微积分失效了,也许有些道理)。
为了在软件中实施分析学,我们需要一些技术手段。首先,需要一种命名机制,使我们能够在思想中定义概念,并开始建模。所谓的对象,正是这样一种机制。可以从以下的级列关系来理解这一点
1. 高级语言规定了数据的类型,使得我们可以为不同的内存块指定不同的数据类型,从而在概念上对它们作出区分。
2. 当程序变得渐渐复杂起来,C语言提供的Struct结构体,使我们可以创建新的数据类型,可以将一组相关的数据放在一起,起个名字。而如果没有 结构体,这种相关性就无法直接在程序中得到表达,必须纪录在文档中或者程序员的思想中。
3. 对象(Object)是比结构体(Struct)更加强大的命名机制,它可以将一组相关的数据和函数放在一起,起个名字。而且通过封装和虚拟函数,一个对象类型所表达的 不仅仅是它自身所代表的概念,它同时表达了它的派生类所具有的特征。即对象所表达的是一个概念的集合而不是一个单独的概念。
4. 更复杂的程序中,对象之间的相互作用产生了某种确定的特征,出现了设计模式。
5. 这个级列的下一步是什么?
对象化没有什么神秘的地方,它只是使我们拥有了一种表述的工具。有时对象化比不对象化更遭,因为我们极有可能犯命名的错误。
在没有对象的概念的日子里,我们无法命名数据和函数的耦合,一些概念也就无法在软件设计中得到自然的表达,因为它们在程序的世界中没有名字!一旦我们能够命名系统中所有的概念,一扇门就被打开了,大量的可能性被发掘出来,形成了今天的面向对象技术。这其中最重要的就是软件中的正交分解技术。首先是继承。在早期的C程序中,经常出现如下的代码:
if a then
a_work_1();
else if b then
b_work_1();
end
…
if a then
a_work_2();
else if b then
b_work_2();
end
通过继承,我们可以捕获以上程序中的关联性,代码被改写为如下方式
x = a or b;
x.work_1();
…
x.work_2();
但作为早期最主要的面向对象技术,很快继承这个概念就不堪重负。通过继承,系统中的所有关系被组织成了一个树状结构。随着树的层次越来越深,整个结构变得越来越不稳定,基类的小小变动随时可能会造成雪崩似的影响。作为一个整体,对象也越来越难以被重用。
此时,接口(Interface)应天命而生。从简单的意义上来理解,接口可以被认为是对对象(Object)的正交分解。如果使用继承,
class CHuman {
public void eat(){..} // human eat
public void sleep(){..} // human sleep
}
class CManager extends CHuman {
public void fireEmployee() { ...} // manager fire employee
};
class CEmployee extends CHuman {…}
公有继承大致上对应于"is a" 关系, 即一种包含关系,在数学上称为偏序(Partial Order)。
偏序在逻辑上隐含的是一种推理,即我们可以根据基类的行为我们可以推论派生类的行为。所以当我们知道某人是经理(CManager)的时候, 我们可以推论出他是一个人,即他能吃能睡。很可惜,这种微妙的信息泄漏也许并不是我们所希望了解的,毕竟董事会雇佣一个职业经理人来为的是管理而不是吃饭。
应用组件技术,我们进行如下建模:
interface IHuman {
bool eat();
bool sleep();
};
interface IManager{
bool fireEmployee();
};
class Manager implements IHuman, IManager{…};
Manager = IHuman + IManager
接口打破了继承所构建的僵化的树状结构,提倡灵活的网状结构,使得整个系统结构扁平化,分解的粒度也更小。有了接口,是否就应该忘了继承呢?不,推理关系仍然是重要的,只是不要滥用。
最近几年,面向方面编程(AOP)逐渐兴起。从分解技术的角度上看,它代表了一个新的方向:形容词与动词的正交分解。例如,我们需要在一个事务中实现转账,
实现转账这个功能可以很容易的编写, "在一个事务中"这一修饰语被抽象出来称为一个Aspect, 并单独实现。通过AOP技术,我们将动作与所需要的修饰组合起来,完成所需要的功能。
最后,谈一谈Reusablity这个概念.
软件设计是从需求领域到软件技术实现领域的一系列模型映射,在每一个层面上都存在着多种正交分解方式。构建软件的目的是为了满足需求,所以整个映射过程应该向着应用层倾斜。有一个说法叫做Object oriented to user, 我是从科泰世纪的陈榕那里听来的。 我想这也正强调了从多种分解方式中作出选择的准则。 可重用的对象意味着它更可能成为构建系统的"特征基元", 同时它的可用性隐含的表达了对应用层用户的意义。 所以Reusability是一个比Objectlization和Encapsulation更为重要的一个概念。