此类文章,书本极多,车载斗量,繁如瀚海,流派繁杂,门道多多。其中天外有天,强牛无数。本文乃是“胡思乱想”兼“胡涂乱鸦”,若有高人高见,指点几番,则乐不可支;若是对我的这派胡言大动肝火,口诛笔伐的话,我提前投降,相信高人是不会杀俘虏的。
     是为序。
温馨小提示:看到组件二字,千万别联想COM

 

1、组件是设计和实现中最可爱的人

组件:软件设计中最好用的元素

 

       “组件”,一种可以在设计时真正被自由组合的元素。在你需要的时候,给你一个 容器,在你又需要的时候,又给你一个迭代器,把抽象留给用户,把困难留给自己。他们就是组件。几乎所有用 MFC 的程序员桌头上都有一本《深入浅出》,而几乎所有用 STL 的程序员桌头都没有《stl源代码剖析》,

我们不要框架,只要独立自由的组件,源码时刻的、编译时刻的、运行时刻的。

 

       定义:组件表现一个逻辑设计的基本单位,是可分解、可组合的。

      

       为什么需要组件:用它来隔离逻辑抽象与物理实体,com 给出的方案是:用接口隔离。

      

       要求:组件之间尽可能独立,坚持通过组件之间的接口互相访问,可能是基类、trait、类型等等。类似:通过局部声明引入另一个组件中带有外部语义的符号,是不良的设计。除非有特别巨大的诱惑或者被迫无奈。

               

       尽量把友元放在组件内,不要让他跑的太远。

               

       组件也有麻烦:组件之间的物理上的依赖,导致组件难以做到自由、无障碍的搭配,尤其是源代码和编译时刻。

      

       对组件之间的物理依赖绝缘:

       1、扔掉私有继承。对于强关联,明确的使用组合。

       2、不要 has-a 一个组件,一定要 has-a 一个接口

       3、让客户显式的去管理资源,不要试图包装到组件中,那会更加难以调试和排错。

   

2、单一范式力量小

       能力有限,只看看OOGP

 

       2.1 OO 这么看

      

       我们依旧从:行为、算法、数据结构三个维度看 OO 如何解剖需求中的共同性与差异性。(这里把“行为”改称“签名”更合适,但是在维度分析的时候用“行为”更合适,额,选词好痛苦)

ü         用叫做 class 的东东捕捉需求中一致的结构和一致的行为

ü         将一组行为一致的 classes 表达为一个 抽象。

ü         同一组的 classes ,上层的 class 提供抽象的默认接口,并共享算法一致的行为。

ü         将运行时会变化的行为统一到接口中,这个接口“可以在编译时期知道,并且和共同性分析中一致的属性关联起来”。

支持OO的语言,用语言的内置机制支持上述的思路,如继承、虚函数等等......

 

2.2   OO有特质

 

我们按照 上面描述 的思路开始旅行

ü         OO代表紧绑定的语义

   
荷兰猪、蒙古诸、野猪都是是猪的一种,我们把这些猪都圈到一起,这就是我们的猪容器。谁想和他们圈到一起,那就得从“猪”派生一下。这就带来两个问题:

1.         一旦猪接口发生变动,整条继承体系都得改啊改啊改.....虽然理论上讲良好的设计应该基本上杜绝,不过“理论上”“应该”这些字眼实在是靠不住...其实捏,这也不算多大问题,软件本来就复杂嘛。继续看问题2

2.         我们对猪按照体重排序,写了一个sort方法,排出了123465.。额,忽然我们还想给狗排序,又忽然想给猫排序......聪明的程序员索性写了一个接口,Isort,猪被迫实现Isort接口,或许那天我们想搜索一头荷兰猪,猪又被迫实现Isearch接口,或许那天我们想把猪永久储存到硬盘,猪还得实现持久化接口。(另:使用对象包含亦有此功效)
   
额,好可怜的猪。身为一头野猪,什么二叉树搜索、COM持久化对象和我没有一点关系,我只想在野外自由的吃喝玩乐,恩,好吧,我承认,我说谎了,其实我想知道我的重量在野猪界到底壮不壮。但是根据人类的离散数学理论,只要在集合中定义了偏序关系,就可以进行排序。也就是说野猪只要通过‘<’操作把报告一下体重就可以确定在野猪界的地位。

3.         于是我们会自然的想到将一组的关联内容集中到一个接口,并发啦、异常啦、甚至消息传递啦,等等等等,都放到一个接口,猪猫狗的都从这儿派生。额,终于搞定了。可是问题依旧存在:
   
首先,庞大的接口语义不明确,将整个明确抽象系统撕开一道模糊的裂缝;
   
其次,对概念模糊大型接口进行编译时刻的类型约束非常困难,导致产生许多语法正确但没有意义的代码,这些,都是日后的隐患;
   
