忽悠一下在C++中使用IoC和DSM (1 -- 5)

 源出处:

http://topic.csdn.net/u/20071218/13/72058c4f-dcd8-4ebd-b4ad-957093283318.html

http://topic.csdn.net/u/20071218/14/1ae337cb-4a7b-4eef-ba82-0346339df3ca.html

http://topic.csdn.net/u/20071218/14/6707a5af-1ee9-45fc-acc4-80330fb4ba89.html

 

自Web和Java诞生以来,软件开发技术,概念以及架构的演变翻新有点让人眼花缭乱,甚至应接不暇。然而,恪守技术含量至上遗风的C++铁杆精英们似乎是其中的另类,与标新立异摈弃繁文褥节的Java,Ruby新生代之间俨如隔世。不用说DSM,就连IoC这个起源于C+   +年代甚至C++土壤的概念和方法虽然在墙外其他晚辈语言部落中广受青睐,在C++红墙内却反而遭长期冷落。IoC和DSM技术虽然看似简单无比平淡无奇遍地都是,甚至被很多C++大老们嗤之以鼻,但却能大大提升C++软件开发的效率及质量,而且能彻底简化和净化很多繁琐丑陋甚至危险的C++原始解决方案。比如,由IBM领头忽悠的一个所谓“服务组件架构”SCA(Service   Component   Architecture)的厂商标准,其C++组件容器的参考实现(RI)虽由IBM几个老大全时操刀,历时两年却���然困难重重地蹒跚在Apache孵化项目阶段。不仅如此,这个沿用传统思维和方法的参考实现使用起来也相当繁琐,甚至危险(用户将被迫采用无类型验证系统的C-style   cast),并且有很多极不自然甚至丑陋的限制(比如对线程模型的特殊要求)(参见   SCA   considered   harmful   一文)。与之对比,如果采用IoC和DSM技术去实现同样的SCA   C++组件容器,一个初级菜鸟程序员却能在几天甚至几小时内以区区数百行浅显易懂的代码轻而易举地大功告成,且其结果还远胜于IBM老大们呕心沥血打造的参考版(易用,类型安全,清除所有不合理限制)(参见   SCA   as   a   DSM   一文)。此类事半功倍的例子在使用IoC和DSM框架的开发中屡见不鲜。其开发效率一个数量级以上的跳跃改进也绝非天方夜谭。70年代关系数据库及SQL技术的引入就使数据库应用的开发效率提高了近两个数量级。 

1.   IoC

IoC   字面上的意思是“控制反转”(Inversion   of   Control)。然而其具体含义五花八门的说法却很容易让人一头雾水。这些说法往往是过多地关注IoC表面的甚至是字面的含义,却忽略了IoC   被用来解决的实质问题(也无视了这个概念的历史和使用现状)。Martin   Fowler   就把IoC阐述甚至更名为“依赖注入”(Dependency   Injection   )设计模式。而IoC鼻祖之一   Stefano   Mazzocchi   却指出Martin   Fowler这个忽悠了一大批人的说法实际上是不得要领(Martin   Fowler文章中代码例子确实非常误导)。所以,这里并不急于给IoC下一个教条定义,而是从IoC的实质目的开始探讨。

IoC   的概念是Michael   Mattson在1996年一篇讨论面向对象框架(Object   Oriented   Frameworks)的文章中提出的。面向对象设计及编程(OOD/OOP)的基本思想简单地说就是把复杂软件系统分解成通过接口相互合作的对象。这些对象类的内部实现之间并不互相牵扯,因而降低了问题的复杂性,且可独立灵活地被重用和扩展。自然而然,经典面向对象的编程语言(如C++,Java)的侧重点就是提供语言机制来方便并简化这种基于对象类的分解,重用和扩展。然而,一个软件系统的开发效率,可扩展性,以及部署维护升级的灵活性等并不完全由其模块化分解的程度和抽象的优劣所决定。很大程度上,能否有效清晰而又灵活地再将这些相对独立制作的分散部件组装成一个紧密合作的整体并完成其部署和配置更是决定该软件项目成败及其产品系统优劣的关键。

