2009年10月12日

1.1 BUILDER生成器

1、 意图

将一个复杂对象的构建和它的表示分离,使得同样的构建过程可以创建不同的表示。

构建是通过对生成器抽象接口的调用实现的。而构建出来的产品的类型(表示),是由具体的进行构建的生成器的子类的实例来决定的。这样导向器只需要调用生成器的抽象接口生成产品,而具体生成什么样的产品,则是根据导向器配置的具体的生成器实例相关。

终于理解了它是如何把复杂对象的构建和它的表示分离的,也理解了为什么同样的构建过程。

2、 动机

3、 适用性

当创建复杂对象的算法应该独立于该对象的组成以及他们的装配方式时——对象的组成及装配方式是生成器的抽象接口来表示的。而对象的创建的算法是有生成器的子类实现的。

当构造过程必须允许被构造的对象有不同的表示时——也就是,同样的构造过程,可以生成不同的产品。这里是通过配置导向器的生成器的子类实例来实现的。

4、 结构

wps_clip_image-372

5、 参与者

l Builder:为创建一个product对象的各个部件指定抽象接口。这些部件最终构成了product,而对这些抽象接口的调用,则是装配product的操作,调用的次数,入参等不同,则最终生成的product也不同。product本身的构造过程可能会非常复杂。但是,这些复杂度对Director(导向者)是隐藏的。这些抽象的接口描述了产品的组成以及装配方式。

l ConcreteBuilder:实现Builder的接口以构造和装配产品的各个部件;定义并明确它所创建的表示;提供一个检索产品的接口。这个类实现了Builder的抽象接口,从而可以创建不同的表示,但是组成和装配过程还是一样的。

l Director:构造一个使用Builder抽象接口的对象。更对象根据Builder的接口来装配并生产产品。

l Product:表示被构造的复杂的对象。ConcreteBuilder创建该产品的内部表示,并定义它的装配过程;包含定义组成部件的类,包括将这些部件装配成最终产品的接口。

6、 协作

l 客户创建Director对象,并用它想要的Builder对象进行配置。

l 一旦产品不仅被生成,导向器就会通知生成器。

l 生成器处理导向器请求,并且将部件添加到该产品中。

l 客户从生成器中检索产品。

wps_clip_image-911

7、 效果

l 它使你可以改变一个产品的内部表示。Builder提供给Director的抽象接口可以使生成器隐藏这个产品的表示和内部结构。它同时也隐藏了该产品时如何装配的。因为产品时通过抽象接口构造的,你改变该产品的内部表示时,所要做的只是定义一个新的生成器。

l 它将构造代码和表示代码分开。Builder模式通过封装一个复杂对象的创建和表示方式提高了对象的模块性。每个ConcreteBuilder包含了创建和装配一个特定产品的所有代码。

l 它可以使你对构造过程进行更精细的控制。

8、 实现

l 通常一个抽象的Builder类为导向者可能要求创建的每一个构件定义一个操作。这些操作缺省什么也不做。一个ConcreteBuilder类对它有兴趣创建的构建重定义这些操作。

l 装配和构造接口:一般构造请求的结果只是被添加到产品中,特殊情况下,需要返回给导向器。

l 产品没有抽象类:一般他们没有公共的部分。如果有也可以设置一个抽象类。

l Builder中缺省的方法为空。

9、 代码示例

class MazeBuilder {

public:

    virtual void BuildMaze() { }//部件的构造方法

    virtual void BuildRoom(int room) { }

    virtual void BuildDoor(int roomFrom, int roomTo) { }

    virtual Maze* GetMaze() { return 0; }

protected:

    MazeBuilder();

};//MazeBuilder是生成器

class StandardMazeBuilder : public MazeBuilder {

public:

    StandardMazeBuilder();

/*

*/

    virtual void BuildMaze();

    virtual void BuildRoom(int);

    virtual void BuildDoor(int, int);

/*

*/

    virtual Maze* GetMaze();

private:

    Direction CommonWall(Room*, Room*);

    Maze* _currentMaze;

};//StandardMazeBuilder是ConcreteBuilder,提供部件的具体构造代码

Maze* MazeGame::CreateMaze (MazeBuilder& builder) {

    builder.BuildMaze();

    builder.BuildRoom(1);

    builder.BuildRoom(2);

    builder.BuildDoor(1, 2);

    return builder.GetMaze();

}//CreateMaze是导向器,调用生成器的抽象接口完成产品的构造过程。

//下面代码描述产品的构造过程

Maze* maze;//最终的产品

MazeGame game;//Director,导航者

StandardMazeBuilder builder;//ConcreteBuilder,实际的构造类

game.CreateMaze(builder);//开始装配

maze = builder.GetMaze();//获取装配后的产品

posted @ 2010-03-31 21:07 常高伟 阅读(224) | 评论 (0)编辑 收藏

1.1 ABSTRACT FACTORY 抽象工厂

1、 意图

提供一个创建一系列相关或相互依赖对象的接口,而无需指定他们具体的类。

2、 动机

“客户仅与抽象定义的接口交互,而不使用特定的具体类的接口。”

这里的主要的思想是封装对象的创建的过程。客户端可以不需要知道具体要创建那些对象,而只需要知道创建某一系列的对象所用到的“工厂对象”即可。

3、 适用性

一个系统要独立于它的产品的创建、组合和表示时。 

一个系统要由多个产品系列中的一个来配置时。 

当你要强调一系列相关的产品对象的设计以便进行联合使用时。 

当你提供一个产品类库,而只想显示它们的接口而不是实现时。 

4、 结构

wps_clip_image-278

5、 参与者

AbstractFactory:创建一系列对象的抽象类。

ConcreteFactory:实现具体创建产品对象的操作。

AbstractProduct:为一类产品对象声明一个接口。

ConcreteProduct:定义一个被相应的具体工厂创建的对象;实现AbstractProduct接口。

Client:仅使用AbstractFactory和AbstractProduct类声明的接口。

6、 协作

在运行时刻,创建一个ConcreteFactory实例,它创建具有特定实现的对象。为创建不同的对象,客户应使用不同的具体工厂。

AbstractFactory将具体对象的创建延迟到它的子类ConcreteFactory中。

7、 效果

1) 它分离了具体的类:一个工厂封装创建产品的责任和过程,它将客户和类的实现分离。客户通过抽象接口操作实例。产品的类名也在具体工厂实现中分离,他们不出现在客户代码中。

2) 它使得易于交互产品系列。

3) 它有利于产品的一致性。

4) 难于支持新的种类。

posted @ 2010-03-03 20:27 常高伟 阅读(171) | 评论 (0)编辑 收藏

1.1 设计模式怎样解决设计问题

1.1.1 寻找合适的对象

面向对象设计最困难的部分是将系统分解为对象的集合。

设计的许多对象来源于现实世界的分析模型,这里和领域驱动设计有点关联。分析所得到的类,很多事现实中并不存在的类。这是抽象的结果。设计中的抽象对于产生灵活的设计至关重要。就像我设计的一个流程调度模型。

1.1.2 决定对象的粒度

记笔记可以让我达到沉流的状态。

1.1.3 指定对象接口
1.1.4 描述对象实现

OMT表示法:

