在桌面系统和网络系统上,框架是普遍应用的,比如著名的ACE, MFC, Ruby On Rails等。而在嵌入式系统中,框架则是很少使用的。究其原因,大概是认为嵌入式系统简单,没有重复性,过于注重功能的实现和性能的优化。在前言中我们已经提到,现在的嵌入式发展趋势是向着复杂化,大型化,系列化发展的。所以,在嵌入式下设计软件框架也是很有必要,也很有价值的。
3.1. 嵌入式软件架构面临的问题前面我们讲到,嵌入式系统软件架构所面临的一些问题,其中很重要的一点是,对硬件的依赖和硬件相关软件的复杂性。还包括嵌入式软件在稳定性和内存占用等方面的苛刻要求。如果团队中的每个人都是这些方面高手的话,也许有可能开发出高质量的软件,但事实是一个团队中可能只有一两个资深人员,其他大部分都是初级工程师。人人都去和硬件打交道,都负责稳定性,性能等等指标的话,是很难保证最终产品质量的。如果组件团队时都是精通硬件等底层技术的人才,又很难设计出在可用性,扩展性等方面出色的软件。术业有专攻,架构师的选择决定着团队的组成方式。
同时,嵌入式软件开发虽然复杂,但是也存在大量的重用的可能性。如何重用,又如何应对将来的变更?
所以,如何将复杂性对大多数人屏蔽,如何将关注点分离,如何保证系统的关键非功能指标,是嵌入式软件架构设计师应该解决的问题。一种可能的解决方案就是软件框架。
3.2. 什么是框架框架是在一个给定的问题领域内,为了重用和应对未来需求变化而设计的软件半成品。框架强调对特定领域的抽象,包含大量的专业领域知识,以缩短软件的开发周期,提高软件质量为目的。使用框架的二次开发者通过重写子类或组装对象的方式来实现特殊的功能。
3.2.1. 软件复用的层次复用是在我们经常谈到的话题,“不要重复发明轮子”也是耳熟能详的戒条。不过对于复用的理解实际上是有很多个层次的。
最基础的复用是复制粘贴。某个功能以前曾经实现过,再次需要的时候就复制过来,修改一下就可以使用。经验丰富的程序员一般都会有自己的程序库,这样他们实现的时候就会比新的程序员快。复制粘贴的缺点是代码没有经过抽象,往往并不完全的适用,所以需要进行修改,经过多次复用后,代码将会变得混乱,难以理解。很多公司的产品都有这个问题,一个产品的代码从另一个产品复制而来,修改一下就用,有时候甚至类名变量名都不改。按照“只有为复用设计的代码才能真正复用”的标准,这称不上是复用,或者说是低水平的复用。
更高级的复用是则是库。这种功能需要对经常使用的功能进行抽象,提取出其中恒定不变的部分,以库的形式提供给二次开发程序员使用。因为设计库的时候不知道二次开发者会如何使用,所以对设计者有着很高的要求。这是使用最广泛的一种复用,比如标准C库,STL库。现在非常流行的Python语言的重要优势之一就是其库支持非常广泛,相反C++一直缺少一个强大统一的库支持,成为短板。在公司内部的开发中总结常用功能并开发成库是很有价值的,缺点是对库的升级会影响到很多的产品,必须慎之又慎。
框架是另一种复用。和库一样,框架也是对系统中不变的部分进行抽象并加以实现,由二次开发者实现其他变化的部分。典型的框架和库的最大的区别是,库是静态的,由二次开发者调用的;框架是活着的,它是主控者,二次开发者的代码必须符合框架的设计,由框架决定在何时调用。
举个例子,一个网络应用总是要涉及到连接的建立,数据收发和连接的关闭。以库的形式提供是这样的:
conn = connect(host,port);
if(conn.isvalid())
{
data = conn.recv();
printf(data);
conn.close();
}
框架则是这样的:
class mycomm:class connect
{
public:
host();
port();
onconnected();
ondataarrived(unsigned char* data, int len);
onclose();
};
框架会在“适当”的时机创建mycomm对象,并查询host和port,然后建立连接。在连接建立后,调用onconnected()接口,给二次开发者提供进行处理的机会。当数据到达的时候调用ondataarrived接口让二次开发者处理。这是好莱坞原则,“不要来找我们,我们会去找你”。
当然,一个完整的框架通常也要提供各种库供二次开发者使用。比如MFC提供了很多的库,如CString, 但本质上它是一个框架。比如实现一个对话框的OnInitDialog接口,就是由框架规定的。
3.2.2. 针对高度特定领域的抽象和库比较起来,框架是更针对特定领域的抽象。库,比如C库,是面向所有的应用的。而框架相对来说则要狭窄的多。比如MFC提供的框架只适合于Windows平台的桌面应用程序开发,ACE则是针对网络应用开发的框架,Ruby On Rails是为快速开发web站点设计的。
越是针对特定的领域,抽象就可以做的越强,二次开发就可以越简单,因为共性的东西越多。比如我们上面谈到嵌入式系统软件开发的诸多特点,这就是特定领域的共性,就属于可以抽象的部分。具体到实际的嵌入式应用,又会有更多的共性可以抽象。
框架的设计目的是总结特定领域的共性,以框架的方式实现,并规定二次开发者的实现方式,从而简化开发。相应的,针对一个领域开发的框架就不能服务于另一个领域。对企业而言,框架是一种极好的积累知识,降低成本的技术手段。
3.2.3. 解除耦合和应对变化框架设计的一个重要目的就是应对变化。应对变化的本质就是解耦。从架构师的角度看,解耦可以分为三种:
1.
逻辑解耦。逻辑解耦是将逻辑上不同的模块抽象并分离处理。如数据和界面的解耦。这也是我们最常做的解耦。
2.
3.
知识解耦。知识解耦是通过设计让掌握不同知识的人仅仅通过接口工作。典型的如测试工程师所掌握的专业知识和开发工程师所掌握的程序设计和实现的知识。传统的测试脚本通常是将这二者合二为一的。所以要求测试工程师同时具备编程的能力。通过适当的方式,可以让测试工程师以最简单的方式实现他的测试用例,而开发人员编写传统的程序代码来执行这些用例。
4.
5.
变与不变的解耦。这是框架的重要特征。框架通过对领域知识的分析,将共性,也就是不变的内容固定下来,而将可能发生变化的部分交给二次开发者实现。
6.
3.2.4. 框架可以实现和规定非功能性需求非功能性需求是指如性能,可靠性,可测试性,可移植性等。这些特性可以通过框架来实现。以下我们一一举例。
性能。对性能的优化最忌讳的就是普遍优化。系统的性能往往取决于一些特定的点。比如在嵌入式系统中,对存储设备的访问是比较慢的。如果开发者不注意这方面的问题,频繁的读写存储设备,就会造成性能下降。如果对存储设备的读写由框架设计,二次开发者只作为数据的提供和处理者,那么就可以在框架中对读写的频率进行调节,从而达到优化性能的目的。由于框架都是单独开发的,完成后供广泛使用,所以就有条件对关键的性能点进行充分的优化。
可靠性。以上面的网络通讯程序为例,由于框架负责了连接的创建和管理,也处理了各种可能的网络错误,具体的实现者无须了解这方面的知识,也无须实现这方面错误处理的代码,就可以保证整个系统在网络通讯方面的可靠性。以框架的方式设计在可靠性方面的最大优势就是:二次开发的代码是在框架的掌控之内运行的。一方面框架可以将容易出错的部分实现,另一方面对二次开发的代码产生的错误也可以捕获和处理。而库则不能代替使用者处理错误。
可测试性。可测试性是软件架构需要考虑的一个重要方面。下面的章节会讲到,软件的可测试性是由优良的设计来保证的。一方面,由于框架规定了二次开发的接口,所以可以迫使二次开发者开发出便于进行单元测试的代码。另一方面,框架也可以在系统测试的层面上提供易于实现自动化测试和回归测试的设计,例如统一提供的TL1接口。
可移植性。如果软件的可移植性是软件设计的目标,框架设计者可以在设计阶段来保证这一点。一种方式是通过跨平台的库来屏蔽系统差异,另一种可能的方式更加极端,基于框架的二次开发可以是脚本化的。组态软件是这方面的一个例子,在PC上组态的工程,也可以在嵌入式设备上运行。
3.3. 一个框架设计的实例3.3.1. 基本架构3.3.2. 功能特点上面是一个产品系列的架构图,其特点是硬件部分是模块化的,可以随时插拔。不同的硬件应用于不同的通讯测试场合。比如光通讯测试,xDSL测试,Cable Modem测试等等。针对不同的硬件,需要开发不同的固件和软件。固件层的功能主要是通过USB接口接收来自软件的指令,并读写相应的硬件接口,再进行一些计算后,将结果返回给软件。软件运行在WinCE平台,除了提供一个触摸式的图形化界面外,还对外提供基于XML(SOAP)接口和TL1接口。为了实现自动化测试,还提供了基于Lua的脚本语言接口。整个产品系列有几十个不同的硬件模块,相应的需要开发几十套软件。这些软件虽然服务于不同的硬件,但是彼此之间有着高度的相似性。所以,选择先开发一个框架,再基于框架开发具体的模块软件成了最优的选择。
### 3.3.3. 分析
软件部分的结构分析如下:
系统分为软件,固件和硬件三大块。软件和固件运行在两块独立的板子上,有各自的处理器和操作系统。硬件则插在固件所在的板子上,是可以替换的。
软件和固件其实都是软件,下面我们分别分析。
嵌入式系统的软件架构设计!软件软件的主要工作是提供各种用户界面。包括本地图形化界面,SOAP访问界面,TL1访问界面。
整个软件部分分为五大部分:
· 通讯层
· 协议层
· 图形界面
· SOAP服务器
· TL1服务器
通讯层要屏蔽用户对具体通信介质和协议的了解,无论是USB还是socket,对上层都不产生影响。通讯层负责提供可靠的通讯服务和适当的错误处理。通过配置文件,用户可以改变所使用的通讯层。
协议层的目的是将数据进行编码和解码。编码的产生物是可以在通讯层发送的流,按照嵌入式软件的特点,我们选择二进制作为流的格式。解码的产生物是多种的,既有供界面使用的C Struct,也可以是XML数据,还可以是Lua的数据结构(tablegt)。如果需要,还可以产生JSON,TL1,Python数据,TCL数据等等。这一层在框架中是通过机器自动生成的,我们后面会讲到。
内存数据库,SOAP Server和TL1 Server都是协议层的用户。图形界面通过读写内存数据库和底层通讯。
图形界面是框架设计的重点之一,原因是这里工作量最大,重复而无聊的工作最多。
让我们分析一下在图形界面开发工作中最主要的事情是什么。
1.
收集用户输入的数据和命令
2.
3.
将数据和命令发给底层
4.
5.
接收底层反馈
6.
7.
将数据显示在界面上
8.
同时有一些库用来进一步简化开发:
这是一个简化的例子,但是很好的说明了框架的特点:
1.
客户代码必须按照规定的接口实现
2.
3.
框架在适当的时候调用客户实现的接口
4.
5.
每个接口都被设计为只完成特定的单一功能
6.
7.
将各个步骤有机的串起来是框架的事,二次开发者不知道,也无须知道。
8.
9.
通常都要有附带的库。