最后,作为一只野猪,只为了知道自己在野猪界的分量,误上了“猪继承体系”这条贼船,承担了诸多与自己毫无关系的概念,浪费了许多条CPU指令,还有可能被加到MFC消息映射链中做消息中转......5555555,救命啊,有人虐待野生动物)。

4.         多重继承是好的思路
幻想:class public: Thread ,
      public: Alloctor,
      public: auto_ptr
我们就获得了一头线程安全且自动清理内存的且异常安全的一直猪。额,我宁可相信布什派出航母是为了解救水深火热的伊拉克人民也不相信上面的代码可以正确运行。
   
但是站在语义的角度来讲,多继承却提供了一种优秀的思路。即是“组件”,我们根据需要,取出对应的组件进行装配,这样给不仅带来了弹性灵活的设计,而且带来了轻松的维护


   
站在CPP的角度来看,多重继承是用胶布把基类缠在一起,并定义了一系列访问规则。而这套规则实在是:
   
首先:而多重继承本来是一种全新的语义,但是却和单继承混到了一条船上,导致二义性无时无刻不在你跟前转悠,每写一行代码都谨小慎微。(没办法,谁让BJ老人家爱效率呢)
   
其次:从组件交互的角度来看,多继承已经体现出组件的语义,但是缺乏组件操作的技术。他(基类)对其他组件(子类)的信息一无所知,除了基类自己所规定的接口签名。而其他组件所拥有的重要的属性:类型和状态,没有被捕获。

 

ü         有二进制复用

语义与二进制契约的一致性,提供给OO天然的二进制复用的能力。
  
想想, 头文件一包含,库一链接,不需要对库有任何了解,就可以轻松使用,这是多么的令人快乐。特别是出现像COM这样的东东,把接口专门分离出来,达到了二进制复用的最高境界:运行时自由的装配组件。(理论上)

ü         有利于发现抽象

根据我们从三个维度看到的OO特征,从抽象中直接剥离出共同性和差异性即映射到classes,对classes的结构和行为的进一步萃取,可以提炼出更精确的抽象系统。不过到了实现阶段,看下面......

 

2.3  OO 差一步

    我们通过OO捕捉共同性和差异性,建立抽象并分而治之的思路建立在这样一个观点上:“总有对象可用,且问题领域与解决方案之间的对象高度一致。亦即:应用领域的抽象总能准确的映射到方案领域的classes类族”。

    在回过头看猪的排序算法sort,是一个很明确的抽象:给我一根杠杆,我能翘起地球;给sort一个偏序集合,sort能把他由小到大串起来。如果给集合中的猪身上印上卫生局的质检印章,search还可以把猪从集合中提取出来送到菜市场。

放到解决方案中来看,sort这个抽象是如此的语义明确以至于根本不需要捕捉一致的行为和结构到接口,更不需要对接口的默认实现和差异性继承。很显然,分析出来的sort这个抽象在解决方案中并不适合搭配clasessort仅仅需要一个类型,并确定类型有一个 opreator < 。这个时候,没有什么比 c++ 中的模板提供的类型机制更贴切的了。(聪明的程序员们马上想到了concept的问题,这个........需要再写一篇)

    另:对于横跨多个抽象的“工作单元”这种情况,很难用对象来描述,因为他本质上就不是“结构与行为集合”,而是一个连续的算法调用和状态转换。

Coplien在《Multi-Paradigm Design for C++》如是说:

Domain analysis uncovers groupings of abstractions and artifacts that are tiedtogether by their commonalities—and perhaps by the similar nature of their vari-abilities. Such groupings are called families [Parnas1976]. While object-oriented design is an exercise in “finding the objects,” domain analysis is an exercise infinding the families.” Note that object-oriented design is a special case of finding families: Classes are families of objects, and class hierarchies are families of classes. We group them together because of what they have in common. We can

 

我们需要一个设计时可灵活搭配的元素,如果分解应用领域的着力点与 OO 支持的共同性差异性维度一致,那么 OO 就是最合适的。而另外的很多时候,其他的思路更适合表达业务以及方案中的抽象,可能是函数、可能是模板、还可能是表达式,更可能是关系数据库。当我们用这些技术准确的表达出抽象中体现的共同性与差异性的时候,我们想要的组件就被实做出来,

 

2.4 GP不是救世主

ü         GP是松耦合的

OO是建立在继承体系上,用“类型一致”这条线串起了所有的蚂蚱。GP则是充分的利用了编译器的信息,依靠“结构一致”的特性,释放了类型之间的耦合。对比OO,释放类型耦合意味着灵活,也意味着复杂,类型控制的重担从编译器卸载到了程序员的肩头上。

ü         强检查类型