1、 对象:最上面的黑体表示类名,下面依次是操作,数据。

wps_clip_image-219

2、 实例化:虚线箭头表示一个类实例化另外一个对象。

wps_clip_image-245

3、 继承:竖线和三角表示继承关系。

wps_clip_image-263

4、 抽象类:类名以黑体斜体表示,操作也用斜体表示。

5、 引用

wps_clip_image-293

箭头加黑点表示一个类引用另外一个类。

重点:

1、 类的继承和接口继承的比较

对象的类和对象的类型的区别:

对象的类定义了对象是怎样实现的,同时也定义了对象内部状态和操作的实现。对象的类型只与它的接口有关。一个对象可以由多个类型(支持多个接口),不同类的对象可以有相同的类型。

类和类型紧密相连,类定义了对象的操作,也定义了对象的类型。

类的继承和接口的继承的差别:

c++中接口继承接近于公有继承纯抽象类。纯实现继承或纯类继承接近于私有继承。

2、 对接口编程,而不是对实现编程——面向对象设计的第一个原则

1.1.5 运用复用机制

1、 继承和组合的比较

继承是一种白箱复用,父类的内部细节对子类可见。

对象组合彼此不知道对方内部细节,成为黑箱复用。

继承的优缺点:

1) 子类可以直接重定义父类的操作。

2) 编译时刻决定了,无法在运行期间更改。

3) 子类要知道父类的实现细节,这样就部分破坏了封装性。子类和父类依赖过于紧密,父类的某些变化必然导致子类的变化。开发过程中遇到过类似的问题。这种依赖,限制了灵活性以及复用性。比如,服务体系中经常出现这样的问题,导致代码拷贝。

组合(通过获得对象的引用而在运行时刻动态的定义)的优缺点:

1) 对象间通过接口彼此交互。

2) 对象只能通过接口访问,不要也不能知道对方细节,这样不会破坏封装性。

3) 运行时刻可以使用另外一个对象替换这个对象,提高了灵活性。

4) 对象的实现基于接口编写,所以实现上存在较少的依赖关系。

5) 优先使用组合有助于保持每个类被封装,并被集中在单个任务上,提高整体内聚性。类和类的层次都维持一个较小的规模,

6) 基于对象组合的设计会有更多的对象(而又较少的类),且系统的行为依赖于对象间的关系而不是定义在某个类的内部。

理想的情况下,应该通过组合原有构件实现新的功能,而不是创建新的构件。

面向对象设计的第二个原则:优先使用对象组合,而不是类继承。

2、 委托

委托时一种组合方法,它是组合具有与继承同样的能力。

委托的主要优点在于它便于在运行时刻组合对象操作,以及更改操作的组合方式。它是软件更加的灵活。

和其他的技术方案相同,它也存在不足之处:增加了软件的复杂度——动态的,高度参数化的软件比静态的软件更难于理解。

3、 继承和参数化类型的比较

1.1.6 关联运行时刻的结构和编译时刻的结构
1.1.7 设计应支持变化

设计应该支持变化——所说的是,一个设计方案,对变化要有一定的适应性,即封装变化。

变化是导致重新设计的原因。设计要对一定范围内的变化友好。

4、 

对于程序的分层设计,对于处于同一分层的模块,对外应保持一定的抽象,并且,使用同种类型的通信协议。

posted @ 2010-03-03 20:26 常高伟 阅读(415) | 评论 (0)编辑 收藏

1.1 变量存储域

1.1.1 一个示例

pang123hui首先提供了一个网上流传的学习代码示例:

int a = 0; //全局区 

void main() 

{

int b; //栈 

char s[] = “abc”; //s在栈,abc在文字常量区 

char *p1,*p2; //栈 

char *p3 = "123456"; //123456在常量区,p3在栈上 

static int c =0; //全局区 

p1 = (char *)malloc(10); //p1在栈,分配的10字节在堆 

p2 = (char *)malloc(20); //p2在栈,分配的20字节在堆 

strcpy(p1, "123456"); //123456放在常量区 

}

这个代码示例中出现了“全局区”,“栈”,“文字常量区”,“堆”等词语。为了统一,我们使用《C专家编程》中的说法:堆栈段,BSS段,数据段,文本段。

各个段的作用如下:

1、 文本段:包含程序的指令,它在程序的执行过程中一般不会改变。

2、 数据段:包含了经过初始化的全局变量和静态变量,以及他们的值。

3、 BSS段:包含未经初始化的全局变量和静态变量。

4、 堆栈段:包含了函数内部声明的局部变量。

当然,上面段的作用不仅于此,具体的作用会在下面的知识点中介绍。

1.1.2 通过代码测试变量的存储位置

Linux下可以通过系统命令“size”查看可以执行程序各个段的大小。但是,可执行程序中的段结构和运行中程序在内存中的段结构并不完全相同,但是有一定的映射关系。具体如下图所示(图片信息来自《C专家编程》):

wps_clip_image-696

下面通过代码示例和“size”来研究变量的存储区域。

test.c

int main()

{

return 1;

}

编译,并且查看可执行程序各个段的大小:

wps_clip_image-779

更改test.c:

int g_data;

int main()

{

return 1;

}

编译,并且查看可执行程序各个段的大小:

wps_clip_image-849

可以发现,文本段,数据段都没有发送变化,而BSS段增加了4个字节。

结论1:未初始化的全局变量保存在BSS段中

继续:

int g_data = 1;

int main()

{

return 1;

}

编译:

wps_clip_image-958

可以发现,BSS段和文本段相同,而数据段增加了4个字节。

结论2:经过初始化的全局变量保存在数据段中

继续:

int main()

{

static int g_data;

return 1;

}

编译:

wps_clip_image-1066

可以发现,文本段,数据段都没有发送变化,而BSS段增加了4个字节。

结论3:未初始化的静态变量保存在BSS段中

继续:

int main()

{

static int g_data = 1;

return 1;

}

编译:

wps_clip_image-1183

可以发现,BSS段和文本段相同,而数据段增加了4个字节。

结论4:经过初始化的静态变量保存在数据段中

继续:

int main()

{

int i_data = 1;

return 1;

}

编译:

wps_clip_image-1288

可以发现,BSS段和和数据段相同,而文本段增加了16个字节。局部变量会在执行的时候在堆栈段中生成,函数执行完毕后释放。

结论5:函数内部声明的局部变量保存在堆栈段中

继续:

const int g_data = 1;

int main()

{

return 1;

}

编译:

wps_clip_image-1430

把全局变量定义为“const”后,也许你会感到奇怪,怎么BSS段和数据段都没有发生变化,而文本段却增加了4个字节。

结论6:const修饰的全局变量保存在文本段中

那么,const的局部变量?

继续:

int main()

{

const int i_data = 1;

return 1;

}

编译:

wps_clip_image-1587

结论7:const修饰的局部变量保存在堆栈段中

继续:

char *pstr = "";

int main()

{

return 1;

}

编译:

wps_clip_image-1666

在做一下更改:

char *pstr = "123456789";

int main()

{

return 1;

}

编译:

wps_clip_image-1733

可以发现,前后数据段和BSS段大小均未发生变化,而文本段增加了9个字节。

