LabVIEW的软件工程方法1.序言本文将会介绍一些关于LabVIEW的系统设计、实现的方法。我希望读者朋友们通过阅读本文,在仔细思考、比较后能得出自己的结论,形成自己独有的设计和实现方法。2.软件设计的原则在讨论更进一步的细节之前,我们先思考一个问题:什么是好的软件?我个人认为,好的软件必须:(1)对于小的需求变动,程序需要改动的地方少;(2)在(财政)预算范围内,程序能按时完成;(3)能实现(几乎)所有的预期功能;(4)使用简单;(5)方便维护;(6)运行良好;(7)错误处理得当;(8)安全;(9)可靠性好。你有过接手别人项目的经历吗?如果有的话,那么你肯定不会对以下情况陌生:(1)别人的程序总是显得结构复杂、编写的方式很奇怪;(2)几乎没测试过、没有文档说明、算法看不懂;(3)改动别人的程序总是比预期花费的时间长,甚至一个小小的改动就能导致整个程序的崩溃。我有过以上痛苦的经历,所以对我来说:简单的程序好,复杂的不好;能把复杂问题简单化的设计就是好设计。3.改进设计的要点怎样尽量把复杂问题处理得简单呢?虽然LabVIEW不是面向对象编程语言,但是我们可以借鉴面向对象的思维方法。例如耦合(Coupling)、黏合(Cohesion)、信息隐藏(Informationhiding)和抽象(Abstraction)就是不错的思维方法。关于耦合、黏合、信息隐藏和抽象的概念可以去查看面向对象方面的资料,我这里就不赘述了。3.1耦合图3-1紧耦合(Bad)如图3-1中所示的VI有相当多的输入、输出参数,看起很复杂。请大家仔细的观察,然后思考这个问题:是否所有的参数对我们要解决的问题来说都是必不可少的?不一定,也许作者只是想把它当“连接器”使用,通过它把其它的VI连起来而已。图3-2松耦合(Good)如图3-2中所示,这个命名为“MeasSystem”的VI是一个松耦合的典型,它的设计思想是:让系统中所有的测试功能都包含在MeasSystem.vi中。其中“Command”输入参数是一个类似下拉菜单的枚举型控件(自定义的枚举型控件更佳),通过它你可以选择你想要完成的测量任务;“Measurement”和“errorout”两个输出参数则分别输出测量的结果和状态。这样做的好处将在下面“黏合方式的对比”介绍。3.2黏合想象一下:如果一个程序中像图3-1中那样的VI有5、6个左右,并且它们从左到右像糖葫芦那样串起来连成一行,会是一个什么样情况?这些VI之间的连线很难做到没有交叉,多半会彼此搅成一团。如果隔个一年半载后,让你对系统进行维护,而且这个系统中有n个像这样搅成一团的程序……这将是一个噩梦。所以,站在系统维护性的角度来看,这样的系统它的黏合性是差的。图3-3WordControl.vi如图3-3所示,“WordControl.vi”是一个利用ActiveX控制Word的程序,它把系统必须用到的Word功能封装在了里面。同图3-2所示的程序类似:“Command”选择Word的功能;“errorout”输出执行状态;因为Word的特殊性(与仪器相比),它多了一个输入参数“StringIn”(用来输入Word路径等),少了一个“Measurement”的输出参数。很显然,“WordControl.vi”也是典型的松耦合方式。图3-4WordControl的前面板图3-4所示的是WordControl的前面板,它分成3块:Input、Local和Output。这三块分别对应输入、中间(局部)和输出变量。图3-5WordControl的应用图3-5所示的是WordControl的一个应用程序,它依次实现的功能是:打开Word文件、跳转到书签、插入文字、保存文档、关闭Word文件。可以看到:每调用一次“WordControl.vi”,它就“专心”的完成一项功能。这样做的好处有3个:(1)如果在调试时出了问题,可以很方便的查出哪部分出了问题;(2)因为控制Word的功能都集成在一个VI中,所以如果要对这个VI进行测试,可以对每个Word的控制功能逐一测试,这样测试就条理清楚;(3)如果需要添加控制Word的功能,只需要在“Command”枚举变量里添加功能的名称,再添加一个新case分支就行了,其它地方不需要更改。3.3信息隐藏请先看一个常见的案例:某个系统需要用到DIO卡来控制LED灯的亮灭和继电器的通断。在测试时,当继电器闭合后,系统的某个相应单元会连接到电源;继电器断开,这个单元就会与电源断开。此外,LED起指示灯的作用,它用来告诉用户某个单元是否通电。一般情况下,可以通过设置DIO卡端口(port)和通道(channelorbyte)的值,来控制DIO的输出。为了更方便的说明问题,我们假设DIO卡对LED灯的控制是负逻辑(ture=灭,false=亮),对继电器的控制是正逻辑(ture=闭合,false=断开)。让我们来看看下图3-6中所编写的VI。初看起来会觉得还不错,除了LED的负逻辑稍微有点难理解外。图3-6信息隐藏(bad)但是如果这个系统有许多LED灯和继电器呢?恐怕得把图3-6所示的编程任务量翻20倍还不止。另外,假设编写这个系统程序的人已经离职了,但是现在系统的硬件驱动有了更改,而且部分硬件驱动的逻辑也变了。现在由你接手这个项目,你需要对众多的端口和通道值重新进行修改和设置;而且,你还发现前任者对这个系统程序几乎没有任何注释说明。这时,你将会觉得这个工作量似乎不像想象中的那么小。DIO驱动不应该这样复杂,它仅仅只有0和1而已。但是如果所有的VI都像图3-6那样编写的话,恐怕这个系统会真的变得很复杂。上述问题的解决方法是,考虑把具体的信息隐藏在组件(component)里面。简单点说,就是考虑组件(component)要做什么,而不是怎样去做。因此,我们可以把DIO要做的事分为4个(考虑到要与前面板命令保持一致,所以就用英文来写这4个事情):()SwitchPowertounitXon.()SwitchunitXPowerindicatoron.()SwitchPowertounitXoff.()SwitchunitXPowerindicatoroff.实际上还有个初始化的功能,但考虑到它对系统的问题解决影响不大,因此由部件自己“决定”是否需要初始化。图3-7DIO组件前面板图3-8所示的是DIO组件命令的说明。图3-8DIO组件命令图3-9信息隐藏示例关于如何实现图3-9中所示VI编程,将在下节讨论。我们先来看信息隐藏后的好处。首先,它的可读性更好。DIO组件只有Command、Unit和errorin三个输入。通过Command,你可以知道DIO组件现在要执行什么任务;通过Unit,你可以知道DIO组件现在对哪个单元进行操作;而errorin,是典型的数据流操作,你可以通过它来传递DIO组件的状态。其次,它的维护性好。熟悉LabVIEW编程的读者可能已经猜到,DIO组件里面的程序结构是状态机,而Command则连接在case结构的select变量(?)上。显然,case结构必有4个分支对应DIO组件要完成的4件事。所以,如果要进行前面所说的系统维护工作的话,只要把DIO组件里的4个case结构修改下就行了。此外,其实它的可扩展性和重复利用性也是很好的。但是现在还看不出什么苗头,所以这里我们暂不讨论。3.4抽象什么是抽象?在我看来,把一个复杂的问题用一个“概念”来简称,这就叫抽象。例如,在LabVIEW编程中,如果我们想把一些文字保存到某个文件里,我们不会考虑直接用“0”和“1”的形式把文字数据保存到硬盘,而会调用“SavetoFile.vi”,要保存的文字作为输入参数送到“SavetoFile.vi”。此时,你就使用了抽象的方法,把复杂的问题进行了可视化管理。你把“0”和“1”数据保存抽象成“SavetoFile”这个概念。软件设计时有两种抽象方法:(1)功能抽象LabVIEW支持子函数(subVI)功能,因此在设计时,我们可以很方便的把一个复杂的程序分割成多个小块。LabVIEW的这种层次结构实际上就是提供的功能抽象:顶层的功能模块可以划分为几个子功能模块,而子功能模块又能含有它自己的子功能模块……这种功能抽象是打破问题复杂性的主要途径。(2)数据抽象计算机处理“0”和“1”,但是编程语言会提供给我们更详尽的数据类型,例如整形(Integer)、实数型(RealNumber)、字符型(String)等。这些数据类型实际上就实现了数据抽象的功能,它们把“0”和“1”抽象成了人们容易理解的整形(Integer)、实数型(RealNumber)、字符型(String)等。LabVIEW还提供了簇(Cluster)这个数据类型,它可以把相关的几个数据绑定在一个,而这些数据不一定是同一个类型。例如,要表示一个圆,它的属性为圆点坐标(X,Y)、半径R和颜色C,其中圆点坐标是一个双精度型的一维数组,半径R是一个双精度型的实数,而颜色是一个0~255的整形。那么我们把圆点坐标(X,Y)、半径R和颜色C绑定成一个簇后(最好是自定义类型的簇),以后就可以用自定义的簇来表示这个圆。这也是一个数据抽象的过程,它把相关的圆点坐标(X,Y)、半径R和颜色C简化成一个数据——簇,这样就方便了我们在应用程序中传递数据。通过把数据封装和隐藏在VI中,抽象的程度可以达到类似面向对象(OOP)编程的水平。用发送信息的方式作为进入和修改数据的唯一途径,是数据抽象的好方法。而数据抽象则是一种模块化设计的好方法。下面我们将用几个例子来说明正确使用抽象的好处。例如,在某个测试系统中的项目中,我们需要编写一个控制继电器闭合、断开的组件,它的基本原理是利用一张继电器板卡来控制继电器的闭合和断开。一种低层次的抽象方法是:用几个subVI来实现初始化、设置和清除硬件卡端口的功能,测试系统的每次继电器操作都是通过调用相应的subVI来完成。这种抽象方法如图3-10所示,为了方便讨论,我们把这些实现初始化、设置和清除的VI称为“relayVIs”图3-10低层次的抽象高层次的抽象做法是:把继电器操作的细节和通道号封装在一个组件里面,例如,用一个命名为“Switch”的VI来把所有的“relayVIs”封装在里面。这样,想进入“relayVIs”进行操作就只有通过“Switch”这个VI。高层次的抽象方法如图3-11所示。图3-11高层次的抽象高层次抽象和低层次抽象相比,好处有哪些?我们通过图3-12来比较一下。图3-11抽象层次比较高层次抽象允许你修改Switch函数,而不改变它的函数接口。例如,如果发生继电器卡更换或者继电器的驱动函数、配置发生改变等情况,你不需更改程序的基本框架,只要用Switch组件把新的“relayVIs”重新封装起来就行了。所以,在高层次抽象里,那个额外的抽象层可以防止软件设计的更改。另外,高层次抽象使你程序的可读性更好。另外,当你发送一个命令(例如“InitializeSwitchSystem”、“ConnectMea1Circuit”等)时,你想继电器进行的动作将是清楚明了的。模糊不清在软件中是坏现象,因为它容易导致bug的产生。4.LOCD的实现LCOD是LabVIEWComponentOrientedDesign的缩写,意思为面向对象的LabVIEW组件。组件(Component)是面向对象中一个常见的概念,我这里就不赘述了,对它有兴趣的读者可以自行去查阅C++Builder的资料。4.1组件的编写技巧当编写组件时,应该综合起来考虑系统中所有组件的内在需求,它们包括:(1)所有的组件,进入它们的公共函数(PublicFunction)和数据(data)的接口应该尽量简单明了;(2)增加、删除和修改组件的功能应该尽可能的轻松简单;(3)组件的任何修改,要尽量对整个系统几乎不产生影响;(4)组件的状态,要能不断被记录保持在自身内部;(5)组件要能自己初始化;(6)组件要有错误处理机制和输入和输出的检查机制。4.2消息发送消息