以语言为中心
--论CTI中间件的设计

一、语言还是拖放式流程图?
二、什么样的语言?
三、语音平台的实现


就CTI应用而言,开发出独立于硬件、独立于具体应用的中间件或语音平台,是非常自然的想法。随便在网上以“CTI中间件”或“语音平台”为关键字搜索,结果可谓多如牛毛。当然,各个产品的侧重点不同,在宣传上也各出奇招,不免让人眼花缭乱。下面的讨论针对面向中小规模的以板卡为主的CTI中间件,TAPI之类的开发接口虽然也有人称之为CTI中间件,但太过底层,这里就不讨论了。

下面所说的语言基本上等同于脚本语言,运行在虚拟机上的都称为脚本语言,JAVA好像没有人叫作“JAVA脚本语言”,呵呵。


一、语言还是拖放式流程图?

我的结论是语言。准确的说是语言优先,或以语言为中心。

流程图是可有可无的装饰,更多的时候甚至是有害的累赘。在很小的IVR应用中,看起来还不错,还算清晰明白。不幸的是大部分应用都很复杂,数据库很复杂,业务流程很复杂,而且经常变动,有时候还要进行复杂的运算。流程图变得像蜘蛛网一样复杂,变得难以理解。

看看Brooks的名著<人月神话>对流程图的批评,我们也许会得到一些启发:"流程图是被吹捧得最过分的一种程序文档。...但当它被分成几张时,也就是说需要采用经过编号的出口和连接符来进行拼装时,整体结构的概观就严重地被破坏了。"(第15章)

你不要告诉我这个流程图不等于那个流程图。本质上他们是一样的,甚至CTI的流程图更加复杂。

拖放式流程图的设计者寄希望于最终用户也能开发或至少能修改CTI应用流程,这些最终用户不必了解编程技术,通过用鼠标拖拖放放就把复杂的应用轻松搞定。实际上学会流程图的“拖放”方法并不简单,让他们去构造复杂应用是不现实的,最终用户是系统的最终使用者。他一般也不敢冒险去直接开发系统,CTI中间件的使用者一般是系统集成商,是有一定编程经验的开发人员。与其让他们使用这种功能有限的流程图,还不如直接使用表达能力更加丰富的CTI语言。

拖放式流程图的拥护者往往认为图形化的东西容易理解,IVR系统都会给使用者一个直观的操作流程图,如:

点击浏览大图

但这个操作流程图和CTI拖放式流程图是不一样的,操作流程图很简单,只需要标明功能就可以了,它不需要指出什么变量操作、数据库访问等内容,也没有复杂的连线和判断。它实际上只是一个需求说明书。这个需求如果用CTI拖放式流程图实现的话,与上面这张图相比至少复杂了数倍,反而无法看清真正的业务操作流程。

拖放式流程图的拥护者也往往认为图形化是软件设计的潮流,君不见VB,Delphi大行其道?这和应用有关系,很多桌面应用其操作界面就是图形化的。而CTI应用本质上是一种服务程序,它是多路的、甚至是在后台进行的;它的操作也不是图形界面的交互,而是电话的、语音的操作。

实际上,大部分支持拖放式流程图的中间件,最终也是将流程图翻译成脚本语言。为了适应流程图,脚本语言变成了“节点描述文件”,阅读困难,修改不易。面对复杂应用,也有一些拖放式流程图的支持者,说刚开始开发应用的时候可以先用拖放式流程图,然后再修改脚本文件。这样一来用户更加麻烦,在两者之间转来转去,劳心费力不说,还增加了用户的学习负担。

我们面对的世界越来越复杂,应用越来越庞大。以语言为中心,使用简单而富有表现力的语言,必然可以面对这些挑战。

怎么样的语言,这是个问题。


二、什么样的语言?

我们来看Ruby语言的设计者日本人Yukihiro Matsumoto的一段话:"基于图灵机的完整性理论,每一种具备图灵完整性的语言能做的事情理论上都可以由另一种具备图灵完整性的语言所代替,只是花费的代价不同。您可以用汇编语言实现所有的功能,但是现在已经没有人再想用汇编编程序了。"("Ruby的设计思想"-《程序员杂志》2003.12)

的确如此,其实还有很多直接用板卡厂家提供的API用C语言编写的CTI应用程序在很好地运行,我们只要简单地考察一下市面上的CTI中间件,给最终用户提供的功能上都大同小异。但是对于一种语言,从复杂程度,应用开发者的“感觉”上,差距甚大。我很推崇C++之父Bjarne Stroustrup博士的一句话:“C++是一种通用的程序设计语言,其设计就是为了使认真的程序员工作的更愉快”。

我曾见过某个国内著名厂商的中间件,其脚本语言的变量是什么SR1,IR10之类,简直就是汇编语言的寄存器。至少我感觉不到“愉快”。

1. VoiceXML?

VoiceXML是没有微软参与的一种所谓标准,我个人感觉语法上太烦琐,做一个简单的事情要写一大段话。并且XML的优势在于描述、包装数据,而不是控制流程。较好的方式都是传统编程语言如JAVA, C++去操纵XML描述的数据。VoiceXML当然有用,与Web页面结合,做互联网的语音门户,别忘记购买自动语音识别(ASR)和文本合成语音(TTS)。

实际上,微软TTS也用XML标识一些复杂的文本(数据),当然,微软不把它叫做VoiceXML.

也许,这只是我这种有C/C++背景人的偏见,我也期待世界大同,愿智者示教。

2. 面向对象还是面向过程?

我的选择是面向过程。