结论8:字符串常量保存在文本段中

1.1.3 结论

1、 经过初始化的全局变量和静态变量保存在数据段中。

2、 未经初始化的全局变量和静态变量保存在BSS段。

3、 函数内部声明的局部变量保存在堆栈段中。

4、 const修饰的全局变量保存在文本段中,const修饰的局部变量保存在堆栈段中。

5、 字符串常量保存在文本段中。

1.1.4 扩展阅读

《C专家编程》第6章——详细介绍各个段的作用。

posted @ 2010-03-03 02:38 常高伟 阅读(278) | 评论 (0)编辑 收藏

读S计划的理念

自助、互助,共同进步!

 

读S计划的初衷

现在有很多学习方式,搜索引擎、论坛、博客,qq群等等,那么我这样的计划还有存在的必要么?这个计划的独特之处在哪里?

读S计划的独特之处不在于其学习内容和方式,学习的内容我们可以根据实际情况调整,可以由多个人同时引导多个学习方向,这些都不是程式化的,也不是重点。

读S计划的独特之处在于其理念:致力于形成一种有计划、有组织,强调互动、互助和共同进步的学习模式和氛围。读S计划不是一个单纯的兴趣小组,我们会按照计划不断的切换话题。读S计划也不是提问的好地方,在每个阶段,我们都有限定的讨论范围,所以无关的问题很容易被忽略。读S计划希望多数人以积极的心态参与进来,更多的讨论、研究和贡献您的想法,而不是被动的接受和看,我们不希望将学习计划做成课堂模式,我们希望做成项目的模式 。读S计划致力于将信息以信息本身进行聚合,而不是以人进行聚合,因为学习计划在不断推进,所以不存在永远的权威人士,但知识本身是客观、中立的、权威的。

 

共建新型技术社区

建立新型的、关系紧密的、以知识体系为轴、强调经验积累与分享的的技术社区。

 

更深入了解此计划,可以阅读:

《技术族谱 之 读S计划:让我们一起快乐学习》

《准备启动一个开源项目 - 技术族谱 - 先期利用Goolge云计算平台》

《关于“读S计划”的聊天记录:统一理念,确定目前的工作》

《授人以“鱼”,不如授人以“渔”,放弃一个目标,设定另一个目标》

期待你加入我们的群组:http://hi.csdn.net/space-mtag-tagid-37.html

posted @ 2009-10-14 21:43 常高伟 阅读(191) | 评论 (0)编辑 收藏

上面一篇文章大致描述了一下插件开发框架整体结构。这篇描述一下核心层的设计和实现。

至于核心层的设计,我想借鉴 一下微内核的思想。核心层只负责实现下面几个功能:

1、 插件的加载,检测,初始化。

2、 服务的注册。

3、 服务的调用。

4、 服务的管理。

插件的加载,检测,初始化

插件的加载利用linux共享库的动态加载技术。具体的方法可以看一下IBM网站的一篇资料《Linux 动态库剖析》

服务的注册

服务的注册与调用采用表驱动的方法。核心层中维护一个服务注册表。

//插件间交互消息类型
typedef enum __Service_Type
{
    Service_Max,
}Service_Type;
//插件用于和其他插件通信接口函数,由插件提供。
typedef PRsp_Ele_Stream (*PF_Invoke_Service_Func)(PReq_Ele_Stream pele_str);
//驱动表
typedef PF_Invoke_Service_Func Service_Drive_Table[Service_Max];

驱动表是一个数组,下标为插件间交互消息类型,成员为插件提供的接收的消息处理函数,由插件初始化的时候,调用插件框架的的注册函数注册到驱动表。

插件的初始化实现为:
//插件用于注册处理的消息类型的函数,由插件框架提供。
typedef RET_RESULT (*PF_Service_Register_Func)(Service_Type service_type);
//插件用于和其他插件通信接口函数,由插件框架提供。
typedef PRsp_Ele_Stream (*PF_Invoke_Service_Func)(PReq_Ele_Stream pele_str);
//插件回复响应函数。插件收到异步请求后,处理完成后,发送响应消息给请求的插件。由插件框架提供
typedef void (*PF_Send_Response_Func)(PRsp_Ele_Stream pele_str);
//初始化插件信息
typedef struct Plugin_Init_St
{
    PF_Service_Register_Func register_func;//服务注册函数,要注册一系列的枚举值。插件可以处理的服务枚举值
    PF_Invoke_Service_Func invoke_serv_func;//和其他组件交互时,调用的用于和其他组件交互的函数。发送请求消息。
    PF_Send_Response_Func send_rsp_func;//再设计一个回复响应消息的接口。收到异步请求后,处理完毕后通知请求模块处理结果。
} Plugin_Init_St, *PPlugin_Init_St;
//初始化插件函数,类似于构造函数。由插件提供,供插件框架加载插件时初始化插件使用。
void PF_Init_Plugin(PPlugin_Init_St pinit_info);

      插件在函数PF_Init_Plugin中调用函数register_func来注册插件要处理的消息类型。

服务的调用
//信元结构体
typedef struct Ele_St
{
    Ele_Tag tag;
    Ele_Length len;
    Ele_Value  value;
    PEle_St next;
}Ele_St, *PEle_St;
//请求消息,信元流格式。
typedef struct Req_Ele_Stream
{
    Plugin_ID src_id;//源插件id
    Service_Type req_type;//请求类型
    PEle_St ele;
} Req_Ele_Stream, *PReq_Ele_Stream;
//响应消息,信元流格式。
typedef struct Rsp_Ele_Stream
{
    Plugin_ID dest_id;//目的插件id
    Service_Type req_type;//响应对应的请求的类型。
    Execute_Result result;//记录执行结果
    Execute_Reason reason;//记录执行结果的原因
    PEle_St ele;
} Rsp_Ele_Stream, *PRsp_Ele_Stream;
//接收插件调用服务请求函数,由插件提供,入参为请求信元流。返回值为响应信元流,用于同步请求处理。
PRsp_Ele_Stream PF_Receive_Invoke_Proc(PReq_Ele_Stream pele_str);
//插件收到响应消息的处理入口函数,由插件提供。如此为响应信元流。
void PF_Receive_Rsponse_Porc(PRsp_Ele_Stream pele_str);

插件间的依赖关系是通过信元流来实现的。至于信元流的使用在我的另一篇博客《使用信元流(TLVStream)规范、简化模块(C/C++)间交互 》 中有描述。插件对外的接口都是统一的。

如果插件要和其他的插件通信,则调用PF_Init_Plugin函数的传递的服务调用接口: invoke_serv_func。插件框架根据信元流的类型,查找驱动表,找到对应的服务接收函数。插件用函数 PF_Receive_Invoke_Proc接受其他插件的请求,此函数是插件想插件框架主动注册到驱动表的。

如果服务时同步的,这直接通过此函数返回,返回的信息在响应信元流中。如果是异步的请求,这插件在处理完成后,通过 send_rsp_func函数来发送响应。

插件的卸载
//卸载插件时调用的函数,类似于析构函数。由插件提供,供插件框架卸载插件时调用。
void PF_Destroy_Func();

posted @ 2009-10-12 20:20 常高伟 阅读(1213) | 评论 (0)编辑 收藏

