嵌入式软件架构设计.doc
【精品文档】如有侵权,请联系网站删除,仅供学习与交流嵌入式软件架构设计.精品文档. 嵌入式系统软件架构设计目录1.前言42.决定架构的因素和架构的影响42.1.常见的误解52.1.1.小型的系统不需要架构52.1.2.敏捷开发不需要架构73.嵌入式环境下软件设计的特点73.1.和硬件密切相关73.2.稳定性要求高83.3.内存不足83.3.1.虚拟内存技术83.3.2.两段式构造93.3.3.内存分配器103.3.4.内存泄漏113.4.处理器能力有限,性能要求高113.4.1.抵御新技术的诱惑113.4.2.不要有太多的层次113.5.存储设备易损坏,速度较慢123.5.1.损耗均衡123.5.2.错误恢复123.6.故障成本高昂134.软件框架144.1.嵌入式软件架构面临的问题144.2.什么是框架144.2.1.软件复用的层次144.2.2.针对高度特定领域的抽象154.2.3.解除耦合和应对变化164.2.4.框架可以实现和规定非功能性需求164.3.一个框架设计的实例174.3.1.基本架构174.3.2.功能特点174.3.3.分析184.3.4.实际效果234.4.框架设计中的常用模式234.4.1.模板方法模式234.4.2.创建型模式234.4.3.消息订阅模式244.4.4.装饰器模式244.5.框架的缺点255.自动代码生成265.1.机器能做的事就不要让人来做265.2.举例265.2.1.消息的编码和解码265.2.2.GUI代码275.2.3.小结285.2.4.Google Protocol Buffer286.面向语言编程(LOP)306.1.从自动化代码生成更进一步306.2.优势和劣势326.3.在嵌入式系统中的应用327.测试337.1.可测试性是软件质量的一个度量指标337.2.测试驱动的软件架构347.3.系统测试347.3.1.界面自动化测试347.3.2.基于消息的自动化测试367.3.3.自动化测试框架367.3.4.回归测试387.4.集成测试387.5.单元测试387.5.1.圈复杂度测量417.5.2.扇入扇出测量427.5.3.框架对单元测试的意义428.维护架构的一致性429.一个实际嵌入式系统架构的演化439.1.数据处理449.2.窗口管理449.3.MVC模式459.4.大量类似模块,低效的复用469.5.远程控制469.6.自动化的TL1解释器479.7.测试的难题479.8.小结4710.总结481. 前言嵌入式是软件设计领域的一个分支,它自身的诸多特点决定了系统架构师的选择,同时它的一些问题又具有相当的通用性,可以推广到其他的领域。本课程试图从嵌入式软件架构设计出发,启发大家对软件架构设计的理解。本课程的很多内容是对谢老师课程在嵌入式领域的具体阐述。提起嵌入式软件设计,传统的印象是单片机,汇编,高度依赖硬件。传统的嵌入式软件开发者往往只关注实现功能本身,而忽视诸如代码复用,数据和界面分离,可测试性等因素。从而导致嵌入式软件的质量高度依赖开发者的水平,成败系之一身。随着嵌入式软硬件的飞速发展,今天的嵌入式系统在功能,规模和复杂度各方面都有了极大的提升。比如,Marvell公司的PXA3xx系列的最高主频已经达到800Mhz,内建USB,WIFI,2D图形加速,32位DDR内存。在硬件上,今天的嵌入式系统已经达到甚至超过了数年前的PC平台。在软件方面,完善的操作系统已经成熟,比如Symbian, Linux, WinCE。基于完善的操作系统,诸如字处理,图像,视频,音频,游戏,网页浏览等各种应用程序层出不穷,其功能性和复杂度比诸PC软件不遑多让。原来多选用专用硬件和专用系统的一些商业设备公司也开始转换思路,以出色而廉价的硬件和完善的操作系统为基础,用软件的方式代替以前使用专有硬件实现的功能,从而实现更低的成本和更高的可变更,可维护性。2. 决定架构的因素和架构的影响架构不是一个孤立的技术的产物,它受多方面因素的影响。同时,一个架构又对软件开发的诸多方面造成影响。软件架构软件规模生命周期非功能性需求非功能性需求团队组成开发模式下面举一个具体的例子。摩托车的发动机在出厂前必须通过一系列的测试。在流水线上,发动机被送到每个工位上,由工人进行诸如转速,噪音,振动等方面的测试。要求实现一个嵌入式设备,具备以下基本功能:1. 安装在工位上,工人上班前开启并登录。2. 通过传感器自动采集测试数据,并显示在屏幕上。3. 记录所有的测试结果,并提供统计功能。比如次品率。如果你是这个设备的架构师,哪些问题是在设计架构的时候应该关注的呢?2.1. 常见的误解2.1.1. 小型的系统不需要架构有相当多的嵌入式系统规模都较小,一般是为了某些特定的目的而设计的。受工程师认识,客户规模和项目进度的影响,经常不做任何架构设计,直接以实现功能为目标进行编码。这种行为表面上看满足了进度,成本,功能各方面的需求,但是从长远来看,在扩展和维护上付出的成本,要远远高于最初节约的成本。如果系统的最初开发者继续留在组织内并负责这个项目,那么可能一切都会正常,一旦他离开,后续者因为对系统细节的理解不足,就可能引入更多的错误。要注意,嵌入式系统的变更成本要远远高于一般的软件系统。好的软件架构,可以从宏观和微观的不同层次上描述系统,并将各个部分隔离,从而使新特性的添加和后续维护变得相对简单。举一个城铁刷卡机的例子,这个例子在前面的课程中出现过。简单的城铁刷卡机只需要实现如下功能:一个While循环足以实现这个系统,直接就可以开始编码调试。但是从一个架构师的角度,这里有没有值得抽象和剥离的部分呢?1. 计费系统。计费系统是必须抽象的,比如从单次计费到按里程计费。2. 传感器系统。传感器包括磁卡感应器,投币器等。设备可能更换。3. 故障处理和恢复。考虑到较高的可靠性和较短的故障恢复时间,这部分有必要单独设计。未来很可能出现的需求变更:1. 操作界面。是否需要抽象出专门的Model来?以备将来实现View。2. 数据统计。是否需要引入关系型数据库?如果直接以上面的流程图编码,当出现变更后,有多少代码可以复用?不过,也不要因此产生过度的设计。架构应当立足满足当前需求,并适当的考虑重用和变更。2.1.2. 敏捷开发不需要架构极限编程,敏捷开发的出现使一些人误以为软件开发无需再做架构了。这是一个很大的误解。敏捷开发是在传统瀑布式开发流程出现明显弊端后提出的解决方案,所以它必然有一个更高的起点和对开发更严格的要求。而不是倒退到石器时代。事实上,架构是敏捷开发的一部分,只不过在形式上,敏捷开发推荐使用更高效,简单的方式来做设计。比如画在白板上然后用数码相机拍下的UML图;用用户故事代替用户用例等。测试驱动的敏捷开发更是强迫工程师在写实际代码前设计好组件的功能和接口,而不是直接开始写代码。敏捷开发的一些特征:1. 针对比传统开发流程更大的系统2. 承认变化,迭代架构3. 简洁而不混乱4. 强调测试和重构3. 嵌入式环境下软件设计的特点要谈嵌入式的软件架构,首先必须了解嵌入式软件设计的特点。3.1. 和硬件密切相关嵌入式软件普遍对硬件有着相当的依赖性。这体现在几个方面:1. 一些功能只能通过硬件实现,软件操作硬件,驱动硬件。2. 硬件的差异/变更会对软件产生重大影响。3. 没有硬件或者硬件不完善时,软件无法运行或无法完整运行。这些特点导致几方面的后果:1. 软件工程师对硬件的理解和熟练程度会很大程度的决定软件的性能/稳定性等非功能性指标,而这部分一向是相对复杂的,需要资深的工程师才能保证质量。2. 软件对硬件设计高度依赖,不能保持相对稳定,可维护性和可重用性差3. 软件不能离开硬件单独测试和验证,往往需要和硬件验证同步进行,造成进度前松后紧,错误定位范围扩大。针对这些问题,有几方面的解决思路:1. 用软件实现硬件功能。选用更强大的处理器,用软件来实现部分硬件功能,不仅可以降低对硬件的依赖,在响应变化,避免对特定型号和厂商的依赖方面都很有好处。这在一些行业里已经成为了趋势。在PC平台也经历了这样的过程,比如早期的汉卡。2. 将对硬件的依赖独立成硬件抽象层,尽可能使软件的其他部分硬件无关,并可以脱离硬件运行。一方面将硬件变更甚至换件的风险控制在有限的范围内,另一方面提高软件部分的可测试性。3.2. 稳定性要求高大部分嵌入式软件都对程序的长期稳定运行有较高的要求。比如手机经常几个月开机,通讯设备则要求24*7正常运行,即使是通讯上的测试设备也要求至少正常运行8小时。为了稳定性的目标,有一些比较常用的设计手段:1. 将不同的任务分布在独立的进程中。良好的模块化设计是关键2. Watch Dog, Heart beat,重新启动失效的进程。3. 完善而统一的日志系统以快速定位问题。嵌入式设备一般缺乏有力的调试器,日志系统尤其重要。4. 将错误孤立在最小的范围内,避免错误的扩散和连锁反应。核心代码要经过充分的验证,对非核心代码,可以在监控或者沙盒中运行,避免其破坏整个系统。举例,Symbian上的GPRS访问受不同硬件和操作系统版本影响,功能不是非常稳定。其中有一个版本上当关闭GPRS连接时一定会崩溃,而且属于known issue。将GPRS连接,HTTP协议处理,文件下载等操作独立到一个进程中,虽然每次操作完毕该进程都会崩溃,对用户却没有影响。5. 双备份这样的手段较少采用3.3. 内存不足虽然当今的嵌入式系统的内存比之以K计数的时代已经有了很大的提高,但是随着软件规模的增长,内存不足的问题依然时时困扰着系统架构师。有一些原则,架构师在进行设计决策的时候可以参考:3.3.1. 虚拟内存技术有一些嵌入式设备需要处理巨大的数据量,而这些数据不可能全部装入内存中。一些嵌入式操作系统不提供虚拟内存技术,比如WinCE4.2每个程序最多只能使用32M内存。对这样的应用,架构师应该特别设计自己的虚拟内存技术。所谓的虚拟内存技术的核心是,将暂时不太可能使用的数据移出内存。这涉及到一些技术点:1. 引用计数,正在使用的数据不能移出。2. 使用预测,预测下一个阶段某个数据的使用可能性。基于预测移出数据或者提前装入数据。3. 占位数据/对象。4. 高速缓存。在复杂数据结果下缓存高频率使用的数据,直接访问。5. 快速的持久化和装载。下图是一个全国电信机房管理系统的界面示意图:每个节点下都有大量的数据需要装载,可以使用上述技术将内存占用降到最低。3.3.2. 两段式构造在内存有限的系统里,对象构造失败是必须要处理的问题,失败的原因中最常见的则是内存不足(实际上这也是对PC平台的要求,但是在实际中往往忽略,因为内存实在便宜)。两段式构造就是一种常用而有效的设计。举例来说:CMySimpleClass:class CMySimpleClass public: CMySimpleClass(); CMySimpleClass(); private: int SomeData;CMyCompoundClass:class CMyCompoundClass public: CMyCompoundClass(); CMyCompoundClass(); private: CMySimpleClass* iSimpleClass; 在CMyCompoundClass的构造函数里初始化iSimpleClass对象。CMyCompoundClass:CMyCompoundClass() iSimpleClass = new CMySimpleClass; 当创建CMyCompoundClass的时候会发生什么呢?CMyCompoundClass* myCompoundClass = new CMyCompoundClass;1. 为CMyCompoundClass的对象分配内存2. 调用CMyCompoundClass对象的构造函数3. 在构造函数中创建一个CMySimpleClass的实例4. 构造函数结束返回一切看起来都很简单,但是如果第三步创建CMySimpleClass对象的时候发生内存不足的错误怎么办呢?构造函数无法返回任何错误信息以提示调用者构造没有成功。调用者于是获得了一个指向CMyCompoundClass的指针,但是这个对象并没有构造完整。如果在构造函数中抛出异常会怎么样呢?这是个著名的噩梦,因为析构函数不会被调用,在创建CMySimpleClass对象之前如果分配了资源就会泄露。关于在构造函数中抛出异常可以单讲一个小时,但是有一个建议是:尽量避免在构造函数中抛出异常。所以,使用两段式构造法是一个更好的选择。简单的说,就是在构造函数避免任何可能产生错误的动作,比如分配内存,而把这些动作放在构造完成之后,调用另一个函数。比如:AddressBook* book = new AddressBook()If(!book->Construct()delete book;book = NULL;这样可以保证当Construct不成功的时候释放已经分配的资源。在最重要的手机操作系统Symbian上,二段式构造法普遍使用。3.3.3. 内存分配器不同的系统有着不同的内存分配的特点。有些要求分配很多小内存,有的则需要经常增长已经分配的内存。一个好的内存分配器对嵌入式的软件的性能有时具有重大的意义。应该在系统设计时保证整个系统使用统一的内存分配器,并且可以随时更换。3.3.4. 内存泄漏内存泄漏对嵌入式系统有限的内存是非常严重的。通过使用自己的内存分配器,可以很容易的跟踪内存的分配释放情况,从而检测出内存泄漏的情况。3.4. 处理器能力有限,性能要求高这里不讨论实时系统,那是一块很大的专业话题。对一般的嵌入式系统而言,由于处理器能力有限,要特别注意性能的问题。一些很好的架构设计由于不能满足性能要求,最终导致整个项目的失败。3.4.1. 抵御新技术的诱惑架构师必须明白,新技术常常意味着复杂和更低的性能。即使这不是绝对的,由于嵌入式系统硬件性能所限,弹性较低。一旦发现新技术有和当初设想不同之处,就更难通过修改来适应。比如GWT技术。这是Google推出的Ajax开发工具,它可以让程序员像开发一个桌面应用程序一样开发Web的Ajax程序。这使得在嵌入式系统上用一套代码实现远程和本地操作界面成为了很容易的一件事。但是在嵌入式设备上运行B-S结构的应用,性能上是一个很大的挑战。同时,浏览器兼容方面的问题也很严重,GWT目前的版本还不够完善。事实证明,嵌入式的远程控制方案还是要采用Activex,VNC或者其他的方案。3.4.2. 不要有太多的层次分层结构有利于清晰的划分系统职责,实现系统的解耦,但是每多一个层次,就意味着性能的一次损失。尤其是当层和层之间需要传递大量数据的时候。对嵌入式系统而言,在采用分层结构时要控制层次数量,并且尽量不要传递大量数据,尤其是在不同进程的层次之间。如果一定要传递数据,要避免大量的数据格式转换,如XML到二进制,C+结构到Python结构。嵌入式系统能力有限,一定要将有限的能力用在系统的核心功能上。3.5. 存储设备易损坏,速度较慢受体积和成本的限制,大部分的嵌入式设备使用诸如Compact Flash, SD, mini SD, MMC等作为存储设备。这些设备虽然有着不担心机械运动损坏的优点,但是其本身的使用寿命都比较短暂。比如,CF卡一般只能写100万次。而SD更短,只有10万次。对于像数码相机这样的应用,也许是足够的。但是对于需要频繁擦写磁盘的应用,比如历史数据库,磁盘的损坏问题会很快显现。比如有一个应用式每天向CF卡上写一个16M的文件,文件系统是FAT16, 每簇大小是2K,那么写完这个16M的文件,分区表需要写8192次,于是一个100万次寿命的CF实际能够工作的时间是1000000/8192 = 122天。而损坏的时候,CF卡的其他绝大部分地方的使用次数不过万分之一。除了因为静态的文件分区表等区块被频繁的读写而提前损坏,一些嵌入式设备还要面对直接断电的挑战,这会在存储设备上产生不完整的数据。3.5.1. 损耗均衡损耗均衡的基本思路是平均地使用存储器上的各个区块。需要维护一张存储器区块使用情况的表,这个表包括区块的偏移位置,当前是否可用,以及已经擦写地次数。当有新的擦写请求的时候,根据以下原则选择区块:1. 尽量连续2. 擦写次数最少即使是更新已经存在的数据,也会使用以上原则分配新的区块。同样,这张表的存放位置也不能是固定不变的,否则这张表所占据的区块就会最先损坏。当要更新这张表的时候,同样要使用以上算法分配区块。如果存储器上有大量的静态数据,那么上述算法就只能针对剩下的空间生效,这种情况下还要实现对这些静态数据的搬运的算法。但是这种算法会降低写操作的性能,也增加了算法的复杂度。一般都只使用动态均衡算法。目前比较成熟的损耗均衡的文件系统有JFFS2, 和 YAFFS。也有另一种思路就是在FAT16等传统文件系统上实现损耗均衡,只要事先分配一块足够大的文件,在文件内部实现损耗均衡算法。不过必须修改FAT16的代码,关闭对最后修改时间的更新。现在的CF卡和SD卡有的已经在内部实现了损耗均衡,这种情况下就不需要软件实现了。3.5.2. 错误恢复如果在向存储器写数据的时候发生断电或者被拔出,那么所写的区域的数据就处于未知的状态。在一些应用中,这会导致不完整的文件,而在另一些应用中,则会导致系统失败。所以对这类错误的恢复也是嵌入式软件设计必须考虑的。常用的思路有两种:1. 日志型的文件系统这种文件系统并不是直接存储数据,而是一条条的日志,所以当发生断电的时候,总可以恢复到之前的状态。这类文件系统的代表如ext3。2. 双备份双备份的思路更简单,所有的数据都写两份。每次交替使用。文件分区表也必须是双备份的。假设有数据块A,A1是他的备份块,在初始时刻和A的内容是一致的。在分区表中,F指向数据块A,F1是他的备份块。当修改文件时,首先修改数据块A1的内容,如果此时断电,A1的内容错误,但因为F指向的是完好的A,所以数据没有损坏。如果A1修改成功,则修改F1的内容,如果此时断电,因为F是完好的,所以依然没有问题。现在的Flash设备,有的已经内置错误检测和错误校正技术,可以保证在断电时数据的完整。还有的包括自动的动态/静态损耗均衡算法和坏块处理,完全无须上层软件额外对待,可以当作硬盘使用。所以,硬件越发达,软件就会越可靠,技术不断的进步,将让我们可以把更多的精力投入到软件功能的本身,这是发展的趋势。3.6. 故障成本高昂嵌入式产品都是软硬件一起销售的给用户的,所以这带来了一个纯软件所不具备的问题,那就是当产品发生故障时,如果需要返厂才能修复,则成本就很高。嵌入式设备常见有以下的几类故障:a) 数据故障。由于某些原因导致数据不能读出或者不一致。比如断电引起的数据库错误。b) 软件故障。软件本身的缺陷,需要通过发布补丁程序或者新版本的软件修正。c) 系统故障。比如用户下载了错误的系统内核,导致系统无法启动。d) 硬件故障。这种故障只有返厂,不属于我们的讨论范围。针对前三类故障,要尽可能保证客户自己,或者现场技术人员就可以解决。从架构的角度考虑,如下原则可以参考:a) 使用具备错误恢复能力的数据管理设计。当数据发生错误时,用户可以接受的处理依次是:i. 错误被纠正,所有数据有效ii. 错误发生时的数据(可能不完整)丢失,之前的数据有效。iii. 所有数据丢失iv. 数据引擎崩溃无法继续工作一般而言,满足第二个条件即可。(日志,事务,备份,错误识别)b) 将应用程序和系统分离。应用程序应该放置在可插拔的Flash卡上,可以通过读卡器进行文件复制升级。非必要的情况不要使用专用应用软件来升级应用程序。c) 要有“安全模式”。即当主系统被损坏后,设备依然可以启动,重新升级系统。常见的uboot可以保证这一点,在系统损坏后,可以进入uboot通过tftp重新升级。4. 软件框架在桌面系统和网络系统上,框架是普遍应用的,比如著名的ACE, MFC, Ruby On Rails等。而在嵌入式系统中,框架则是很少使用的。究其原因,大概是认为嵌入式系统简单,没有重复性,过于注重功能的实现和性能的优化。在前言中我们已经提到,现在的嵌入式发展趋势是向着复杂化,大型化,系列化发展的。所以,在嵌入式下设计软件框架也是很有必要,也很有价值的。4.1. 嵌入式软件架构面临的问题前面我们讲到,嵌入式系统软件架构所面临的一些问题,其中很重要的一点是,对硬件的依赖和硬件相关软件的复杂性。还包括嵌入式软件在稳定性和内存占用等方面的苛刻要求。如果团队中的每个人都是这些方面高手的话,也许有可能开发出高质量的软件,但事实是一个团队中可能只有一两个资深人员,其他大部分都是初级工程师。人人都去和硬件打交道,都负责稳定性,性能等等指标的话,是很难保证最终产品质量的。如果组件团队时都是精通硬件等底层技术的人才,又很难设计出在可用性,扩展性等方面出色的软件。术业有专攻,架构师的选择决定着团队的组成方式。同时,嵌入式软件开发虽然复杂,但是也存在大量的重用的可能性。如何重用,又如何应对将来的变更?所以,如何将复杂性对大多数人屏蔽,如何将关注点分离,如何保证系统的关键非功能指标,是嵌入式软件架构设计师应该解决的问题。一种可能的解决方案就是软件框架。4.2. 什么是框架框架是在一个给定的问题领域内,为了重用和应对未来需求变化而设计的软件半成品。框架强调对特定领域的抽象,包含大量的专业领域知识,以缩短软件的开发周期,提高软件质量为目的。使用框架的二次开发者通过重写子类或组装对象的方式来实现特殊的功能。4.2.1. 软件复用的层次复用是在我们经常谈到的话题,“不要重复发明轮子”也是耳熟能详的戒条。不过对于复用的理解实际上是有很多个层次的。最基础的复用是复制粘贴。某个功能以前曾经实现过,再次需要的时候就复制过来,修改一下就可以使用。经验丰富的程序员一般都会有自己的程序库,这样他们实现的时候就会比新的程序员快。复制粘贴的缺点是代码没有经过抽象,往往并不完全的适用,所以需要进行修改,经过多次复用后,代码将会变得混乱,难以理解。很多公司的产品都有这个问题,一个产品的代码从另一个产品复制而来,修改一下就用,有时候甚至类名变量名都不改。按照“只有为复用设计的代码才能真正复用”的标准,这称不上是复用,或者说是低水平的复用。更高级的复用是则是库。这种功能需要对经常使用的功能进行抽象,提取出其中恒定不变的部分,以库的形式提供给二次开发程序员使用。因为设计库的时候不知道二次开发者会如何使用,所以对设计者有着很高的要求。这是使用最广泛的一种复用,比如标准C库,STL库。现在非常流行的Python语言的重要优势之一就是其库支持非常广泛,相反C+一直缺少一个强大统一的库支持,成为短板。在公司内部的开发中总结常用功能并开发成库是很有价值的,缺点是对库的升级会影响到很多的产品,必须慎之又慎。框架是另一种复用。和库一样,框架也是对系统中不变的部分进行抽象并加以实现,由二次开发者实现其他变化的部分。典型的框架和库的最大的区别是,库是静态的,由二次开发者调用的;框架是活着的,它是主控者,二次开发者的代码必须符合框架的设计,由框架决定在何时调用。 举个例子,一个网络应用总是要涉及到连接的建立,数据收发和连接的关闭。以库的形式提供是这样的:conn = connect(host,port);if(conn.isvalid()data = conn.recv();printf(data);conn.close();框架则是这样的:class mycomm:class connectpublic:host();port();onconnected(); ondataarrived(unsigned char* data, int len); onclose();框架会在“适当”的时机创建mycomm对象,并查询host和port,然后建立连接。在连接建立后,调用onconnected()接口,给二次开发者提供进行处理的机会。当数据到达的时候调用ondataarrived接口让二次开发者处理。这是好莱坞原则,“不要来找我们,我们会去找你”。当然,一个完整的框架通常也要提供各种库供二次开发者使用。比如MFC提供了很多的库,如CString, 但本质上它是一个框架。比如实现一个对话框的OnInitDialog接口,就是由框架规定的。4.2.2. 针对高度特定领域的抽象和库比较起来,框架是更针对特定领域的抽象。库,比如C库,是面向所有的应用的。而框架相对来说则要狭窄的多。比如MFC提供的框架只适合于Windows平台的桌面应用程序开发,ACE则是针对网络应用开发的框架,Ruby On Rails是为快速开发web站点设计的。越是针对特定的领域,抽象就可以做的越强,二次开发就可以越简单,因为共性的东西越多。比如我们上面谈到嵌入式系统软件开发的诸多特点,这就是特定领域的共性,就属于可以抽象的部分。具体到实际的嵌入式应用,又会有更多的共性可以抽象。框架的设计目的是总结特定领域的共性,以框架的方式实现,并规定二次开发者的实现方式,从而简化开发。相应的,针对一个领域开发的框架就不能服务于另一个领域。对企业而言,框架是一种极好的积累知识,降低成本的技术手段。4.2.3. 解除耦合和应对变化框架设计的一个重要目的就是应对变化。应对变化的本质就是解耦。从架构师的角度看,解耦可以分为三种:1. 逻辑解耦。逻辑解耦是将逻辑上不同的模块抽象并分离处理。如数据和界面的解耦。这也是我们最常做的解耦。2. 知识解耦。知识解耦是通过设计让掌握不同知识的人仅仅通过接口工作。典型的如测试工程师所掌握的专业知识和开发工程师所掌握的程序设计和实现的知识。传统的测试脚本通常是将这二者合二为一的。所以要求测试工程师同时具备编程的能力。通过适当的方式,可以让测试工程师以最简单的方式实现他的测试用例,而开发人员编写传统的程序代码来执行这些用例。3. 变与不变的解耦。这是框架的重要特征。框架通过对领域知识的分析,将共性,也就是不变的内容固定下来,而将可能发生变化的部分交给二次开发者实现。4.2.4. 框架可以实现和规定非功能性需求非功能性需求是指如性能,可靠性,可测试性,可移植性等。这些特性可以通过框架来实现。以下我们一一举例。性能。对性能的优化最忌讳的就是普遍优化。系统的性能往往取决于一些特定的点。比如在嵌入式系统中,对存储设备的访问是比较慢的。如果开发者不注意这方面的问题,频繁的读写存储设备,就会造成性能下降。如果对存储设备的读写由框架设计,二次开发者只作为数据的提供和处理者,那么就可以在框架中对读写的频率进行调节,从而达到优化性能的目的。由于框架都是单独开发的,完成后供广泛使用,所以就有条件对关键的性能点进行充分的优化。可靠性。以上面的网络通讯程序为例,由于框架负责了连接的创建和管理,也处理了各种可能的网络错误,具体的实现者无须了解这方面的知识,也无须实现这方面错误处理的代码,就可以保证整个系统在网络通讯方面的可靠性。以框架的方式设计在可靠性方面的最大优势就是:二次开发的代码是在框架的掌控之内运行的。一方面框架可以将容易出错的部分实现,另一方面对二次开发的代码产生的错误也可以捕获和处理。而库则不能代替使用者处理错误。可测试性。可测试性是软件架构需要考虑的一个重要方面。下面的章节会讲到,软件的可测试性是由优良的设计来保证的。一方面,由于框架规定了二次开发的接口,所以可以迫使二次开发者开发出便于进行单元测试的代码。另一方面,框架也可以在系统测试的层面上提供易于实现自动化测试和回归测试的设计,例如统一提供的TL1接口。可移植性。如果软件的可移植性是软件设计的目标,框架设计者可以在设计阶段来保证这一点。一种方式是通过跨平台的库来屏蔽系统差异,另一种可能的方式更加极端,基于框架的二次开发可以是脚本化的。组态软件是这方面的一个例子,在PC上组态的工程,也可以在嵌入式设备上运行。4.3. 一个框架设计的实例4.3.1. 基本架构软件 WinCE,ARM芯片,触摸屏,图形化界面, Lua脚本界面硬件 各种具体硬件,如ADSL测试芯片,VOIP测试芯片,可插拔替换固件 pSOS实时系统, MIPS芯片USB 2.0对外提供XML,TL1接口4.3.2. 功能特点上面是一个产品系列的架构图,其特点是硬件部分是模块化的,可以随时插拔。不同的硬件应用于不同的通讯测试场合。比如光通讯测试,xDSL测试,Cable Modem测试等等。针对不同的硬件,需要开发不同的固件和软件。固件层的功能主要是通过USB接口接收来自软件的指令,并读写相应的硬件接口,再进行一些计算后,将结果返回给软件。软件运行在WinCE平台,除了提供一个触摸式的图形化界面外,还对外提供基于XML(SOAP)接口和TL1接口。为了实现自动化测试,还提供了基于Lua的脚本语言接口。整个产品系列有几十个不同的硬件模块,相应的需要开发几十套软件。这些软件虽然服务于不同的硬件,但是彼此之间有着高度的相似性。所以,选择先开发一个框架,再基于框架开发具体的模块软件成了最优的选择。4.3.3. 分析软件部分的结构分析如下:协议层图形界面TL1 Server内存数据库通讯层SOAP ServerTerm/scriptSOAP Client硬件通讯层任务1任务2任务任务n硬件抽象层(FPGA/IOBoard.)派发器协议层视图系统分为软件,固件和硬件三大块。软件和固件运行在两块独立的板子上,有各自的处理器和操作系统。硬件则插在固件所在的板子上,是可以替换的。软件和固件其实都是软件,下面我们分别分析。软件软件的主要工作是提供各种用户界面。包括本地图形化界面,SOAP访问界面,TL1访问界面。整个软件部分分为五大部分:通讯层协议层图形界面SOAP服务器TL1服务器通讯层要屏蔽用户对具体通信介质和协议的了解,无论是USB还是socket,对上层都不产生影响。通讯层负责提供可靠的通讯服务和适当的错误处理。通过配置文件,用户可以改变所使用的通讯层。协议层的目的是将数据进行编码和解码。编码的产生物是可以在通讯层发送的流,按照嵌入式软件的特点,我们选择二进制作为流的格式。解码的产生物是多种的,既有供界面使用的C Struct,也可以是XML数据,还可以是Lua的数据结构(tablegt)。如果需要,还可以产生JSON,TL1,Python数据,TCL数据等等。这一层在框架中是通过机器自动生成的,我们后面会讲到。内存数据库,SOAP Server和TL1 Server都是协议层的用户。图形界面通过读写内存数据库和底层通讯。图形界面是框架设计的重点之一,原因是这里工作量最大,重复而无聊的工作最多。让我们分析一下在图形界面开发工作中最主要的事情是什么。1. 收集用户输入的数据和命令2. 将数据和命令发给底层3. 接收底层反馈4. 将数据显示在界面上同时有一些库用来进一步简化开发:这是一个简化的例子,但是很好的说明了框架的特点:1. 客户代码必须按照规定的接口实现2. 框架在适当的时候调用客户实现的接口3. 每个接口都被设计为只完成特定的单一功能4. 将各个步骤有机的串起来是框架的事,二次开发者不知道,也无须知道。5. 通常都要有附带的库。固件固件的主要工作是接受来自软件的命令,驱动硬件工作;获取硬件的状态,进行一定的计算后返回给软件。早期的固件是很薄的一层,因为绝大部分工作是由硬件完成的,固件只起到一个中转通讯的作用。随着时代发展,现在的固件开始承担越来越多原来由硬件完成的工作。整个固件部分分为五大部分:硬件抽象层,提供对硬件的访问接口互相独立的任务群任务/消息派发器协议层通讯层针对不同的设备,工作量集中在硬件抽象层和任务群上。硬件抽象层是以库的形式提供的,由对硬件最熟悉,经验最丰富的工程师来实现。任务群则由一系列的任务组成,他们分别代表不同的业务应用。比如测量误码率。这部分由相对经验较少的工程师来实现,他们的主要工作是实现规定的接口,按照标准化文档定义的方式实现算法。任务定义了如下接口,由具体开发者来实现:OnInit();OnRegisterMessage();OnMessageArrive();Run();OnResultReport();框架的代码流程如下:(伪代码)CTask* task = new CBertTask();task->OnInit();task->OnRegisterMessage();while(TRUE)task->OnMessageArrive();task->Run();task->OnResultReport();delete task;task = NULL;这样,具体任务的实现者所关注的最重要的事情就是实现这几个接口。其他如硬件的初始化,消息的收发,编码解码,结果的上报等等事情都由框架进行了处理。 避免了每个工程师都必须处理从上到下的所有方面。并且这样的任务代码还有很高的重用性,比如是在以太网上还是在Cable Modem上实现PING的算法都是一样的。4.3.4. 实际效果在实际项目中,框架大大降低了开发难度。对软件部分尤其明显,由实习生即可完成高质量的界面开发,开发周期缩短50%以上。产品质量大大提升。对固件部分的贡献在于降低了对精通底层硬件的工程师的需要,一般的工程师熟知测量算法即可。同时,框架的存在保证了性能,稳定和可测试性等要素。4.4. 框架设计中的常用模式4.4.1. 模板方法模式模板方法模式是框架中最常用的设计模式。其根本的思路是将算法由框架固定,而将算法中具体的操作交给二次开发者实现。例如一个设备初始化的逻辑,框架代码如下:TBool CBaseDevice:Init()if ( DownloadFPGA() != KErrN