借助于编译器的类型信息,类型不一致的行为都会被早早的抓出来。编译器会吧:放一个doublevector<int> 这种行为扼杀在编译阶段。编译期捕捉到绝大多数强约束,会大大提高程序的健壮性。

ü         只有源代码级别的复用

既然GP建立“结构一致”的基础上,那就和库的代码纠缠在一起,既然和代码纠缠在一起,就无法实现二进制的复用,起码目前来看很难实现。不过这已经很让人愉悦了,毕竟已经得到了小时候梦寐以求的可复用代码。

 

3、多范式挺合适

 

引上文:当我们用这些技术准确的表达出抽象中体现的共同性与差异性的时候,想要的组件就被实做出来。即,当语言所支持的特性准确的匹配到业务领域的共同性与差异性的时候,实现的产品就是松耦合、强内聚的组件。

那么使用多范式需要考虑两个问题,语言内置的支持和不同范式的组合。

语言的内置支持是比较固定的内容,我们从Coplien的书里扣几个表出来加以说明

 

表格 3 c++语言领域的共同性与差异性

共同性

差异性

绑定时间

实例化(此处的实例化指运行时)

c++语言机制

函数名及语义

除算法之外的所有差异性

编译

null

template

算法

编译

null

重载

 

 

 

 

数据结构

状态

运行时间

Y

小型对象

集合、标志

编译时间

Y

enum

各种运算时需要的

类型

编译时间

Y

template

状态

源码时间

Y

小型对象

算法、[数据结构]、状态

编译时间

可选

继承

算法、[数据结构]、状态

运行时间

可选

继承多态

 

从表中单独萃取出语言的每一个特性,看起来就是下面的样子:

ü         重载

共同性:一致的名字,即一致的方法语义。

差异性:接口算法不同。

抽象语义:同一个方法,要操作不同的抽象(有限个)。而我们面对的抽象接口并不一致,这时候应该为这个方法提供重载版本。

ü         继承、虚函数

额,口水都在上面用完了。这个语言机制支撑起了对象的世界

ü         函数模板

共同性:代码结构,函数模板是一个带有差异参数的算法抽象

差异性:某些特性类型会引起算法的细节上的不同

抽象语义:在绝大多数抽象类型上具备一致的算法。

ü         类模板

共同性:一致的代码结构与类的数据结构

差异性:特别的类型或值

抽象语义:c++中靠着类模板和函数模板实现对GP的支持。上面写过,GP就是建立在“结构一致”的基础上,属于源码层面的东东,必然滴导致类型耦合松散,不易表达出具体的抽象来。

 

单一的范式难以准确的描述一个系统,这时候理应借助多种范式的结合,在相互依赖的领域,此者尤甚。

继续站在CPP的角度来看,OOGP就是非常互补的范式,正确的配合两种范式,可以精确的发现和描述单一范式难以发现和描述的抽象

ü         继承缺乏技术:继承的技术被死死的钉在“对对象布局和行为集的访问规则”上,而模板则蹲在代码级,有的是技术。LokiTypelist就充分的展示了代码级的炫目技术。

ü         继承类型匮乏,继承只有向下的类型约束。这常常会导致设计阶段类型信息缺乏,而这些东西,模板把他们从编译器中拿了出来,这些类型信息,可以向上递交、水平传递、向下约束,这为设计提供了充分的弹性。

      

ü         只使用tempalte 能够特化行为,却不能特化数据结构,而捕捉结构集,是继承的强项。

template <typename Thread , typename Alloctor,typename auto_ptr> class

{

       void wight(ThreadAlloctorauto_ptr){...}

};

 

template <typename Alloctor,typename auto_ptr>

void < typename Safe_Thread , typename Alloctor,typename auto_ptr>::wight(...){......}

// 如你所见,一个或数个大大的error从编译器跳了出来,编译器不允许这样的特化扩展

 

而继承的扩展是非常的方便

class public: Thread ,

         public: Alloctor,

         public: auto_ptr

{

       void wight(Thread...){...}

}

class 受保护猪 public: Safe_Thread ,

         public: Alloctor,

         public: auto_ptr

{

       void wight(Safe_Thread...){...}

}

// 吼吼,这就是紧绑定和高阶抽象的威力了吧

// 有时候,扩展就是这么简单。

 

追本求质,还是两个基本点,base classes可以无限多,而template只能有一种形式的共同代码结构,tempalte可以借助 base classes 制作无数多的版本;base class classes紧紧绑定,classes可以借助template接触耦合。

取众之所长,就能得到我们真正想要的东西:“组件”,一种可以在设计时真正被自由组合的元素;在不同的阶段:代码、编译、装载、运行时刻可以灵活搭配的元素。

 

 

(待续)

posted on 2008-05-02 21:14  gogovista  阅读(119)  评论(0编辑  收藏  举报