这几天为了设计插件开发框架,尝试用了一下发散思维来思考问题。中间看过依赖注入,AOP(面向方面编程),以及契约式设计等。虽然有些工具无法直接使用,但是这些思想还是可以借鉴的,比如依赖注入,契约式设计。至于AOP,和工具相关性较大,虽然思想不错,但是无法直接在C++中使用。

我设计的插件间的依赖不是通过接口实现的,而是通过插件间的数据(信元流)。而信元流的检测可以使用契约来检查。

插件开发框架的总体结构

结构图

微内核

1、 负责插件的加载,检测,初始化。

2、 负责服务的注册。

3、 负责服务的调用。

4、 服务的管理。

扩展层:

1、 日志的打印。

2、 消息(信元流)的解释,将二进制格式解释为文本。便于定位。

3、 消息和日志的追踪。

分布式处理层:

1、 用于和其他的框架通信。

2、 和其他的框架搭配,形成一个分布式的系统。

自动化测试框架层:

1、 集成 cppunit 。

2、 自动化集成测试框架。

3、 自动化功能测试框架。

和第三方框架集成层:

1 、和 第三方框架 集成层。

posted @ 2009-10-12 20:19 常高伟 阅读(740) | 评论 (0)编辑 收藏

原创  使用信元流(TLVStream)规范、简化模块(C/C++)间交互 收藏

  问题描述:
  在软件开发过程中,一般会对复杂的现实世界进行抽象,并且采用分而治之的策略,将大系统分解为子系统,将子系统分解为一个个的模块。模块间的通信一般采用函数调用的方式,这样会产生一些问题:
   1. 模块间的接口增多会导致模块间紧密耦合,不利于模块的重用、调试、维护。
   2. 接口函数参数会很复杂,容易出现很庞大,功能面面俱到的结构体。
   3. 不利于扩展,新增功能要新增接口函数,或者修改接口参数。


信元流的定义
  信元流是一种处理数据的方式。它对现实世界的数据进行抽象,形成一个一个的信元,然后由这些信元以一定的方式组合起来,形成信元流,来传递数据。下面信元流的实现。
  信元的实现
  信元由三部分组成,分别是信元标识(Tag),信元长度(Length),信元值(Value),即TLV格式。下面是一个信元的c定义:   
  typedef struct __ELEMENT_ST

     {

          ELE_TAG tag;

          ELE_LEN len;

          ELE_VALUE value;

     }ELEMENT_ST;

  信元流的实现:
  信元流分为两部分,信元流头部(head)和体部(body)。head部分用于记录信元流的整体描述,比如,信元流的起始模块,目的模块,以及信元流的消息类型等等。当然,也可以根据需要自己进行扩展。body部分包括信元流中信元的总大小,即ELE_MSG_BODY中body的长度。

        typedef struct __ELEMENT_STREAM_ST
        {
            MOD_ID src_mod;
            MOD_ID des_mod;
            ELE_MSG_TYPE type;
            ELE_MSG_BODY body;
        }ELEMENT_STREAM_ST;
        typedef struct __ELE_MSG_BODY
        {
            ELE_MSG_LEN len;
            char body[MAX_ELE_MSG_BODY_LEN];
        } ELE_MSG_BODY;

  构造信元流
  定义好结构体后,下面定义两个函数,分别向信元体中添加信元和删除信元。
   //pbody信元体的指针。这里没有使用信元流的指针是为了让函数的重用性更好,用户可以自己定义信元流。
        //tag:添加信元的TAG值,len:添加信元的长度。pvale:添加信元的值。
        int AddElementToStreamBody(ELE_MSG_BODY *pbody, ELE_TAG tag, ELE_LEN len, void *pvalue);
        //pbody信元体的指针。 //tag:获取信元的TAG值,buf_len:pbuf的长度。pbuf:目标缓冲区,要把信元VALUE的值写入吃缓冲区。
        int GetElementFromStreamBody(ELE_MSG_BODY *pbody, ELE_TAG tag, int buf_len, void *pbuf);
  信元流的body是一个缓冲区,信元在里面顺序排列。信元在body中顺序不影响它的获取。添加和获取的方法比较简单,不再赘述。


  一个信元流的实例
  下面来举一个具体的例子来介绍一下信元流的使用,以及它的优点。
  假如由两个模块A和B。A模块负责处理业务逻辑,B模块负责处理呼叫控制。A调用B的一个接口发起呼叫,接口如下。
        typedef struct __MAKE_CALL_ST
        {
            char caller_num[MAX_NUM_LEN];//主叫号码
            char called_num[MAX_NUM_LEN];//被叫号码
        }MAKE_CALL_ST;
        int MakeCall(MAKE_CALL_ST *pcall_info);

  后面需求有更改,某些情况下药携带主叫的callid信息。结构体会变成:
    typedef struct __MAKE_CALL_ST
    {
        char caller_num[MAX_NUM_LEN];//主叫号码
        char called_num[MAX_NUM_LEN];//被叫号码
        CALL_ID caller_callid;
    }MAKE_CALL_ST;
  某些情况下又需要携带主叫的SDP信息,结构体会变成:
    typedef struct __MAKE_CALL_ST
    {
        char caller_num[MAX_NUM_LEN];//主叫号码
        char called_num[MAX_NUM_LEN];//被叫号码
        CALL_ID caller_callid;
        SDP_INFO call_sdp;
    }MAKE_CALL_ST;
  随着需求的增加,这个结构体会越来越大,并且,其中的成员在某些情况下要使用,某些情况下又不使用,造成模块间的冗余数据。
  当然,你也可以采用其他的方法,比如,再多定义几个接口和结构体。但是这样的话接口和结构体的数量会呈爆炸式的增长。
  使用信元流可以很好的解决这个问题。把号码,callid,sdp等全部定义成信元,需要的时候塞进去,不需要的话就不添加。另外还有一个好处就是,一个模块可以对外只公布一个接口,来收取信元流,然后在根据信元流的类型进行分别处理。这样,一个模块对外就只有一个接口了,模块间的耦合性会降低。


一点改进
  上面定义的信元流的格式在实际使用的过程中还是碰到了一些问题,最突出的就是,信元流的大小是固定死的。这种情况下,如果信元信息很小,会导致空间浪费,效率降低;如果信元信息很多,信元流的空间又不够。

  可以对上面的这种方案进行一下优化,把信元的定义更改为:

  typedef struct __ELEMENT_ST

     {

          ELE_TAG tag;

          ELE_LEN len;

          ELE_VALUE value;

          ELEMENT_ST  *pnext_ele;//下一个信元流

     }ELEMENT_ST;

  将信元流的定义更改为:

        typedef struct __ELEMENT_STREAM_ST
        {
            MOD_ID src_mod;
            MOD_ID des_mod;
            ELE_MSG_TYPE type;
            ELEMENT_ST  *pfirst_ele;//第一个信元流
        }ELEMENT_STREAM_ST;

  将信元流和信元更改为动态申请的内存。这样既可以提高效率,有没有了大小的限制。

  需要增加两个接口,来申请和释放信元流。

  唯一不好的地方时,动态申请的内存需要程序员记得释放,否则会内存泄露。不过还有一个方法,即增加一个申请信元流的函数,如下
        ELEMENT_STREAM_ST *GetEleStream()
        {
            static     ELEMENT_STREAM_ST *pstream = NULL;
            if (NULL != pstream)
            {
                FreeEleStream(pstream);   
                pstream = NULL;
            }
            pstream = AllocteEleStream();
            return pstream;
        }

  这样的话,通过函数GetEleStream获取的信元流,只在函数范围内有效,退出函数后,立即无效。