以支持对象分解为己任的经典面向对象语言(如C++,Java)并没有引入超越传统命令式语言(imperative   language)以外的系统组装部署和配置手段(当然,Java   5,C#现在都开始往这方面添料)。因而,虽然它们能够有效地应付底层子系统的拼装和连接,但在进行大范围基于组件(既高层业务层模块)层次的相应作业时就显得简陋,死板,冗长和低效。比如,在使用各种std的IO流类,STL容器类以及boost库类这些底层模块类时,采用语言本身的机制(自动变量或用   new算符)直截了当地实例化这些被使用类的对象就尽善尽美了。但在使用高层业务模块时,为了避免对其具体实现类的的静态依赖,人们不得不叠床架屋对语言机制进行额外的手工包装。应运而生的是一系列处理所谓管线逻辑(plumbing   logic,既非业务逻辑)的设计模式,比如factory,builder,directory,adaptor,singleton,   configuration/property   manager,factory的manager甚至manager的manager等等。遗憾的是,一个世纪以来这些实际上是弥补语言缺陷的权宜之计反而受到狂热追捧而非深入反思。另外,在传统软件的设计和实现中,业务逻辑往往直接调用这些管线逻辑,从而破坏了业务逻辑的简洁性和独立性(比如增加了单元测试的困难)。更重要的是,软件组装部署和配置的逻辑是支离破碎地散落混迹于各个业务逻辑组件中,既不直观(往往是见树不见林)也不灵活(牵一发则可能动全身)。往往使得在宏观结构上理解,维护,修改和扩充一个现有软件要反而难于当初从头开发这个软件。 

为解决以上问题,Michael   Mattson提出了面向对象框架的IoC设计原则。依照该原则,管线逻辑被转移并集中至软件框架内,业务逻辑模块并不需知道更不必调用组件框架的服务,例如不用关心和调用其factory或lookup其directory或context等。软件的组装部署和配置完全是由管线逻辑框架反过来主动控制业务逻辑模块来安排。Michael   Mattson用所谓的好莱坞原则(Hollywood   Principle)“别来电(问)我,我会去电(告诉)你”(don't   call   me,   I   will   call   you)形象地比喻了这一设计思想。这个比喻中的“我”指的是负责管线逻辑的组件框架,“你”则是被其调遣配置的一个组件。比如,在一个业务逻辑模块A需要调用另一个业务逻辑模块B的场景中,传统的非IoC的设计(比如EJB2.0)是让A调用管线逻辑(B的factory或某个directory服务)来获得B的引用(或指针)。在IoC框架内,框架不但完成A和B的实例化并保持追踪,而且B的实例引用(或指针)也是由框架主动调用A的接口函数(比如构造及赋值函数等)赋予   A。从而,A的实现可以专著于业务逻辑,而管线细节(比如B的实例化及如何获得其引用或指针)可以让外部框架透明地安排妥当。这种架构完全避免了业务逻辑对具体管线逻辑框架的牵连从而降低了业务逻辑模块的复杂度,但更重要的是集中的组装部署和配置逻辑为提高软件宏观结构的直观性和灵活性铺平了道路(后面还将具体讨论)。

简短概括一下,从概念上说,IoC就是模块化软件组装部署配置框架的一个设计原则。依该原则,业务逻辑模块既不需要处理管线逻辑也可以对外部管线框架本身一无所知(agnostic)。软件的搭接完全由外部管线框架对业务逻辑模块的主动操控来完成。从具体实现上说,除了一些开发工具以外,IoC框架不过是一个封装了必备的管线逻辑及IoC机制的轻型类库(比如PocoCapsule/C++   IoC类库大约是70K)。从使用上说,用户制作好业务逻辑组件(见下面非侵入性与POCO讨论),并将软件组装及部署描述(见后面讨论)提供给IoC框架(作为   IoC框架库函数的调用参量,或直接驱动一系列库函数调用)。IoC容器(被调用的库函数)将参照用户的描述相应地实例化和配置各个组件并将它们搭接为所希望的部署状态。 

2.   非侵入性与POCO

对于象C++和Java这类不支持动态类型的语言环境,一个很自然的问题就是业务逻辑组件需要支持什么样的公共接口以使外部IoC框架能对其进行操控。早期的组件框架(比如Apache   Avalon,EJB2.0,CORBA组件模型   CCM,JTRS-SCA等)几乎清一色地采用侵入式(invasive)设计,也就是强制规定业务逻辑模块(称作bean)必须与特定的公共组件接口模型兼容,既支持由组件框架定义的用来对组件进行部署配置的公共接口类型或基类(base   class)以及进行实例化的所谓home接口。侵入式设计不仅学习和使用繁琐(EJB2.0和CCM均是以繁为完美的恶例),也大大地限制了组建框架的开放性和适应性。因为众口难调,所以几乎每一个问题领域均定义了N个自己的组件框架和组件接口模型(比如机器人领域里就至少有10个)。侵入式设计导致组件接口模型只能被其特定的框架所支持,从而形成了各自为政老死不相往来的组件框架孤岛,限制了组件的重用范围以及框架的通用性。大量这类侵入式组件框架以及与之相应的上下左右整个配套开发体系(如果侥幸有的话)均仅仅是在小范围内被采用,并以高成本在低水平上无谓地被重复开发和维护(CCM就是这方面最恶名昭著的例子)。 

因此,现代IoC框架大都采用非侵入式(non-invasive)设计,也就是不对组件接口模型(即接口及函数签名)做任何规定。换句话说,非侵入的   IoC框架一视同仁地支持任何组件接口模型,包括已经被定义的和还未被定义的模型,也包括标准组织定义的或用户自定义的模型。这些组件接口模型可以采用(或不采用)任何公共或自定义接口,模板(template)或基类(也可以根本不是C++对象,比如是C/C++函数),可以采用任何(合理的)实例创建/生命控制回收手段和部署配置函数,包括构造或析构函数,各种自定义duplicate/release/factory/pool/directory   lookup函数,以及各种全局或成员函数等(参见开源C++非侵入式IoC框架项目   PocoCapsule/C++   IoC的介绍   )。在非侵入的C++   IoC组件框架中,因为所有组件无论其接口模型的新旧美丑高矮胖瘦均被一视同仁地按平头百姓对待,故均被统称为“平庸C++对象”(Plain   Old   C++   Object   )或POCO(相应于Java中的“平庸Java对象”POJO)。

3.   软件组装及部署描述
在使用IoC框架的C++软件开发过程中,管线模块及管线衔接代码转移并集中至IoC框架内,业务逻辑组件(高层���块或库类)的开发只需关注业务逻辑本身。软件的搭接由IoC容器(或库类)按照用户提供的软件组装及部署描述(assembly   and   deployment   description)完成。因为由分散组件搭接成的软件呈树状或更广义的图状结构,所以,软件组装及部署描述就是对这种树图结构模型的描述。这种描述通常有三种表达(编写)形式,既指令代码(code),元数据(metadata),和用户数据(data)。

虽然经典面向对象语言(比如C++和Java)可以有效地按程序步骤构造树图状数据结构,但对结构整体的“模型描述”却苍白无力。在C++中,要搭建一个树图结构的代码不外乎就是一步步地去调用类似allocNode(),addNode(),wireNodes()等等函数。树图结构模型在这种指令式的(imperative)“步骤描述”中荡然无存。对于管线逻辑比较简单的应用(例如底层或子模块),结构模型并不至关重要甚至是多此一举,而步骤描述或干脆抛弃IoC框架也许更直接了当。然而对于管线逻辑较复杂的应用,对结构搭接具体步骤描述所引入的复杂性则与使用IoC框架抽象管线逻辑的原始初衷背道而驰。

所谓元数据表述就是以编程语言的元数据结构来描述软件的管线结构模型。其实质就是利用传统指令性编程语言中类(class)结构定义的声明式语法来表达管线结构。比如,如果一个结构有10个节点(组件),按此方法就是让用户定义一个包含10个相应成员函数的类(class)并辅助以相应注释(annotation)标识管线连接。进行软件搭接时,IoC框架通过反射机制来解读这个类的结构并将其看做组件搭接的管线结构描述。这种方法,看似提供了一种声明式的(declarative)模型描述,实际上则属于一种牵强附会甚至是生搬硬套的kludge,除了能满足“只使用编程语言本身来表述管线逻辑”这一教义心态之外不具任何正面意义。

IoC   框架中有效灵活自然直观的结构描述形式恰恰就是被大牛们鄙视为恶俗的用户文本数据描述形式。无论大牛们对这种C++和Java语法机制以外的方法如何深恶痛绝,都不得不面对下面一个尴尬的窘境。C++和Java这类被他们(比如红帽Jboss的首席科学家)奉为万能银弹的编程语言中并不提供对树图结构整体具体实例的有效描述手段。因为这类语言的目的仅仅是提供对象类的包装抽象机制,而并不是提供具体多对象系统整体部署结构的模型表述方法(更不要说对各种模型之间变换,甚至变换的变换的声明式描述)。

4.   基于XML的组装及部署描述
主流IoC框架(甚至很多传统非IoC组件框架如EJB2.0和CCM)中用户文本数据形式的组装及部署描述大都是基于XML。XML的设计目的恰恰就是提供对树图结构的声明式描述。对用户来说,XML标准及技术成熟稳定,已被普遍采用和支持(各种XML解析器和工具满天飞)。另外,XML框架内具备完善的声明式结构转换技术(XSLT,XQuery),为从底层通用IoC组件部署描述提高到“针对问题域的特定建模”DSM(domain   specific   modeling)准备好了理想平台。最重要的是,与SQL类似,XML是种连编程菜鸟都大呼容易的简单直观技术,这就使得很多领域专家能够对组件构成的系统进行搭建和部署。

下面以开源项目PocoCapsule/C++   IoC框架   中的一个具体例子来介绍这个方法。这个例子的完整代码以及文档在PocoCapsule源代码包   和安装包   中均可找到。
这个例子中所要搭建部署的是一个如下图所示包含定时触发器(tick   generator),GPS定位器(gps   locator),导航显示器(navigate   display)三个组件的GPS系统。



PocoCapsule容器支持所谓POCO(既前面所说的“平庸C++对象”),从而对组件接口模型几乎没有任何限制。这三个组件的基类   TickGen,GPSLocator和NavDisplay都是由用户自己在Interfaces.h   中如下定义的:

C/C++ code
 
    
class EventListener { public : virtual ~ EventListener() {} virtual void refresh() = 0 ; }; class EventEmitter { public : virtual ~ EventEmitter() {} virtual void subscribe(EventListener * ) = 0 ; }; class TickGen : public EventEmitter { public : virtual void start() = 0 ; }; class GPSLocator : public EventEmitter, public EventListener { public : virtual int get_pos_x() = 0 ; virtual int get_pos_y() = 0 ; }; class NavDisplay : public EventListener { public : };


这些组件的具体实现类TickGenImpl,GPSLocatorImpl和NavDisplayImpl则大致定义如下(忽略所有与组装不相关细节)。

C/C++ code
 
    
class TickGenImpl : public TickGen { ... public : TickGenImpl( int cnt, int interval); ... }; class GPSLocatorImpl : public GPSLocator { ... public : GPSLocatorImpl(); ... }; class NavDisplayImpl : public NavDisplay { ... public : NavDisplayImpl(GPSLocator * loc) ; ... };


这些组件基类和具体实现类的定义以及它们的编译连接(动态或静态库均可)等开发制作方式与一般C++应用模块没任何区别,完全不需考虑IoC容器。它们甚至可以是IoC时代以前已由第三方制作好,不提供源代码的模块。在这个例子中,除了需要用到这些组件实现类的构造算子原型以外省略掉了其他所有不需要关心的实现细节。

接下来,可以用PocoCapsule的XML语法来描述这个GPS应用。PocoCapsule采用与Spring   Framework尽量相近的XML文本格式(schema,定义在poco-application.context.dtd   中),并针对C++语言特征进行了扩充。比如,将bean实例化后的setter调用普遍化为任何IoC调用。在这个文本格式中,一个POCO组件的实例将被声明为一个 元素,包含其构造算子的参数( ),实例化后的IoC   ( ),以及IoC方法的参量(也是 )等子孙元素。比如,这个例子中的GPS应用结构就可以由下面一段XML声明(见setup.xml   ):

XML code
 
    
... < poco-application-context > ... < bean class ="TickGenImpl" lazy-init ="false" > < method-arg type ="short" value ="10" /> < method-arg type ="short" value ="1" /> < ioc method ="subscribe" > < method-arg ref ="gps-locator" /> ioc > < ioc method ="start" /> bean > < bean id ="gps-locator" class ="GPSLocatorImpl" > < ioc method ="subscribe" > < method-arg ref ="nav-display" /> ioc > bean > < bean id ="nav-display" class ="NavDisplayImpl" > < method-arg ref ="gps-locator" /> bean > poco-application-context >


这段XML声明简单直观地表达了三个组件实例以及它们之间的互相衔接结构。可以形象化地将它用相应的C++形式表示如下:

C/C++ code
 
    
TickGenImpl * tick_gen = new TickGenImpl( 10 , 1 ); tick_gen -> subscribe(gps_locator); tick_gen -> start(); ... GPSLocatorImpl * gps_locator = new GPSLocatorImpl; gps_locator -> subscribe(nav_display); ... NavDisplayImpl * nav_display = new NavDisplayImpl(gps_locator);


虽然这两种描述看似表达同样的概念,但实际上他们有本质区别。C++版本表达的是构造这个应用的具体先后步骤,因此实际上必须重新修改上面C++代码行的次序才能让程序正常编译和工作。而XML版本表达的则是一种结构,而并非构造这个结构的步骤。结构中各 节点的实例化次序与它们在   XML表述中的先后次序无关,而是由IoC容器根据用户声明的节点属性(比如lazy-init的值)以及衔接时依赖关系的先后来决定。

至此,用户仅需要将这个XML描述以文件或字符串形式交给PocoCapsule/C++   IoC容器(既以文件名或XML字符串为参量调用PocoCapsule/C++   IoC库函数,见main.C   ),让其自动组装部署所描述的应用。关于PocoCapsule详细的使用及工作机制描述可参阅其入门教材   ,用户手册   ,及代码实例   。

5.   DSM和模型变换
前面一节例子中用到的XML文本格式是由PocoCapsule容器支持的核心格式(core   schema)。因为XML在IoC框架中用于描述基于组件应用的结构模型,所以这个格式也称为建模格式。该建模格式与其他IoC容器所采用的XML格式大同小异。这种格式有直观易学,格式定义紧凑,普遍适用等优点。然而,这些这些具有双刃剑特征的优点也意味着相应的缺点,诸如直接引用组件编程接口函数签名,表述力和抽象度低,容易造成冗长的声明及低级错误。比如,前面GPS例子中,XML模型描述直接涉及了有两个参量的TickGenImpl的构造算子。从该模型描述中,人们无从判断这两个参量的目的和意义。如果该模型描述声明了不匹配的参量类型,用户可以侥幸地得到IoC容器异常报告(虽然可能难于解读)。但如果这两个参量类型声明无误,而它们的数值在无意间被相互颠倒了(因为恰好是同一类型),那么用户就只能听天由命了。以setter函数取代多参量的构造或工厂函数来完成组件配置看似避免了这一问题,但实际上不但可能破坏无侵入(non-invasive)原则,而且还可能使模型描述更加冗长。同样,如果试图增强核心格式以减少这些缺点,则会牺牲其相应的优点。众多的重型组件框架(如EJB,CCM等)采用的庞大XML描述格式和令人畏惧的   UML2.0及XMI就是前车之鉴。

解决这个鱼和熊掌两难问题的一个有效方法是反其道而行之,干脆不去寻求一个能青菜萝卜一刀切“为所有人解决所有问题”的建模格式。而是提供一个开放平台,允许用户根据自己的特定需要决定取舍制定最佳建模格式,也就是所谓“针对问题域特定建模”DSM(domain   specific   modeling)。比如(参见PocoCapsule中dsm-gps例子   ),用户可以为自己要描述的GPS专门量身定制一个DSM格式gps-device.dtd   。按照这个DSM格式,前面例子中的GPS系统可以被重新描述如下(参见setup.xml   ):

XML code
 
    
< gps-device > < tick-generator use =”TickGenImpl” count =”10” interval =”1”/> =”GPSLocatorImpl”/> =”NavDisplayImpl”/> > .


与支持“通用目的建模”GPM(general   purpose   modeling)的核心格式(poco-application.context.dtd   )比较,这个DSM格式(gps-device.dtd)不但抽象度高,而且定义更紧凑。在其模型描述中完全不涉及组件接口具体函数签名,甚至连组件间具体的连接也被藏于幕后。而其配置参数的含义则一目了然。

如前所说,XML框架中的XSLT和XQuery已经为支持这个DSM格式准备好了现成的平台。这个平台提供了以声明方式描述模型之间互相转换。因而,用户在定义了一个DSM后只需要再提供该DSM格式至另一个建模格式(比如IoC框架的核心建模格式)的转换XSLT描述。那末,集成了XSLT转换器的   IoC框架(比如PocoCapsule/C++   IoC   and   DSM框架   )将会按照指定的转换描述将一个由新定义的DSM描述的结构转换为由另一个DSM定义的结构,直至递归到该IoC框架的核心格式结构。不仅建模格式可以被转换,而且模型转换的XSLT描述本身的格式也可以被转换(甚至转换的转换的转换),以简化转换描述的设计。这就是所谓的“高阶转换”HOT(higher   order   transformation   )。关于IoC框架中DSM模型转换的进一步描述可参阅PocoCapsule   DSM入门教材,   用户手册   ,及代码实例   。

一个非侵入(non-invasive)IoC框架一视同仁地支持任何组件接口模型。DSM又使其能轻而易举地支持任何用户或标准组织定义的模型描述格式。因此,一个IoC+DSM框架实际上是一个非常有效灵活的框架的框架。它让菜鸟可以轻松且高质量地实现很多组件框架,包括本文开始说的服务组件架构   SCA(参阅   ),以及软件无线电SDR的JTRS-SCA核心框架(CF)组件架构(参阅),各种机器人软件组件架构(参阅),CORBA组件架构(参阅),等等如下图所述的在   PocoCapsule中提供的DSM框架。

你可能感兴趣的:(C++及设计模式心得)