Posted on 2005-11-14 17:00
canonical 阅读(505)
评论(0) 编辑 收藏 所属分类:
设计理论
分析学的离散形式是分而治之(Divide And Conquer)。 这一思想在软件设计领域的重要性不言而喻。
大系统分解为小系统,小系统分解为模块,模块分解为对象,对象分解为函数,函数分解为增删改查等动词和集合/个体等名词,如此递归下来。
在很多关于软件的"最佳实践"中,都列举了这种分解过程中的注意事项,如高内聚,低耦合等。
但是为什么要强调这些概念,谁能保证这个checklist是完整的, 具体实践过程中又当如何去做?
我们能否跳出软件的圈子,利用软件领域之外的词语重新表述一下这种思想?将大的系统分解为多个小的系统之后, 因为系统规模变小,
处理起来一般会容易一些, 但这是否就是分析学的全部?
有一个小故事,说有人问一个科学家,如果地球文明将要毁灭,只有一句话可以传给后人,那他最想告诉后代的是什么。那个科学家回答:宇宙万物都是由原子组成
的。分析学的哲学基础是还原论,原子论可以说是数千年来还原论最辉煌的胜利。原子论也清楚的向我们揭示了分析学的奥秘:千变万化仅是事物的表象,分解之后
它们都由同质的基元构成。在分解的过程中,问题的规模越来越小,问题的数目似乎越来越多,但当问题空间因为某种原因"塌缩"的时候,分解后的子问题出现大
量的重叠,整个问题的复杂性出现了本质性的降低。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更为重要的一个概念。