posted @ 2009-10-12 20:18 常高伟 阅读(341) | 评论 (0)编辑 收藏

在这一系列的上一个文章中,介绍了构建C/C++插件开发框架的初步设想,下面我会一步步的向下展开,来实现我的这个设想。

今天主要谈一下我对这个框架的功能认识,或是期望。昨天看了一篇关于持续集成能力成熟度模型 的一篇文章,受此启发,我对此框架的认识渐渐清晰。

这个框架可以当做我们公司底层产品(交换机,资源服务器等)的基础设施。上层基于java开发的产品可以直接在OSGI上开发。

核心功能:

1、最重要的一个功能是,提供一个模块化的编程模型,促进模块化软件开发,真正的实现针对接口编程。

2、提供一个有助于提高模块可重用性的基础设施。

3、提供一个C/C++插件的运行环境。

4、提供一个动态插件框架,插件可以动态更改,而无需重启系统。这个功能虽然不难实现,但是用处好像不是很大。


扩展部分功能:

1、支持分布式系统结构,多个运行框架组合起来形成一个系统,对模块内部隐藏远程通讯细节。

2、支持系统的分层架构。

3、能够和其他的开发框架进行集成,比如OSGI,SCA等。

4、多个运行框架中,能够实现对运行框架的有效管理。

5、概念上要实现类似于SCA中component(构件),composite(组合构件),Domain(域)的概念。


开发部分功能:

1、为了简化开发,开发一个Eclipse插件,用于开发框架中的C/C++插件。能够根据插件开发向导,最终生成符合插件规范的公共代码,配置文件,Makefile文件等。


调试部分功能:

1、提供一个统一的日志处理函数,可以集成Log4cpp。

2、提供模块间的消息日志,以及框架对外的接口日志。

3、提供消息和日志的追踪功能,能将和某事件相关的消息和日志单独提取出来。

4、提供资源监测功能,监测对资源(内存,套接字,文件句柄等)的使用情况。


测试部分功能:

1、集成一些单元测试框架,比如unitcpp,达到自动化单元测试的目标。

2、自己实现自动化集成测试框架,并且开发相应的Eclipse插件,简化集成测试(利用脚本和信元流)。

3、集成原有的自动化功能测试框架flowtest,并且开发相应的Eclipse插件,简化功能测试。

4、实现性能测试,监测框架。


部署部分功能:

1、实现自动化部署。特别是在分布式应用的情况下。

2、提供一个命令行程序,通过命令更改系统配置,管理插件。

posted @ 2009-10-12 20:18 常高伟 阅读(844) | 评论 (0)编辑 收藏

 最近一直在学习OSGI方面的知识。买了一本《OSGI原理和最佳实践》,可是还没有到。遗憾的是,OSGI目前的几个开源框架只支持Java,对C和C++都不支持的。可惜我们公司目前主要的开发语言还是c和c++,即便是引进OSGI,所得的好处范围有限。而我对松散耦合的模块化开发向往已久。查了一下OSGI对C++支持的好像是有一个开源项目,不过好像应用范围很小。而SCA标准中是有对C++实现模型的支持的,但是几个开源的框架目前还只支持JAVA。

  昨天看了丁亮的转载的一篇博客《C/C++:构建你自己的插件框架 》,原文的链接:http://blog.chinaunix.net/u/12783/showart_662937.html 。看了一下里面讲的方法,自己倒是可以实现。所以有了构建自己的c/c++插件开发框架的想法。今天先写一下初步的设想。

C/C++插件开发框架的要素

  BlueDavy有一篇介绍服务框架要素的文章(链接:http://www.blogjava.net/BlueDavy/archive/2009/08/28/172259.html )。我的插件框架也要考虑、解决以下的几个问题:

  1、如何注册插件;

  2、如何调用插件;

  3、如何测试插件;

  4、插件的生命周期管理;

  5、插件的管理和维护;

  6、插件的组装;

  7、插件的出错处理;

  8、服务事件的广播和订阅(这个目前还没有考虑要支持);

  其中有几个点很重要:1)插件框架要能够使模块松散耦合,做到真正的面向接口编程;2)框架要支持自动化测试:包括单元测试,集成测试;3)简化部署;4)支持分布式,模块可以调用框架外的插件。

采用的技术
  插件框架要解决的一个问题就是插件的动态加载能力。这里可以使用共享库的动态加载技术。当然,为了简单,第一步只考虑做一个linux下的插件框架。

  总体结构

  框架的总体结构上,参考OSGI的“微内核+系统插件+应用插件”结构。这里要好好考虑一下把什么做在内核中。关于微内核结构,以前我做个一个微内核流程引擎,会在后面有时间和大家分享。

  框架中模块间的数据传送,有两种解决方法:一是普元采用的XML数据总线的做法。优点是扩展性好,可读性好。但是速度有些慢。二是采用我熟悉的信元流。优点的效率高,访问方便,但是可读性差一点,另外跨框架的数据传送,需要考虑网络字节序的问题。

  对于框架间的通信,通过系统插件封装,对应用插件隐藏通信细节。

      部署

      努力做到一键式部署。

posted @ 2009-10-12 20:16 常高伟 阅读(2399) | 评论 (0)编辑 收藏

《重构》第三章学习笔记

我们必须培养自己的判断力,来决定在什么时候进行重构。

1.1  Duplicate Code(重复代码)

如果你在一个以上地点看到相同的程序结构,那么将他们合而为一会更好。

1.2  Long Method(过长函数)

拥有短函数的对象会活得比较好,比较长。

间接层所能带来的全部益处:解释能力(可读性),共享能力(重用性),选择能力(?)。

现在OO 语言基本解决了函数调用所产生的开销。

“ 你应该更积极进去的分解函数。我们遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个函数中,并以其用途(而非实现手法)命名。我们可以对一组甚至短短一行代码(拥有复杂逻辑,难以理解)做这件事。哪怕替换后的函数调用动作比函数自身还长,只要函数名称能够解释其用途,我们也该毫不犹豫的这么做。关键不在于函数的长度,而在于“做什么”和“如何做”之间的语义距离。 ”

“如何确定该提炼哪一段代码?一个很好的技巧是:寻找注释。它们通常是指出“代码用途和实现手法间的语义距离”的信号。如果代码需要用注释来说明其用途,那么就要考虑把这段代码提炼成独立的函数,并且用注释来为此函数命名。”

复杂条件式和循环液常常是提炼的信号。

1.3  Large Class(过大类)

如果想利用单一的class 做太多的事情,其内往往会出现太多的 instance 变量。

如果class 中拥有太多的代码,也是“代码重复、混乱、死亡”的绝佳滋生点。

1.4  Long Parameter List(过长的参数列表)