面向过程可以采用结构化的编程方式,最容易理解,也与实际的语音操作流程相吻合。

面向过程可以采用函数调用的编程方式,也很容易学习。

最后一点,面向过程很容易实现。

3. 面向事件?

有些CTI中间件完全基于事件的处理,比如,每播放一个语音,播完了是个事件,按键打断是个事件,播放失败是个事件,用户挂机是个事件。太烦琐。如果是面向过程的,我认为应该这样简单:

SetBreak(true); // 允许打断
...
Play("InGddm.wav"); // 播音:“请输股东代码”
gddm = "";
GetKeys(gddm, 10); // 收码最多10位,放置在gddm变量中
...

我认为应用程序真正要处理的事件只有两个,一个是系统挂断事件,还有就是系统退出事件。两者都中断了正常执行的流程,应用程序需要关闭一些打开的资源,清理现场。

4. 语法结构

类似C语言的语法结构应该被首先考虑,看看C++, JAVA, C#的发展就知道了。

我的选择:

  1. 1). 不要指针, 但最好能支持数组;
  2. 2). 不要goto;
  3. 3). 变量不要预先定义,不要显式的变量类型,也就是动态类型,如:
  4. 不必先定义: int i;
    直接写: i=0; // i隐含就是整型
    或: s = "你好"; // s隐含就是字符串型
    s = i; // s又被变成整型了
    至少要有三种隐含的变量类型,整型,浮点型,字符串。布尔类
    型和整型一致。
  5. 4). 可以定义全局变量和局部变量, 可以定义符号常量
  6. 5). 支持复杂表达式, 如:
  7. if( i==mon*30+day || money > (1000.26+v2)*0.5 ) 或: money = 12800.76; name = "bluesen"; s = "金额是: " + money + "; 姓名是: " + name;
  8. 6). 支持while(){}循环和for()循环, 但不要do{}while()循环如:
  9. for(i=0; i<20; i++)
  10. 7). 支持switch(){case()}多分支语句;
  11. 8). 支持文件包含;
  12. 9). 支持用户自定义函数,参数变量为引用调用.

三、语音平台的实现

语音平台就是虚拟机,可以在不同的硬件上开发不同的虚拟机。当然,大家都知道只需要把硬件接口独立出来,对不同的硬件加以实现,这个我称之为虚拟硬件接口,如果用C++实现就是虚拟基类。

理想的设计是语言应该是独立的、自包含的,对于一般的CTI应用,用这一种语言就可以全部完成,所以要有一套强大的可以扩展的库,比如数据库、TCP/IP操作,需要定义各自一致的接口。

举例来说,对于呼叫中心,ACD,IVR等等都应该在虚拟机上完全用语言来实现,这就是所谓的CTI服务器。当然座席端软件不用这个语言实现。

1. 配置

很容易想到每条线路运行一个脚本,不同线路可以运行不同的脚本,每条线路使用独立的线程。

给语音平台设计一个配置文件:

0 = Ivr1.txt // 第一条线路,运行Ivr1.txt的脚本
2-7 = Ivr2.txt // 第3条线路到第8条线路,运行Ivr2.txt的脚本

2. 扩展

定义好一个动态连接库接口,可以开发出各种外部应用,这些应用可以和传入变量,也可以传出变量。而调用起来就像系统库函数或用户自定义函数一样。

比如,短信网关,E-mail收发,都可以用动态库扩展来实现。这样全部都纳入虚拟机的体系之中,不必考虑复杂的外部通讯。

3. 虚拟线路

可以在语音平台上配置虚拟线路,虚拟线路不对应实际的硬件。它继承自虚拟硬件接口,所以可以运行全部语音操作的库函数,当然不会有什么反应。

虚拟线路的出现,可以在没有实际硬件的情况下调试脚本语言,检查应用的逻辑是否正确。

虚拟线路的更重要的应用是:可以在上面运行数据库操作、短信收发等与语音硬件无关的脚本,构成应用网关。

4. 线路间通讯和调度

对于某些资源,让每条线路都去独立地访问是不现实的。比如数据库访问,如果系统的线路数比较多,如100线以上,系统效率将非常低下。某些数据库的访问连接数是有限制的,可能要花钱购买。

有一种解决方式是,让虚拟线路成为数据库访问网关(服务),物理线路向网关发出请求,由网关代为服务,再把结果返回给物理线路。

这样就要有高效的线路间通讯模式。

  1. 1). 消息队列
  2. Unix的进程间通讯之一就是是消息队列。
  3. 每条线路自带4组消息队列,还有4组全局消息队列。
  4. 任何一条线路均可以向其它线路或全局队列投递消息,也能检查是否
  5. 有消息到达,或收取消息。
  6. 消息采用先进先出的数据结构。
  7.  
  8. 2). 共享全局变量
  9. 一般情况下语言的变量局限于本线路,别的线路无法存取。
  10. 共享全局变量提供一种机制,让每条线路均可以存取约定的
  11. "符号"-值 对。
  12. 共享全局变量可以成为线路间的信号量。

上述线路间通讯模式都是在内存里面完成的,不必借助于外部数据库,因而效率很高。

5. 调试和开发环境

像现代的语言集成开发环境(IDE),理想的调试和开发环境至少能设置断点,单步执行,查看变量,跟踪到用户自定义函数内部等等。

  1. 这个世界不存在完美的东西,它只存在于我们的内心。
  2. 语言,正如一位哲人所说,你一开口说话就已经被人误解。
  3. 也许我们所要做的就是:不要试图发明一种新语言。

bluesen 2003.12.16 于深圳