Feeds:
文章
评论

Archive for the ‘Cherub in Work’ Category

In this tutorial, we will work through how to create a spoken English assessment Flex application using AISpeech API and their ASSDK (ActionScript SDK).

Before you start

Ask AI Speech Ltd (mail to api@aispeech.com) for a developer account, application ID, secret key and a release version of AISpeech API ASSDK (AISpeechLib.swc).

Note that this ASSDK supports ActionScript 3.0 only. Version for ActionScript 2.0 is also available but just ask.

Headset or microphone is necessary for the demo application.

Target

We are building a spoken English assessment application. It is a Flex application. The application allows user to read a sentence in English. It then records user’s speech, and shows score of each word, respectively. The higher the score is, the better pronunciation the user has done.

(更多…)

Read Full Post »

在上一篇心得中,简述了AISpeech API ASSDK的分层和解耦设计。本篇谈封装和透明。

谈到封装,从设计角度讲,我觉得,首先要考虑的是“暴露”什么。ActionScript的代码中,需要暴露的包括对象的方法和属性,同时加上事件。

方法

原ASSDK设计的类似网络连接组件为FmsConnector。除了一个构造函数,总共设计8个public方法,7个getter或者setter以及5个实现IEventDispatcher接口需要的public方法。8个public方法列表如下:

init()

connect()

closeConnect()

callServer()

requestSpeechCore()

removeAllNetStreamEventListener()

reset()

disposeAll()

重构后,用于RTMP连接的RTMPConnection,除了一个构造函数,总共设计3个public方法,分别是

connect()

disconnect()

rpc()

几个重构时的考量:

原ASSDK的方法命名不好看。closeConnect不如disconnect。

原ASSDK中init()方法仅仅将实例的状态设为NOT_READY;这个方法被砍掉了。

原ASSDK中callServer()的实现和requestSpeechCore()的实现几乎一致,仅仅是NetConnection::call()方法的第一个参数不一样,这两个方法被统一为rpc()方法。closeConnect()、removeAllNetStreamEventListener()、reset()都或多或少用于断开连接,合并为disconnect()方法。

disposeAll()被弃用,ASSDK用于客户端,且其实现不涉及大量实例的复制,几乎不涉及内存占用/泄露的问题,Flash Player默认的垃圾回收机制够用了。

除此以外,通用的网络连接组件的实现,例如实例状态维护、连接超时维护等通用属性和方法,由一个ConnectionBase类来实现,该类还继承于EventDispatcher,把原ASSDK中那5个IEventDispatcher的方法也给省了。

少量且命名简短的public方法,让重构后的RTMPConnection的使用者看起来就舒服。此外,原ASSDK的FmsConnector类文件640行,重构后的RTMPConnection类文件400行。如果再刨除保留的注释掉的原实现的代码,有效代码加有效注释(ASDoc注释)200行多一些。

属性

最重要的需要暴露的属性是组件的状态和组件的参数;最应该避免暴露的是组件内所依赖的其他组件。就网络连接的HTTTP实现,仅通过其基类暴露了一个状态属性和一个配置参数属性。属性暴露,就ActionScript而言,一定要使用getter或者setter来确认是否只读属性。RTMP的相关实现还没有完全完成重构,但其暴露的NetStream、NetConnection的client、NetStream的client等等,最终都要避免。

事件

不记得在哪里看到过,说ActionScript所有的处理都是异步的。ActionScript中的事件的实现,相当完备可靠,但也相当的烦人。

ActionScript的NetStatus事件类,是我觉得最老太太裹脚布的一个实现,其info属性有65种值,涵盖了NetConnection、NetStream、NetGroup和SharedObject的各种各样事件类型。其中,连接服务器失败有三种可能:Failed、Rejected和InvalidApp。

原ASSDK在自定义组件(FmsConnector)和相关组件的设计上,就事件而言,有如下三个问题:

第一,对组件内系统定义的事件无选择的封装;

第二,对组件的事件,无选择的转发;

第三,自定义事件并不是组件相关的。