过长的产生导致程序难以理解。

1.5  Divergent Change(发散式变化)

“ 一个class 受多个外界变化的影响 ”,则把这多个变化封装成一个新的类。即“ 将总是一起变化的东西放在一起 ”

针对外界某一变化所有相应的修改,都应该只发生在单一的class 中,而这个 class 的所有内容都应该反映该外界变化。总的思想就是,封装变化。这个地方和设计模式的想法是一致的。

1.6  Shotgun Surgery(散弹式修改)

和发散式变化不同,每次遇到变化,都要在多个class 中进行小的修改以响应之。他们分散在多处,很容易出错。

这里的主要思想是集中变化。

散弹式修改指的是,“ 一种变化引发多个class 的修改 ”,发散式变化指的是“ 一个class 受多个外界变化的影响 ”。

这两种情况下,通过重构, 使“外界变化”和“待修改类”呈一对一关系 的理想境地。

1.7  Feature Envy(依恋情节)

某个函数对其他类的数据的兴趣,高过对host class 的兴趣。即对其他的类的数据的依赖十分大。

1.8  Data Clumps(数据泥团)

数据泥团指的是总是绑定在一起出现的数据。

一个好的评断方法:删除众多数据中的一项数据,其他数据是否是因而失去了意义?如果他们不再有意义:你应该为他们产生一个新的对象。

形成新的对象后,可以根据Feature Envy 将一些操作移至此对象中。

1.9  Primitive Obsession(基本型别偏执)

建立多个很小,但是很灵活的对象。

1.10  Switch Statements( switch 惊悚现身)

使用面向对象编程,要少用switch 和 case 语句。而是用多态来替换它。

1.11  Parallel Inheritance Hierarchies(平行继承体系)

每当你为一个class 增加一个 subclass 的时候,必须为另一个 class 增加一个 subclass 。一般这两个 class 的前缀相同。

1.12  Lazy Class(冗赘类)

类显得多余,没有价值。

1.13  Speculative Generality(夸夸其谈未来性)

这个往往是过度设计的结果:对某种变化的应对,而这种变化没有发生。

1.14  Temporary Field(令人迷惑的暂时值域)

变量只在特定的情形下有效,而并不是所有的情况下有效。很多情况下,这些值域应该不属于此class ,而应该单独的提取成新的类。

1.15  Message Chains(过度耦合的消息链)

用户向一个对象索取另一个对象,然后在向后者索求另一个对象,然后在索求另一个对象——客户与查找过程的航行结构紧密耦合。

1.16  Middle Man(中间转手人)

对象的基本特征之一就是封装——对外部世界隐藏实现细节——封装往往伴随委托。委托的过度运行,就导致了Middle Man 。

1.17  Inappropriate Intimacy (亲密关系)

两个class 之间的关系过于亲密。比如,花大量的时间探究彼此的 private 成分。

1.18  Alternative Classes with Different Interface(异曲同工的类)

类名不同,但是功能相似。

1.19  Incomplete Library Class(不完美的程序类库)

基础类库无法满足实际的需求。

1.20  Data Class(纯稚的数据类)

它们拥有一些值域,以及用于访问(读写)这些值域的函数,除此之外一无长物。

1.21  Refused Bequest(被拒绝的遗赠)

子类不像继承父类的函数和数据,这往往是继承体系的错误。

如果子类复用父类的行为,但又不愿支持父类的接口,这种情况下Refused Bequest 的坏味道会很强烈。

1.22  Comments(过多的注释)

注释其实是一种香味,更多的情况下它被用作除臭剂:即代码中出现坏味道(设计糟糕的代码),然后用注释“除臭”。这个时候我们应该对这些坏味道的代码进行重构,然后,你会发现注释变成了多余的。

当你感觉需要注释,请先尝试重构,试着让所有的注释都变得多余——代码本身就是自注释的。

注释可以用来记述“为什么做某事”、“打算做某事”、“无十足把握的区域”,而不必记录“怎么做”。

posted @ 2009-10-12 20:15 常高伟 阅读(781) | 评论 (0)编辑 收藏

以前做过一个产品,共分为三层:平台层,应用服务器层,应用层。其中有一个业务流程,实现是在应用层,但它那里的信息不全,需要通过应用服务器层向平台层获取必要的业务数据,然后通过应用服务器层控制业务流程。当时考虑这个结构的时候,主要的出发点就是业务和控制分离,将业务处理从平台层剥离开来。当时,在具体是实施过程中,我们工程师对这种结构抵触心理很强烈。他认为我们的业务开发非常的繁琐,而且经常要贴“狗皮膏药”。

先抛开上面这个实例的设计思路,这里面反映出一个问题:软件开发过程中,软件体系结构同样需要“重构”

结合经典的《重构》,这里简单的写一下软件体系结构重构的定义,原因,设计,方法。仅作抛砖引玉,希望能和大家一起思考。

何谓重构

对软件体系结构的一种调整,目的是在不改变其“外在行为”的前提下,调整其结构,使其易于修改,维护和理解。

为何重构

1、使整个系统易于添加新的功能。为系统添加新功能将会非常的容易。

2、调整系统中各个模块的功能,角色,使整个系统更容易理解。
何时重构

由于系统结构的重构成本非常高,所以要选择一个合适的重构时机。

1、为系统添加功能时重构。此时项目进度压力如果非常大,这放弃此时重构。

2、软件第一个版本开发完毕后重构。在第一个版本开发完毕,第二个版本开发之前,根据第一个版本的开发经验,对系统进行重构。

3、开发出系统原型时进行重构。开发出一个系统的原型的时候,如果发现系统需要重构,这及时的进行,这个时候重构成本较低,但对是否重构决策要求较高。

重构的必要条件

重构之前必须为软件系统建立一个可靠的、自动化的功能测试环境,这样才能有效防止重构带来的危害。好的测试时重构的根本。重构之前,首先检查自己是否有一套可靠的测试机制。这些测试必须有自我检验(selfchecking)能力。

重构与设计

系统架构层次的重构,因为重构的成本相对较高,所以预先设计的程度要相对较深,要尽量考虑系统可能遇到的情况,方案要适当的灵活和强固。尽量减少系统结构的重构。

软件体系结构的重和代码的重构的区别

1、针对的层次不同:一个是系统结构层次的,一个是代码层次的。

2、重构成本不同,系统结构的重构成本相对较高。

3、是否重构的决策者不同。

4、重构的时机不同。

posted @ 2009-10-12 20:14 常高伟 阅读(334) | 评论 (0)编辑 收藏

在学习的不同的阶段我们应该保持的心态:
1、接触之前,拥有一个好奇的心态。与自己原有的知识对比,不要对新的知识产生偏见。比如,原来一直采用瀑布式软件开发,在接触敏捷软件开发之前,不要对敏捷软件开发模式产生偏见:“敏捷开发根本不能够和瀑布式开发相提并论”。偏见会阻碍新知识的学习,而是要对敏捷开发保持一种好奇心:

1)这种理论为什么会提出来?
2)主要为了解决什么问题?
3)现有的理论无法解决此问题吗?
4)它是基于什么样的原理?
5)它是如何运行的?

