为了能够驱动语音卡进行工作,需要如下组件相互协作来共同完成。ChannelManager是工作站层面的虚拟机,由它加载板卡适配器并使系统进行就绪状态;CTICardDriver板卡适配器,通过其命名我们就可以了解到其主要是对板卡驱动程序的封装,完成驱动的加载与卸载工作,适配器的另外一个重要任务就是在ChannelManager的初始化进程中创建其支持的通道;Channel语音通道,实现了内线通道、外线通道、录音通道、传真通道及虚拟通道等具体类,在创建时由具体的适配器进行实例化工作。其类结构层次如下图所示:
(图3.1 板卡适配器层的类结构)
此图是在进行本次系统重构之前完成的,虽然已经初具LightweightCTI的雏形了,但是其中仍然存在一些问题:
1、 每个类需要完成太多的工作,以至于有些类显得很“肥”,这违反了单一职责原则(SRP),从图中我们可以看到AbstractChannel中包含了Debug、RunScripts及Run等本应该属于脚本引擎接口的一些方法;
2、 类之间的依赖过多,违反了面向对象设计的基本原则 – 封装性,信息隐蔽也就没有办法很好的实现啦:)
……为此上图并不是当前LightweightCTI源代码所支持的结构,当然至少是优于上图的。我们将结合源代码详细对每个接口进行介绍。
由上图我们可以看到通道管理器充当着十分重要的角色,它不但需要负责维护适配器的加载与卸载工作,而且还兼管着系统中所有已经初始化的通道。有点类似于MVC模式中的控制器的角色。ChannelManager的详细接口定义如下:
(图3.2 ChannelManager接口定义)
在代码中我们看到了CTICardDrivers及Channels两个属性的定义,通过它们就可以检索到系统中已经初始化的板卡适配器及所有通道。而将CTICardDrivers的访问级别定义为保护级是并不想将其公开给应用开发人员使用,他们也不关心是如何实现适配器的。而应用开发人员需要使用Channel实现具体的业务功能,所以将其访问级别定义为公共级。那么通道管理器是如何来驱动适配器及通道的呢?让我们看看下图就明白啦。
(图3.3 适配器层初始化的时序图)
以下为通道管理器初始化时的代码片段。
(图3.4 通道管理器初始化代码片段)
从1721行到1753是从系统配置文件中读取每个适配器的配置信息并根据配置的类型实例化相应的适配器(对应于时序图1.2CreateCTICard消息),值得注意的是系统目前已经支持从DLL或系统注册的类中加载适配器。这样将使系统更具灵活性,如此在部署系统以后将可以根据配置信息来改变系统所使用的语音板卡适配器,而不用重新编译系统。在实例化适配器时并没有立即对其进行初始化,而是先对其进一步配置,然后在第1762行发出Initialize消息(对应于时序图
板卡适配器的功能之一是完成对应语音卡驱动程序的加载与卸载工作,除此之外它更重要是充当类工厂的角色,适配器要完成通道对象(Channel)的创建工作。在图6中还不十分明显,请看下图。
(图3.5 板卡适配器与通道的类结构图)
通道管理器并不需要知道如何创建每个通道,这个工作交给适配器来完成。为此将可以很好的屏蔽通道实例化的细节,同时,也可以根据系统配置信息灵活改变所要创建的通道类型。在上图中通过简单的应用工厂方法这一设计模式就大大的提高了系统的灵活性,使得对于适配器及通道的创建工作都不用硬编码到程序中。
适配器的接口十分简单,其中只包含了2个只读属性与4个方法:
(图3.6 板卡适配器接口)
另外,为了能够在适配器的加载与卸载过程中通知到用户程序,在适配器基类中加入了三个事件,用户程序将可以通过事件回调函数来控制适配器的初始化、释放允许、释放工作。
(图3.7 板卡适配器通知事件)
通道是支持语音相关业务应用开发的组件,只有通过它才能完成如挂机、摘机、来电显示、播放语音等具体功能。在LightweightCTI中它与应用逻辑最靠近,但是它又是最为简单的一个类。
(图3.8 通道接口)
在Channel接口中除了为数不多的几个属性外,其余都是一些与电话语音相关的函数,在具体的实现类中将由它们完成拨打电话、播放语音等操作。
接口函数主要分为以下几类:
Ø 振铃以及摘机、挂机函数;
Ø 获取主叫和被叫号码函数;
Ø 拨号及放音函数,包括通过文件放音或TTS放音函数;
Ø 录音函数;
Ø 连通函数,通过此类函数可以实现内线外呼、外线转人工及电话会议的功能。
看到这里,你不禁会问就这点接口怎么实现实际应用中的催缴、语音查询、固话彩信等功能啊?各位看官莫急切听我慢慢道来。说到这里让我们回过头来再看看图1所帖出的代码,里面有挂机检测、对方忙检测、无人接听以及所有这些情况下对应的业务逻辑。虽然其让人感觉好像是一个完整的语音应用啦,而这也正是程序本身的蔽病所在。
业务逻辑与板卡本身的处理混合在一起,程序结构一点也不清晰,如果想知道一个业务是如何完成的,必须在各状态节点之间跳来跳去,阅读这种程序是否有点头晕呢?如此程序扩展或后期的维护工作都很困难。如想增加一个处理用户按错键的状态,也必须清楚的理解所有程序逻辑才能进行改动,否则的话将使程序陷入混乱之中,而通过对Channel的封装你将可以像编写一般的程序一样,轻松的完成业务逻辑,开发人员可以将更多精力集中于系统业务的开发。下面是使用LightweightCTI模拟电话银行的程序片段:
(图3.9 东进电话银行模拟程序)
实现电话银行演示程序总共的代码仅仅42行,而采用原始状态机方式完成电话银行演示系统的代码,大家打开演示程序就明白了。当然,在这简单明了的演示业务逻辑背后LightweightCTI框架做了许多工作,比如上面说的挂机检测、摘机、放音及获取主叫或被叫号码等函数。那么Channel内部究尽做了些什么工作呢?让我们抽丝剥茧一层层揭开它神秘的面纱吧。
首先,从板卡驱动层面来看,Channel只是板卡的一种资源(外线、内线、录音及传真等),因此驱动接口(API)都将其作为一个参数来看待,如东进公司DBDK开发包中的相关函数RingDetect(wChnlNo: word): Boolean,这样系统中多条通道要同时运行的话就需要对每个通道进行轮询(也就是一个循环),当业务逻辑复杂后整个循环体少则数百行代码多则数千行,这是相当恐怖的;
其次,板卡驱动层的接口代码与业务逻辑代码混在一起加重了程序的复杂性,因而业务逻辑的修改变得十分困难,更降低了软件的复用性,如果从一种板卡移植到另一种板卡非要重新写过全部程序不可;再次,系统中满眼都中各种状态,其要么进行了一下定义如Welcome1、Hint、GetIVR等,要么干脆是一些100、200、300的数字,而程序逻辑则穿梭于这些状态之间,就如一碗意大利面条,这样的程序又有几个人看得懂呢?
最后,也是最重要的一点是程序在一个循环内不断的轮询来自每一个通道的信号,致使所有系统资源都被你的语音应用程序占据着,经常有人问怎么自己的程序都没怎么运行而CPU占用率却在70/80%以上,这就是问题所在。
而采用LightweightCTI框架中的Channel进行语音应用开发,上述提到的种种问题都将迎刃而解。为此,我们首先要解决的是让每个通道在自己独立的工作空间运行,在Windows环境下有进程、线程两种方式。经过考察进程显然不能满足我们的要求,一方面对进程进行初始化需要耗费很多资源与时间,另一方面,进程之间的通讯也比纯种麻烦,如利用多通道搭建电话会议等。那么剩下的线程方式是否符合我们的要求呢?答案是肯定的。在此我不想费口舌来解释线程的种种优点。
通过下图可以帮助我们更加清楚的了解Channel的接口及其所依赖的接口:
(图3.10 Channel及其依赖的类接口)
从图中我们可以清楚的看到与Channel直接相关的只有脚本引擎(IScriptEngine),通过脚本引擎即可将语音应用相关的业务逻辑与板卡驱动接口隔离开来,做到互不影响即有利于程序的移植,也可以在系统运行时(不停机的状态下)动态的插拨应用组件。那么系统又是如何实现的呢?
(图3.11 执行通道业务的业务逻辑代码)
在启动通道线程执行用户程序时依次将执行下列操作:(1)、检查通道是否配置了可用的脚本引擎,如果存在的话则直接调用引擎执行相应的脚本并将执行中遇到到错误或需要记录的资料通过AOutPuts参数传回来,传回的AOutPuts参数通过脚本完成事件使得用户程序有机会进行处理;(2)、如果没有配置有效的脚本引擎的话,系统将检查是否为其编写了业务逻辑处理事件(FOnRun)若存在的话则直接调用。以上两个都存在时系统将会抛出异常阻止线程的执行。通过以上两种途径就实现了业务逻辑的分离,是不是十分简单呢?
你更适合采用哪种方式来实现业务逻辑:
Ø 脚本执行引擎 通过脚本引擎可以将业务逻辑存放在单独的文件中,并可以随时根据业务的变化进行调整而不需要重新编译整个应用软件。部署起来也十分方便,你只需要将脚本放置在语音软件可以访问到的地方即可,比如放到应用服务器上语音应用则部署到一台工控机上;
Ø 业务处理事件 适合于较小型的语音应用开发,在业务逻辑变化不大时应用业务逻辑处理事件将会十分方便,也不需要单独部署脚本项目;在LightweightCTI的Demo中我也会演示如何将其分割到独立的文件中,以便你在不重新编译系统就可以更改企业业务流程;
你可以根据自己的实际情况来选择采用哪种方式。下面将通过一个实现IScriptEngine接口的例子详细介绍如何使用脚本来执行你的业务逻辑。首先,需要申明的一点是下面的这个例子并不是真正的脚本引擎,但同样也完成了我们需要做的工作,由此也可以看见LightweightCTI架构是多么的灵活。
TAbstractDemoScriptEngine类只简单的实现了IScriptEngine接口(图13)中的三个方法,并添加了必要的属性,详细的类定义见下图:
(图3.12 演示项目脚本引擎定义)
由于它并不是真正的脚本引擎,所以其对于LoadScripts及Debug方法只是进行了申明而没有实现代码。而隐藏了真正实现细节的RunScripts方法内部都做了些什么工作呢?让我们看看下面的代码片段。
(图3.13 RunScripts实现细节)
首先,将检查是否已经正确的初始化了通道对象,没有通道对象怎么进行工作呢?所以它是不可少的,否则的话系统抛出异常阻止线程调用脚本引擎继续执行;其次,系统写下日志记下脚本开始执行的时刻,这样以利于在程序调试时进行跟踪让开发人员或系统管理员了解脚本都做了些什么工作,在系统日志部分我们将会详细的说明它的好处;最后,执行用户在Run方法内定义的业务逻辑,可以看到在RunScripts方法内最关键的就是这一句,其它都是为其做准备。值得注意的是,我们是将Run方法包含于try…except块内的,这样能够在LightweightCTI捕捉业务逻辑中的异常。在具体的应用程序中用户只需继承TAbstractDemoScriptEngine并实现Run方法就可以了。
(图3.14 电话银行演示程序定义)
那是相当的简单,短短的4行代码就完成了其定义。接下来让我们看看实现业务逻辑的Run方法都做了哪些工作。下图所示的外呼演示实际上也只使用了几行代码。
(图3.15 外呼演示的Run方法)
在通道中不但可以使用PlayFile方法进行文件放音,还能够容易的挂接第三方TTS引擎实现TTS放音。在详细介绍之前请先回到图13,在图中通道并没有直接与ITTSEngine发生关系,而是通过ChannelManager来调用的。早在对LightweightCTI进行本次重构之前是将TTS引擎放在板卡适配器(CTICardDriver)类中定义的,这样使得Channel不但依赖于通道管理器也依赖于板卡适配器;另一方面,在重构后LightweightCTI具备支持多厂商多板卡的能力,而在实际的应用环境中每台语音虚拟机只需要用到一种TTS产品,所以将TTS引擎提到通道管理器中。在实际的应用中ITTSEngine也并不做实际的工作,它仅仅是具体TTS产品的一个代理或者说是Channel放音的适配器而矣。
TTS引擎的接口也十分简单,如下图所示:
(图3.16 TTS引擎接口)
TTS引擎中我们只定义了两个方法PlayMessage与PlayToFile,分别对应于文本放音和将文本转换为语音文件,通过它们将可以满足大部分的语音应用开发。下面让我们一窥通道是如何借助TTS引擎来放音的。
(图3.17 通道使用TTS放音)
通道首先检查TTS引擎是否正确的初始化,如果没有进行初始化或系统中没有配置TTS引擎系统将抛出一个异常,否则使用TTS引擎接口进行放音,实际的放音还是通过相应的TTS产品实现的,如微软、IBM等TTS产品。下面是通道借助TTS引擎(适配器)进行放音的时序图:
(图3.18 TTS放音时序图)
OK,关于通道的基础性功能介绍就到这里,这时你禁会问“哦,就这么点我怎样实现哪怕是最简单的催费任务呢?”下面我将对其进行详细的介绍,首先想阐明的一点是对于绑定服务功能最多的通道管理器(ChannelManger)我并不是十分满意,为什么呢?设计有点僵化,后续的工作中将按照前文所提出的体系结构重点对其进行重构和迭代。
暂且抛开它不表。