第三个问题好解释:原ASSDK定义了一个通用的AudioIOEvent,该Event定义了一长串事件名常量,用于ASSDK各个组件。这个AudioIOEvent足足定义了46中不同的事件。直接的问题是:如果我想确认FmsConnector类能派发多少种不同的事件,必须要在FmsConnector函数内用查找“dispatchEvent”字符串的方法加上手工统计。新ASSDK定义了NetEvent,涵盖了10中不同的和网络连接组件相关的事件。其中两种是正常事件(CONNECTED和RESPONSED),另外8中异常事件。说实话,10种事件,我嫌多。

第一个问题是这样的。经典NetStatus事件的处理,用switch case结构。原ASSDK实现中,几乎照着NetStatus事件类文档写了NetStatus各种info的处理。这里是纯的代码“看”着不爽的问题:很多处理都是直接log输出,然后就break了,如下:

switch (event.info.code)

{

    case "NetStream.Failed":
        _logger.debug("NetStream.Failed");
    break;           
    case "NetStream.Publish.BadName":
        _logger.debug("NetStream.Publish.BadName");
    break;

    case "NetStream.Play.StreamNotFound":
        _logger.debug("NetStream.Play.StreamNotFound");
    break;

}

重构后,改成

_logger.debug(event.info.code)

switch(event.info.code)