2、学习了一段时间,拥有一定基础之后,要以一种批判的、怀疑的心态来对待学习的知识。当你拥有了对新知识的一定认识后,有可能会发现它很好解决了你以前遇到的一些非常棘手的问题,或者发现你找到了某一领域问题的完美的解决方案。这个时候,你要让自己坚信,“没有银弹”,没有包治百病的灵丹妙药。任何方案、理论、技术都有其依赖的条件,或者在解决原有问题后,引入的新的问题。此时,你要以一种批判的、怀疑的态度来对待它。要搞清楚:

1)它所依赖的前提条件是什么?
2)应用它的时候有没有什么假设?
3)它在那些场景下适用、那些场景下不适用?
4)它在解决原有问题之后,有没有引入新的问题?

3、当你达到专家级或顶级水平之后,应该持有一种“独孤求败”的心态:寻求新的挑战,争取“百尺竿头更进一步”,就像独孤求败一样,”闯荡江湖,只求一败 “:把每次的失败看成进步的机会。同时保持敬畏、谦虚的心态,你所掌握的知识,也不是”银弹“,也有其局限之处,或者随着环境的发展,已经无法适应环境,需要寻求新的突破。这个时候,你要根据你丰富的实践,提出创新的思路或知识。
总之:
1、在学习新知识、新领域的时候,要保持一个空杯的心态,只有这样才能为自己敞开自我提升之路。
2、偏见导致对新知识的排斥,教条倒是对新知识的盲从。
3、没有绝对的真理,也没有绝对的谬论。绝对的真理让我们盲从,绝对的谬论让我们偏见。

posted @ 2009-10-12 20:13 常高伟 阅读(123) | 评论 (0)编辑 收藏

1  构筑测试体系

如果你想进行重构,首要前提就是要拥有一个可靠的测试环境。

“编写优良的测试程序,可以极大的提高我的编程速度,即使不进行重构也是如此。”

1.1  自我测试代码(Self-testing Code )的价值

“Class 应该包含他们自己的测试代码。”

“每个Class 都有一个测试函数,并用它测试自己这个 Class 。”

确保所有的测试都完全自动化,让它们检查自己的测试结果。

只要写好一点功能,就立即添加测试。

一整组(a suite of )测试就是一个强大的“臭虫”侦测器,能够大大缩减查找“臭虫”所需要的时间。

“实际上,编写测试代码的最有用时机是在开始编程之前。当你需要添加特性的时候,先写相应的测试代码。听起来离经叛道,其实不然。填写测试代码其实就是问自己:添加这个功能需要做什么。编写测试代码还能使你把注意力集中于接口而非实现上头(永远是件好事)。预先写好的测试代码也为你的工作按上一个明确的结束标志:一旦测试代码运行正常,工作就可以结束了。”

构建自我测试的代码。

1.2  JUnit测试框架( Testing Framew )

频繁的运行测试,每次编译请把测试也考虑进去,每天至少执行每个测试一次。

单元测试和功能测试

“每当你接获臭虫提报,请先撰写一个单元测试来揭发这只臭虫。”——如何揭发?这里需要根据报告准确定位。单元测试会对此有帮助吗?

1.3  添加更多的测试

“观察Class 该做的所有事情,然后针对任何一项功能的任何一种可能失败的情况,进行测试。”

“测试应该是一种风险驱动(risk driven )行为,测试的目的是希望找出现在或未来的可能出现的错误。”

“测试的诀窍是:测试你最担心的部分。”

这点和我目前的想法不大相同。我目前的想法是,测试要对程序做100% 的保证,所以,要测试程序可能行为的每一种情况,保证其正确性。按照我的想法,值域的设置和访问函数也是要测试的。作者的意思是,测试代码要用最低的成本,获取最大的收益。这一点,要我在实际的环境中进行抉择。

“编写不是十分完美的测试并实际运行,好过对完美测试的无尽等待。”——我持怀疑态度。

运用测试用例前后执行的函数:tearDown 和 setUp ,保证测试用例之间相互隔离,而非相互影响。

做一个懒惰的程序员——。

考虑可能出错的边界条件,把测试火力集中在那儿。

“测试(优先)可以调高编程速度”,这一点我要在实践中验证一下,如果真是这样,那我就要尝试在我们部门推行这种方法。

“当测试达到一定的程度后,测试效益会呈现递减态势。”所以,你不要期望通过测试找出所有的bug ,而是要通过测试,找出绝大多数的 bug 。

这个地方其实也符合“二八定律”:即20% 的测试可以找出 80% 的 bug ,其余的 80% 的测试可以找出剩下的 20% 的 bug 。我们要做的,就是写这 20% 的测试,而非 100% 的测试。

posted @ 2009-10-12 20:11 常高伟 阅读(140) | 评论 (0)编辑 收藏

上一篇介绍了微内核流程引擎开发背景,这篇介绍它的功能描述。

基本功能:

1、能够通过脚本定义流程,更改流程。

2、对软交换系统应用服务器的所有的接口都可以编辑。

3、异常处理,实现补偿机制。

4、流程要支持:顺序执行,分支处理,跳转执行。

5、脚本中支持简单的数据库操作,比如:记录查询(根据查询结果决定流程),字段查询,记录增删改。


扩展功能:

1、提供多种调用形式:1)动态链接库直接调用;2)socket通信调用;3)远程调用;4)WSDL方式调用。

2、实现一个流程引擎虚拟机。专门处理流程。

3、支持业务以无状态的形式开发。所有的状态在脚本中定义。

4、开发一个流程编辑界面。

5、开发一个脚本编译器,检查脚本的错误。

6、开发一个简单的语言,实现快速流程编辑的功能。这里要实现一个编译器,编译结果就是流程脚本。

7、实现一个方向编译器,从流程脚本到流程开发语言。

上面的这些功能有的已经实现,有的正在实现。后面我会详细描述这些功能的设计与实现。

posted @ 2009-10-12 20:08 常高伟 阅读(238) | 评论 (0)编辑 收藏

我设计的流程引擎是脚步驱动的。脚本中定义了流程执行的环境,流程操作的对象,流程执行的步骤。下面是一个流程脚本的示例:

<?xml version="1.0" encoding="utf-8"?>
<process name="make_call">
    <data type="user_tel">called_number</data>
     <object type="user" id="global_data:called_number" operation="must">obj_user</object>
     //用户对象描述中,号码是必须的,是流程引擎和业务的交互唯一标识,callid是可选的。
    <object type="user" object_num="global_data:called_number" operation="must">obj_user</object>
    <sequence name="make_call">
        <invoke interface="make_call" node="make_call_001" object_user="obj_user" calling_number="6699" original_number="123456" call_type="local_call">

        </invoke>
        <invoke interface="play_voice" node="play_voice_001" object_user="obj_user" play_long="100" play_file="/home/welcome.au">

         </invoke>   
    </sequence>
</process>


脚本的含义

1、process的name属性表示流程的名称,用于在程序中调用。

2、<data type="user_tel">called_number</data>表示定义了一个流程的全局外部变量。有程序在调用流程是作为流程数据传送给流程。这个数据要在后面的流程中使用。

3、<object>部分在流程中定义流程操作的对象。一般分为用户和会场。这里表示是用户。属性“id”表示对象的唯一标识。这里引用的是流程的全局数据:global_data:called_number,也就是在上面定义的数据。属性“operation”表示此对象是可选还是必选。如果是必须,这如果此对象被释放,这流程也要被被结束。否则,不结束。中间的内容表示对象在流程中的唯一标示,这里是obj_user,后面的节点可以通过使用它来操作对象。

4、<sequence>表示顺序调用下面的节点。

5、<invoke >表示调用节点。属性“interface="make_call"”表示此节点调用的接口是make_call。make_call是在代码中定义好的关键字,对应一个软交换系统的接口。属性“node”表示节点的唯一标识,在流程内部唯一,可以在流程跳转的时候使用。 “object_user="obj_user"“表示make_call 接口操作的对象。有<object>创建。 calling_number="6699" original_number="123456" call_type="local_call"表示的是make_call接口调用时的数据。

6、<invoke interface="play_voice"表示对此对象进行放音。

这个脚本的意思是,根据流程输入的号码,创建用户对象,并且发起呼叫,对用户进行放音。


复杂的脚步定义:

上面的是一个简单的示例。为了能够实现流程编辑,要考虑很多的情况,要能够进行分支处理,跳转执行,捕获事件等。

1、分支的实现

    <recive event="user_key" ="" node="receive_key" object_user="obj_user" time_out="10"></recive>
    <switch condition_type="user_key" object="obj_user">
            <case condition="9092">
                <sequence name="d">
                </sequence>
            </case>
            <case condition="time_out">
                <sequence name="d">
                </sequence>
            </case>           
            <otherwise>
                  <sequence name="">
                      <goto node="play_voice_001">goto_001</goto>
                  </sequence>
            </otherwise>
    </switch>

1)<recive event="user_key"表示接受指定用户的按键。如果超过10秒为收到按键则认为用户按键结束。

2)<switch condition_type="user_key"表示一用户的按键为分支条件,进行分支处理。

3)<case condition="9092">表示如果用户的按键式0092的话则进入此分支进行处理。

4)<case condition="time_out">如果超时为收到用户按键,这进入此分支处理

5)<otherwise>如果上面的条件都不满足,则进入此分支处理。


2、跳转的实现:

<goto node="goto_001" next_node="play_voice_001"></goto>

表示此节点是一个跳转节点,要跳转到的下一个节点是play_voice_001。


3、信号捕获的实现:

       <pick name="pick_001" time_out="10">
            <on_event event="on_ring_180" result="success" reason="normal">
                <sequence name="008">
                </sequence>
            </on_event>
            <time_out>
                <sequence name="008">
                </sequence>
            </time_out>
            <otherwise event="on_ring_180:on_ring_183">
                  <sequence name="009">
                    </sequence>
            </otherwise>
        </pick>

1)<pick name="pick_001" time_out="10"><pick>活动会等待一组相互排斥事件中的一个事件的发生,然后执行与发生的事件相关联的活动。它会阻塞业务流程执行,以等待某一特定的事件发生,比如接收到一个合适的消息或超时警报响起。当其中任何一个事件被触发后,业务流程就会继续执行,pick也随即完成了,不会再等待其他事件的发生。

2)<on_event event="on_ring_180" result="success" reason="normal">表示如果收到的on_ring_180,且结果是success,原因是normal。触发此流程的处理。

3)<time_out>表示超时为收到制定事件的处理。

4)<otherwise event="on_ring_180:on_ring_183">表示收到其他的事件,比如:on_ring_180或on_ring_183,都进入此分支处理。

posted @ 2009-10-12 20:08 常高伟 阅读(180) | 评论 (0)编辑 收藏

开发背景

我们公司是主要从事企业语音方面产品的开发,主要产品比如:调度系统,指挥系统,电话会议系统,呼叫中心系统等。这些系统都有一个共同特点,就是涉及到呼叫,放音,收发按键,会场操作。我们的业务产品都是基于我们的软交换系统之上构建的,软交换系统的应用服务器向外提供这些服务。


产生的问题

我们在开发的过程中就发现一个问题,每个产品在此接口上都会做很多重复的开发,特别是在IVR处理上面。

IVR (Interactive Voice Response)即交互式语音应答,可以提高呼叫服务的质量并节省费用。IVR是一种功能强大的电话自动服务系统。

由于我们的系统重新架构,正在开发软交换API接口,所以我们试图解决这个问题。

为了方便上层对底层接口的调用,减少重复开发,产品能够针对用户需求迅速更改,我们提出了事务流的概念。即上层程序设定一个事务流,比如:相对用户9092发起呼叫,用户应答后对用户放音,让用户输入密码,密码验证成功加入制定会场。上层程序设定好这样的事务流后,发送给下层进行处理。下层处理完毕后,回复处理结果。

事务流的概念最终并没有实现,一是我们对事务流的异常处理分析不足;二是采用事务流的概念扩展性,可维护性不是很好。

后来我们在事务流的基础上引进了SOA的概念,即将底层的接口封装为一系列松散耦合的服务,在上层通过对服务的编排实现流程编辑的功能。但是实施SOA困难非常大,一是SOA没有大规模的应用,文档,经验都比较少,实施风险较大;二是实施 SOA要工具的支持,需要投资。这样的话我们宁可重复开发。

最终这个流程编辑的功能并没有完成,但是我们还是在API中实现了一个简单的流程编辑,这个流程编辑是通过硬编码来实现的,和我们的愿望相差较大。


新的思路

后来看了一家平台软件公司的介绍,他们是做企业应用平台的。企业应用中很多的一点就是工作流。他们的平台就是集成了一款开源的工作流引擎:JBPM。第一次接触JBPM,感觉很多思路可以借鉴。后来在网上搜资料的时候搜到几篇研究微内核流程引擎的文章:

揭秘jbpm流程引擎内核设计思想及构架

微内核过程引擎的设计思路和构架

微内核工作流引擎体系架构与部分解决方案参考

这几篇文章是我设计流程引擎的核心基础。看完之后最终决定自己设计一个微内核流程引擎。


使用开源还是自己开发

决定动手开发前,有两种方式,一是使用开源产品,比如JBPM;二是自己动手开发。当时的考虑是:

1、JBPM是用java开发,我们公司的业务产品目前基本上都是c,和java交互不便。

2、JBPM是针对企业工作流设计的,工作流和IVR导航一个重要的区别是,IVR导航对时效性要求很高。要求系统能够及时响应。

3、我不是JAVA出身,当时对java还有些畏难心理。

所以,最终决定自己用C++开发流程引擎。

如果是现在的话,即便是决定自己开发,一会好好研究一下JBPM的源码的。


借鉴的其他思路

1、数据驱动(DD)。在脚本中XML定义流程,程序启动后将流程读入程序,然后供上层程序调用。如果流程更改,这直接修改脚本即可。

2、BPEL。Business Process Execution Language 的缩写,意为业务过程执行语言 ,是一种基于XML 的,用来描写业务过程的编程语言。我的脚步语言参考BPEL的描述。

posted @ 2009-10-12 20:05 常高伟 阅读(369) | 评论 (0)编辑 收藏


posts - 19, comments - 0, trackbacks - 0, articles - 0

Copyright © 常高伟