{

    仅case那些一定要处理的事件

{

不知道要节省多少行代码……

第二个问题,就代码和逻辑污染而言,是最严重的。在第一部分中介绍过,原ASSDK实现了各个组件,录音机组件使用FmsConnector,总的库函数(RecorderLib)使用录音机组件,提供给JS的接口封装使用了总的库函数。FmsConnector可以发出的十数种事件,先发给了录音机组件;录音机组件再集成了他自己支持的事件,一起发出,发给了总的库函数;总的库函数再累积其自己的和其他依赖组件的事件,一起发出来(几乎是涵盖了AudioIOEvent中涉及的60多种事件),发给提供给JS的接口封装。因此,在原ASSDK各个类文件中,到处可见长长的对NetStatus事件的处理函数……

重构中,从两个方面精简。首先,从分层和解耦的设计上分析B是否需要接受和处理A的时间。对录音机而言,其正常的工作(部分)依赖网络连接组件,但是,录音机应该仅关注网络连接组件是否可用,而不在乎其CONNECTED事件和其他网络连接异常的事件(连上了就是可用,出现连接异常,就是不可用了。)

此外,对事件进行分类。该分类是按照应用级需求做的。就应用而言,网络连接上了和网络连接出现异常,是两个重要的事件,都需要相应的在用户界面上有所响应,这两类事件不可少。各种不同的连接异常事件,可以将其原因以信息形式反馈。这样一来,网络连接组件是可能仅设计3个自定义事件的。

透明

该透明的透明;不该透明的一定不要透明;可以不透明的,就尽量不透明。

参数和数据一定要透明。就AISpeech API的使用而言,大部分是基于Web的应用,使用JSSDK。参数和数据从通过JSSDK、传到ASSDK、传到云端接入层进而转发到语音服务部分。如果中间层层设卡,各处都增加或多或少的数据格式转换、数据信息的增减,整个系统的输入和输出将越来越不可控。AISpeech API从设计伊始,就一直强调这个问题。

如上参数和透明的设计,还使得AISpeech API的各个SDK使用上的一致性很好。在最新的SDK设计中,允许由Web应用指定一个参数,直接对语音内核进行操作,各个中间环节成为真正的透明。

不该透明的是那些被封装的数据结构、内部实现逻辑、组件内的实例依赖关系、组件内的事件等等。上述封装部分其实讨论了要暴露哪些,此外,还有一个潜逻辑是:尽量少的暴露。其他的,都封装起来了,都是不透明的。

可以不透明的,很好的一个例子,是默认参数。我们做过逐行分析示例代码中各个参数是否可以通过默认方式省略。

另外一个可以不透明的例子,是组件的初始化。在重构后的代码,提供了一个“工厂”类,该类根据应用的实际情况,来选择并初始化各个组件。

Read Full Post »

GTest是Google的单元测试框架

这个题目涉及两个问题:一个是项目组织结构的规划,一个是编译/测试/发布的流程。

本文描述的内容尚未设计自动化持续集成相关配置。

示范项目结构

本项目包含两个库的源码,库源码都放在src目录下,相应的单元测试程序放在test目录下。

GTest完整的引入到项目中,跟随项目一起编译,从而保证项目整体移植时,随时可编译。

image

单元测试代码

TFoo.cpp中仅包含使用GTest的宏函数,不包含main主函数。TFoo.cpp在编译的时候,和GTest提供的gtest_main.cc一起编译。

TFoo.cpp的内容如下(假设Foo.h中声明了一个add_foo函数,执行加法)

#include "src/Foo.h"
#include "gtest/gtest.h"

TEST(TFoo, Test)
{
    ASSERT_EQ(6, add_foo(2, 4));
}

CMakeLists.txt

项目根目录级的CMakeLists.txt内容如下

cmake_minimum_required(VERSION 2.6)

add_subdirectory(gtest-1.6.0)

add_subdirectory(LibraryA)

add_subdirectory(LibraryB)

# ==== unittests ====

enable_testing()

add_test(NAME TFoo COMMAND TFoo)

add_test(NAME TBar COMMAND TBar)

LibraryA的CMakeLists.txt内容如下

cmake_minimum_required(VERSION 2.6)

project(LibraryA)

# ==== unittest TFoo ====

set(GTEST_ROOT ${PROJECT_SOURCE_DIR}/../gtest-1.6.0)

include_directories(${PROJECT_SOURCE_DIR})

include_directories(${GTEST_ROOT}/include)

ADD_EXECUTABLE(TFoo test/*.cpp ${GTEST_ROOT}/src/gtest_main.cc)
target_link_libraries(TFoo gtest gtest_main)

LibraryB的CMakeLists.txt内容和LibraryA的类似,新加了可执行单元测试TBar。

使用流程

执行源码外的cmake

cd Example

mkdir Build

cd Build

cmake ..

make可执行单元测试

make TFoo TBar

执行单元测试

make test

当然可以单独执行每一个编译好的单元测试,例如

cd Build/LibraryA

./TFoo

可以看到GTest的彩色输出。

Read Full Post »

从2011年10月份开始,零零碎碎2个月的时间,对AISpeech API的ASSDK(ActionScript SDK)进行重构。起先我是作为ASSDK的使用者来看ASSDK的设计的,后来逐步深入源码,忍不住就上手重构了。

重构是程序员的天性,我想这个问题可以单独列个调侃性的题目来写。这次重构,不完全是程序员之间互相看不惯对方代码的结果。简单说,确实有重构的必要。另外,新版本上线也折腾了一段时间了,公司负责技术文化建设的同事一直要我写些心得。一来是想详细说明这次重构是由必要的,二来还账(这也算一种技术债务吧),就准备了如下系列的题目:

AISpeech API ASSDK 重构若干心得

(一)分层和解耦

(二)封装和透明

(三)有风格和统一风格

(四)设计模式和UML

(五)单元测试

(六)第三方库

(七)再看SDK的易用性

本文从第一部分开始。

原ASSDK中分层和解耦的问题

原ASSDK的实现中,虽然有模块的设计,但实现上,各模块之间耦合太紧。我有一个比喻:当时的ASSDK好比一团胶泥,任何功能改动或者新功能添加,甚至是修改一个bug,都好比在胶泥上掏个洞或者再黏上一块。

ASSDK的核心功能是将用户的录音数据通过网络通道发送给服务器、请求计算并且获得结果。在基于RTMP协议的实现中,由于ActionScript库函数的设计,录音机设备、录音机功能和网络连接以及服务器响应四个功能被揉在了一起。

ActionScript文档中实现相关功能的实例代码总共几十行,除了准备NetConnection、NetStream、注册各类事件处理函数、建立服务器连接等,最经典的就是如下三行:

microphone = Microphone.getMicrophone();

netStream.attachAudio(microphone);

netStream.publish(fmsAppURL);

第一行说获得了麦克风实例;第二行说通过已经建立的网络连接通道发送麦克风实例采集的用户音频数据;第三行说开始录音吧。

整个ASSDK以及其所支持的应用的最核心的功能点,就在这3行,原ASSDK在实现的时候,从如上3行示例代码出发,没有对录音设备录音机功能控制网络连接处理服务器响应(处理服务器响应的功能在示范代码的网络事件处理函数中实现)这四部分从逻辑上进行解耦。

其次,我个人对这三行经典范例用于AISpeech API ASSDK有看法:从本位上讲,ASSDK的核心是一个录音机。然而上面代码直译成自然语言,其本位是网络连接 – 3行经典代码中的两行都是面向 NetStream这个对象实现的,连“开始录音”这样明确的对Recorder对象的操作,也被NetStream实现。对于ActionScript库函数这样的设计,我没意见(我猜Adobe是希望通过库函数的设计绑定用户使用其FMS)。但正是这样的设计,让ASSDK的录音机和网络连接一直紧紧的绑在一起。

接着上面展开,ASSDK公开给使用者的,是5个录音机控制接口,即初始化/开始录音/结束录音/开始回放/结束回放。原ASSDK分不开“录音机功能控制”和“网络连接”组件,就将这两者从功能示范性代码一直带到了发布的代码。

其三,当AISpeech API功能进行扩展的时候,问题一下子复杂了。需求很简答:录音机和API之间采用其他的网络协议通信,例如HTTP。由此简单的需求展开出一系列实现上的变更:

  • NetConnection和NetStream对不适用了。服务器端基于HTTP协议做了定制,ASSDK端要从Socket级别来实现连接;
  • 使用Flash Player的新功能SampleDataEvent来获得原始音频采样数据;
  • 录音回放可以播放存储于本地临时存储空间的用户录音数据;
  • ……

原ASSDK已经“实现”了上述的功能扩展,但很不幸,是以胶泥的方式。

在本系列第二部分“封装和透明”中,对原ASSDK中录音机、网络连接组件和处理服务器响应组件紧耦合的表现还另外有分析。

ASSDK 2.0版的分层和解耦设计

AISpeech API ASSDK包括四个核心组件:录音设备录音机功能控制网络连接处理服务器响应。这四部分在实现细节上,各自有各自不同的关注点,或者说责任区。

  1. 录音设备类,负责获取系统麦克风设备、属性设置、麦克风设备状态维护以及相关的操作(例如提示用户允许使用麦克风设备);
  2. 录音机功能控制类,负责开始/技术录音、开始/结束回放以及录音机状态维护;
  3. 网络连接类,负责建立网络连接、发送/接受数据、底层连接状态维护;
  4. 处理服务器响应类,负责对服务器的响应的解析和高层请求状态维护。

尝试用自然语言分层:

  • ASSDK的使用者操作录音机;
  • ASSDK的使用者获得语音计算(识别)结果。

由此导出,网络连接类和ASSDK的使用者没有半毛钱关系。

如下的各模块间的关系,也用自然语言来描述:

image

 

  • 录音机通过麦克风设备来访问硬件资源,采集用户的录音;
  • 录音机将用户的录音数据通过网络连接发送给AISpeech API服务;
  • 服务器响应处理模块通过网络和AISpeech API服务通信;

分层的设计,应该直接体现于包的设计。新版ASSDK明确实现了recorder、device、net和core四个包。前面提到的通过SampleDataEvent功能获得原始录音数据的类,很好的放在了com.aispeech.recorder包中。

这里简单说一下模块间的几个解耦原则:

明确依赖关系

模块间的依赖关系是微妙的,把依赖关系弄反了,可能在功能进一步扩展的时候时候,才发现抽象错了。我坚持用自然语言的方式来确定依赖关系:A使用/通过B完成某种功能,则A依赖B。

一旦依赖关系明确了,则被依赖组件所暴露的接口和数据应该尽量少。

面向接口设计

面向接口设计,这个没有什么疑义。这里说明一点:面向接口设计不能保证分层和模块化合理。依赖是设计顺序不能搞错了:先分层分模块且明确依赖关系,然后面向接口设计以解耦。

数据透明

ASSDK相对SDK的使用者,在其和API之间应该是透明;网络组件相对录音机和API之间,应该是透明的。合理的分层才能实现正确的数据透明。

状态聚类

组件可以有多种状态,以及多种状态间跳转的途径(事件)。例如,网络连接组件,可能发生申请连接被拒绝/申请连接超时/连接超时/连接被服务器断开等等导致“连不上服务器”,但究其根本,网络连接组件对于其使用者来说,暴露可用和不可用两个状态,以及在两个状态间切换的事件即可。不应该也不必要由网络组件的使用者(例如录音机组件)来维护网络组件的状态、事件处理等等。

 

总结

原ASSDK的终极问题在于“没有设计”。虽然从形式看,有各种包、有各种类、有各种接口和相应实现,但究其本质,没有层次、没有梳理依赖关系,从而没有合理的解耦,形成了一团胶泥。

原ASSDK的状况,有其特别的“历史原因”,即前文总结的,所有的实现都源自于ActionScript库函数及文档提供的一个原型实例。然而,在进一步工程实现的过程中,在新需求和新功能开发中,做的多,想的少,实现的多,设计的少,积少成多以致到了积重难返的地步。

Read Full Post »

b foo

我想在foo函数(类ClassA的成员函数)上设置一个断点,执行

b foo

但是GDB报告说

Function “foo” not defined.

Make breakpoint pending on future shared library load? (y or [n])

问题是,foo函数并不是在一个shared library中定义的,而是在一个静态库中定义的,在可执行文件(假设叫proc)编译的时候,已经链接好了。

问题在于执行b命令的时候,需要指定foo函数的全名,即

namespace::class_name::function_name

当我意识到这个问题的时候,也犯了错误:没有意识到foo的全名前还有一个namespace。(因为是在改别人的代码)。在不确定的时候,总结正确的流程如下:

1. 确保在编译proc的时候,加上了-g参数,用于调试。

2. 用如下命令列出proc所具有的全部的符号

nm proc

3. 在proc所具有的全部的符号中,搜索foo,得到

_ZN3NamespaceA6ClassA16fooEP6ClassB
_ZN3NamespaceA6ClassA16fooEb

由于C++支持重载,我的proc中有两个foo函数,一个参数是ClassB的一个指针,一个参数是boolean型。上述符号来自对编译时C++函数的签名法则,不同编译器生成的结果可能不同。

这时候,可以确定完整的foo函数签名应该是

NamespaceA::ClassA::foo

执行

b NamespaceA::ClassA::foo

GDB会同时在上述两个函数设置断点,并给出如下提示

Breakpoint 1 at 0x82223aa: file /home/userA/project/foo/foo.cpp, line 280.
Breakpoint 2 at 0x8222b96: file /home/userA/project/foo/foo.cpp, line 351.
warning: Multiple breakpoints were set.
Use the "delete" command to delete unwanted breakpoints.

 

Read Full Post »

aichinese-homepage

不是所有人都有这么好的创意,不是所有小朋友的老爸刚好在一个产品团队中。 Smile

不知道AIChinese下一个焦点图创意是什么?

Read Full Post »

aid201002是AIChinese产品的内部开发代号。aid201002项目组已经经历了4次迭代开发,迭代周期从2个月到6周不等。最初的两个迭代,是产品设计阶段。直到第二个迭代结束,才完成了AIChinese的第一次公开发布。

回顾每个迭代的末期,整个项目组,从管理、设计、开发到测试,全线都是紧张得一塌糊涂。即使如此,还是不得不做很多的妥协,之前是将发布时间延迟1-2周,现在则是把预计的功能点砍掉几个。

每次发布结束,“痛定思痛”,在制定新迭代计划和功能点的时候,思必驰的大领导和主管市场的头目,都会补充一句:避免项目前松后紧。

我看,前松后紧是避免不了的。

简单一个问题:距离项目结束还有一周的时间,眼看着工作是玩不成了,怎么办?十个项目有六个会这样:公司领导把项目组的负责人召集起来,狠挖灵魂深处,结论是:项目推迟2周完成;另外有三个项目会这样解决:不仅项目推迟2周完成,再增加人手。在项目最“紧张的”时候,对大多数决策人而言,可以有弹性的,只有进度(Time),最多再增加一项成本(Cost)。项目管理教科书上说:项目可以控制的元素包括进度Time、质量Quality、范围Scope、成本Cost、商业收益Business Benefit(还有一个什么来着?忘记了……)。其实,调整其他控制元素的弹性的情形不仅是可以接受,而且是经常遇到的。例如,眼看项目完不成了,新需求还在开发,完成的需求还到处冒小火花 — 测试组不断的报各种各样的Bug。这时候,可以接受的策略是,仅保证重要的需求,边边角角的小毛病就睁一只眼闭一只眼了。这个策略就是在放松质量的弹性;又例如,眼看项目完不成了,一些新需求就干脆不做了,这个策略是在放松范围的弹性。就产品项目而言,前述的各个控制元素都是可以有弹性的,动哪个,需要开发、商务和产品客户一起来决定,所谓征求项目的相关人(Stakeholders)的综合意见。话说回来,是不是增加了可以“动”的东西,进度的压力就小了啊?可惜不是。割肉从来不是件轻松的事情,不是有俗话说:手心手背都是肉嘛。

其次,“三岁看老”这样的事情仅是古话,六十年前连Internet还都没有出现,日新月异的变化如何可以预见?这里插一段闲话,月初刚刚买了一个佳能500D,我和我老婆都不懂,所以拣便宜货。Amazon上刚好有打折,老婆就动了心。不过她还是有脑子的:让我查查近期佳能会不会有新货发布,这样老型号就会进一步降价。查了下,别说,500D的下一代的下一代,传说中的600D,还真的要在这个月发布。长话短说,还是买了。这不前天(2月7日)佳能就宣布了发布600D,我给老婆看了下新闻。她很自信的说:2年后,我再买便宜的600D,然后把500D给云震,把卡片机给焯焯。当然,我很高兴她有这样的长远的设计,但这里面有很多的问题:2年后,为啥要买600D替换500D呢?600D也好,500D也好,都是入门级的单反,2年后,难道我们继续徘徊在入门级?2年后,单反还值得兴奋吗?2年后会不会什么3D HD照相机就已经普及了?2年后,我们的这个500D有机会留给云震吗?这几年,我们一个相机掉水里了,一个相机丢掉了,不买这个500D,我们也是只有一个卡片机,没备份啊。

如上,说是题外话,也不全是。我是说,在项目开始阶段,期望百无一失的需求,期望十全十美的产品设计,是天方夜谭。这说法是老生常谈了,基于此,敏捷的概念是这样的流行。敏捷就是讲究拥抱变化。思必驰的市场负责人对项目管理有一个提纲挈领的认识:项目管理就是控制客户期望和项目产出的匹配。项目产出不能低于用户的期望,也不要高于用户的期望,最理想的是一模一样。这和股价的预测结果是一样的:如果某公司的表现和预期一致,则对该公司的股价的预测往往是有效的。无论表现是高于预期还是低于预期,都会导致股价的预测的偏差。预测有偏差了,不论实际股价是高于预测值还是低于预测值,都会影响投资的效果。

一个典型的案例是,用户对产品的期望是逐渐逼近,逐渐明确的。实实在在的东西,往往会改变用户对产品的认识,这些个改变,直接影响到需求的变化。开发人员得到的需求是做一个桌子,做出来了,客户看到了,启发了在桌子下面增加一层隔板的设计,可以增加储物空间,新的需求就来了。这个时候,没有隔板的桌子已经不能满足客户对产品的期望,必须要做出来有隔板的,否则客户对现在这个桌子没有信心,他的兴奋点和营销策略已经完全转移到了有隔板的桌子了。

另外一个典型案例是,用户对越接近成品的产品,越有实质的意见和建议。新的意见,往往导致需求变更或者增加。还是以桌子为例。开发人员根据迭代原则,很快的给出了一个桌子的原型,四条腿,一个面,向客户征求意见。客户可能就是大概看了一眼,觉得是个桌子,好吧,继续吧。当开发人员把桌子大体都完成了,该雕刻的花纹雕刻了,该上的漆上了,再拿给客户看,客户对接近成品的桌子就会有很多很具体的意见了。类似增加个隔板的要求在这个时候出现,不奇怪。

还有一个典型案例是,决策者往往是很忙的,他没时间对产品原型做认真的研究,但对接近成品的产品,则有一种责任感驱动,进行认真的考量。决策者的重要的意见,不采纳不行,采纳了,就是工作量。

一点点人性论的东西:客户对产品的要求是无止境的。比较邪恶的说法是:不会让你项目组闲着没事干的。完成了A,能不能把B也做出来?反正有时间的。

另外一点点人性论的东西:人都是懒惰的,项目伊始,没人会说今天的事情按计划完成了,我多做一点,提前完成一点,以防今后某一天没完成计划,提前做出余量。没完成计划的余量最好提前计划好,别指望靠“前紧后松”来解决。

Read Full Post »

« Newer Posts - Older Posts »