LabVIEW的软件工程方法.docx
《LabVIEW的软件工程方法.docx》由会员分享,可在线阅读,更多相关《LabVIEW的软件工程方法.docx(25页珍藏版)》请在冰豆网上搜索。
LabVIEW的软件工程方法
LabVIEW的软件工程方法
LabVIEW的软件工程方法
1.序言
本文将会介绍一些关于LabVIEW的系统设计、实现的方法。
我希望读者朋友们通过阅读本文,在仔细思考、比较后能得出自己的结论,形成自己独有的设计和实现方法。
2.软件设计的原则
在讨论更进一步的细节之前,我们先思考一个问题:
什么是好的软件我个人认为,好的软件必须:
(1)对于小的需求变动,程序需要改动的地方少;
(2)在(财政)预算范围内,程序能按时完成;
(3)能实现(几乎)所有的预期功能;
(4)使用简单;
(5)方便维护;
(6)运行良好;
(7)错误处理得当;
(8)安全;
(9)可靠性好。
你有过接手别人项目的经历吗如果有的话,那么你肯定不会对以下情况陌生:
(1)别人的程序总是显得结构复杂、编写的方式很奇怪;
(2)几乎没测试过、没有文档说明、算法看不懂;
(3)改动别人的程序总是比预期花费的时间长,甚至一个小小的改动就能导致整个程序的崩溃。
我有过以上痛苦的经历,所以对我来说:
简单的程序好,复杂的不好;能把复杂问题简单化的设计就是好设计。
3.改进设计的要点
怎样尽量把复杂问题处理得简单呢虽然LabVIEW不是面向对象编程语言,但是我们可以借鉴面向对象的思维方法。
例如耦合(Coupling)、黏合(Cohesion)、信息隐藏(Informationhiding)和抽象(Abstraction)就是不错的思维方法。
关于耦合、黏合、信息隐藏和抽象的概念可以去查看面向对象方面的资料,我这里就不赘述了。
耦合
图3-1紧耦合(Bad)
如图3-1中所示的VI有相当多的输入、输出参数,看起很复杂。
请大家仔细的观察,然后思考这个问题:
是否所有的参数对我们要解决的问题来说都是必不可少的不一定,也许作者只是想把它当“连接器”使用,通过它把其它的VI连起来而已。
图3-2松耦合(Good)
如图3-2中所示,这个命名为“MeasSystem”的VI是一个松耦合的典型,它的设计思想是:
让系统中所有的测试功能都包含在Meas中。
其中“Command”输入参数是一个类似下拉菜单的枚举型控件(自定义的枚举型控件更佳),通过它你可以选择你想要完成的测量任务;“Measurement”和“errorout”两个输出参数则分别输出测量的结果和状态。
这样做的好处将在下面“黏合方式的对比”介绍。
黏合
想象一下:
如果一个程序中像图3-1中那样的VI有5、6个左右,并且它们从左到右像糖葫芦那样串起来连成一行,会是一个什么样情况这些VI之间的连线很难做到没有交叉,多半会彼此搅成一团。
如果隔个一年半载后,让你对系统进行维护,而且这个系统中有n个像这样搅成一团的程序……这将是一个噩梦。
所以,站在系统维护性的角度来看,这样的系统它的黏合性是差的。
图3-3Word
如图3-3所示,“Word”是一个利用ActiveX控制Word的程序,它把系统必须用到的Word功能封装在了里面。
同图3-2所示的程序类似:
“Command”选择Word的功能;“errorout”输出执行状态;因为Word的特殊性(与仪器相比),它多了一个输入参数“StringIn”(用来输入Word路径等),少了一个“Measurement”的输出参数。
很显然,“Word”也是典型的松耦合方式。
图3-4WordControl的前面板
图3-4所示的是WordControl的前面板,它分成3块:
Input、Local和Output。
这三块分别对应输入、中间(局部)和输出变量。
图3-5WordControl的应用
图3-5所示的是WordControl的一个应用程序,它依次实现的功能是:
打开Word文件、跳转到书签、插入文字、保存文档、关闭Word文件。
可以看到:
每调用一次“Word”,它就“专心”的完成一项功能。
这样做的好处有3个:
(1)如果在调试时出了问题,可以很方便的查出哪部分出了问题;
(2)因为控制Word的功能都集成在一个VI中,所以如果要对这个VI进行测试,可以对每个Word的控制功能逐一测试,这样测试就条理清楚;
(3)如果需要添加控制Word的功能,只需要在“Command”枚举变量里添加功能的名称,再添加一个新case分支就行了,其它地方不需要更改。
信息隐藏
请先看一个常见的案例:
某个系统需要用到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个事情):
(1)SwitchPowertounitXon.
(2)SwitchunitXPowerindicatoron.
(3)SwitchPowertounitXoff.
(4)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结构修改下就行了。
此外,其实它的可扩展性和重复利用性也是很好的。
但是现在还看不出什么苗头,所以这里我们暂不讨论。
抽象
什么是抽象在我看来,把一个复杂的问题用一个“概念”来简称,这就叫抽象。
例如,在LabVIEW编程中,如果我们想把一些文字保存到某个文件里,我们不会考虑直接用“0”和“1”的形式把文字数据保存到硬盘,而会调用“Saveto”,要保存的文字作为输入参数送到“Saveto”。
此时,你就使用了抽象的方法,把复杂的问题进行了可视化管理。
你把“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的资料。
组件的编写技巧
当编写组件时,应该综合起来考虑系统中所有组件的内在需求,它们包括:
(1)所有的组件,进入它们的公共函数(PublicFunction)和数据(data)的接口应该尽量简单明了;
(2)增加、删除和修改组件的功能应该尽可能的轻松简单;
(3)组件的任何修改,要尽量对整个系统几乎不产生影响;
(4)组件的状态,要能不断被记录保持在自身内部;
(5)组件要能自己初始化;
(6)组件要有错误处理机制和输入和输出的检查机制。
消息发送
消息发送(MessageSending)是一种控制组件的方法。
例如,如果要让组件A要调用组件B,让组件B完成某件事情组件。
那么组件A就可以通过发送消息给组件B,告诉组件B该做什么,从而达到预定的目的。
在LabVIEW中,消息放送的发式有很多种,但是我推荐利用枚举型控件来发送消息。
因为它使用简单、方便,而且还能自动文档化。
实际上,使用这种方法最大的好处是:
当你在编程时,可以在程序框图里把组件拖出来,而且使用时只要点击枚举型控件,选择你想要实现的功能就行了。
枚举型控件的相关信息
LabVIEW的在线帮助上提供了很全面的枚举型控件的信息,本文把它提取出来列成以下几点:
(1)枚举型控件是一个有下拉框的控件;
(2)下拉框里的选项在控件的属性里进行编辑;
(3)枚举型控件的数据类型可以是U8、U16和U32,这些数据类型决定了枚举型控件下拉框里选择项的最大个数。
比如,如果一个枚举型控件它的数据类型是U8,那么它下拉框里的选择项最多可以达到256个;同理,一个U16型的枚举型控件,它下拉框里的选择项最多可以达到65536个。
那么U32呢……我想2^32对一般编程者来说应该是无数个。
(4)如果把枚举型控件连接到case结构的选择变量(),那么case结构的分支名称将会由数字变成字符串了;
(5)如果枚举型变量参与算术运算(除了“+1”和“-1”的运算外),它将会被当成一个无符号整型(U8、U16或U32)进行运算;
(6)+1”和“-1”的运算,会让枚举型控件里的选择项被旋转,即选择项的开头到结尾的顺序被颠倒过来;
(7)如果让数字作为输入,枚举型控件作为输出。
那么枚举型的输出为最接近数字的那个枚举号所对应的选项(比如数字为,那么枚举型显示第2个选择项)。
如果数字大于枚举型控件里的选择项个数,那么枚举型控件将显示最后一个选择项。
枚举型控件的使用
枚举型控件总共有7种使用方式:
(1)格式化成字符窜
把选择项的内容转换成文本形式,即字符串格式。
当你想把状态保存到一个文件或数据库时,可以采用这个方法。
具体用法参见图4-1。
图4-1枚举型转化成字符串
(2)扫描字符串
把输入的字符串转化成枚举型变量形式,这个与
(1)的用法正好相反。
图4-2字符串转化成枚举型
(3)类型强制转换
图4-3所示的是把一个数字转换成相应的枚举项。
注意,如果枚举型控件里没有与数字“2”匹配的表现形式(representation),那么在此例中,输出的默认显示结果为“ErrorState”;否则为“Test2”。
图4-3枚举型变量的类型强制转换
(4)平化转换
图4-4所示的是平化转换。
当需要通过端口发送指令时,它是非常有用的。
例如,在一个实时系统中,PC客户端和实时服务器之间的通讯协议采用TCP/IP协议,那么我们可以用平化转换的方法在PC客户端和实时服务器之间传送、接收命令。
图4-4平化转换
(5)case结构
如图4-5所以,枚举型变量能增加case结构的可读性,实现自动化文档功能。
这种方法经常在状态机里应用。
图4-5case结构
(6)产生事件的序列
如图4-6所示,由枚举型变量构成的数组,如果连到for循环让其自动索引,就能产生一个事件的序列。
当然,这些事件的先后顺序可以通过简单的下拉菜单选择二改变。
而枚举型变量构成的数组,可以从数据库或者保存的文件中产生获得,这样一个完全可配置的系统只需要少数几个步骤就可以实现。
图4-5事件序列
(7)状态机
如图4-6所示,把枚举型控件连接到while循环的寄存器(register),就构成了LabVIEW中极其重要的一种结构——状态机。
图4-6状态机
严格自定义类型的枚举型控件
请大家思考这种情况:
当你在编写组件时,创建了一个枚举型控件,采用了中所述的使用方法,而且这种方式始终贯穿在你的整个程序中。
现在,你需要在组建中添加一个新功能,你双击了枚举型控件后添加新功能的名称,接着你就会发现你的程序框图的运行箭头断了。
你的把所有的枚举型变量、常量都得改,这是个很繁琐的工作,程序越大越明显。
上述问题的解决方法就是:
创建一个严格自定义类型的枚举型控件。
在LabVIEW菜单中点击“新建”>>“自定义控件”,就可以创建它严格自定义类型的枚举型控件了,图4-7所示的就是严格自定义类型的枚举型控件。
在编写组件时,一律采用这个自定义的枚举型控件。
当你需要修改它时,在自定义控件的面板里修改选择内容就可以了。
修改这个等于把你组件中所有的枚举型控件都修改了。
图4-7严格自定义的枚举型控件
局部变量的保存
我们希望组件在完成一次动作后,能够保持住它此刻的状态信息。
这些状态信息是私有变量,它们是隐藏的。
从组件外部你不能直接改变它们,只能通过发送消息给组件的方式才能进入这些数据。
保存组件状态信息的有效方法是:
利用LabVIEW的for循环或while循环的移位寄存器,把状态信息保存到VI中。
图4-8所示的就是移位寄存器的此种用法。
图4-8移位寄存器
移位寄存器是一种局部变量,它最初的目的用来传递循环时产生的某些值。
它有一个重要的特性——持续性。
即使移位寄存器所在的VI运行完后,移位寄存器中存储的值仍保留在里面。
我们利用这个特性来保持组件的状态信息,使组件保持独立性,从而达到松耦合、数据私有的目的,这些都是有利于信息隐藏(本文节中有详细说明)的。
组件的基本结构
图4-9所示的是一种最简单的组件结构:
图4-9组件的基本结构
首先,在while循环框里放一个case结构;接着,在while循环框外,用一个枚举型常量连接到while循环框,并把连接点改为一个的移位寄存器;然后把移位寄存器连接到case结构的选择变量上。
这样就组成了一个最简单的组件结构。
在节中介绍了一些组件编写技巧,以下我们来逐一论述:
(1)组件要能自己初始化
回到图4-9,在右边的程序框图中,我们用到了一个功能函数“首次调用”(在LabVIEW函数面板中点击“编程”>>“同步”>>“首次调用”即可)。
在while循环框左边,有一个小圆轮,那就是“首次调用”。
当这个组件作为subVI第一被调用时,“首次调用”的返回值为真。
此时,枚举型常量里的“Initialize”就会被传送到case结构中,使case结构执行名称为“Initialize”分支里的代码。
“Initialize”分支里的代码一般用来完成文件加载、数组大小设置、仪器设置,或者与初始化相关的功能。
值得注意的是,当你从前面板按“箭头”执行该程序时,“首次调用”返回值总是为真。
另外,如果你的组件不需要初始化,可以不用“首次调用”这个功能。
(3)组件要有错误处理机制和输入和输出的检查机制
图4-10组件的各状态
图4-10所示的是某个组件的各个状态,它有6个状态:
UnderRange、Command1、Command2、Command3、Finish、OverRange。
其中,UnderRange和OverRange两个状态的作用是用来做异常处理:
当枚举量的输入超过范围时,它将会反馈一个错误信息给调用这个组件的程序。
这样做是为了让组件具备错误处理能力,从而保证组件运行的流畅性。
Finish状态可以用来检查后置条件,甚至你还可以添加一个Start状态来检查前置条件。
关于前置条件和后置条件的检查将会在下节中介绍。
(4)增加、删除和修改组件的功能应该尽可能的轻松简单;组件的任何修改,要尽量对整个系统几乎不产生影响
如果要在组件中增加一个新功能,首先把新功能的名称添加到枚举量里面,接着在case结构中复制或添加一个新case分支,最后把新功能的动作添加到新的case分支里就行了。
反之,如要删除一个功能,只要删除对应的case分支和枚举量里面的功能名称。
同理,修改功能的话,也只需把相应的case分支做修改就行了。
(5)进入组件的公共函数(PublicFunction)和数据(data)的接口应该尽量简单明了
5.软件工程概要
到目前为止,本文主要是围绕设计及其实现问题来进行阐述。
诚然,设计在软件工程中占了很大一部分,但不是全部。
软件工程是一个巨大的学科,在这一章我们将更全面的讨论这个问题。
之前,因为我们想专注于“设计”这个问题,所以其它问题我们没有讨论。
但是,为了更深入的进行研究,我们需要先讨论一些软件工程方面其它相对次要点的问题。
要明白世界上没有“万灵丹”,具体问题需要具体分析。
现在所有软件工程的理论都是根据最新的信息而发展起来的,这也意味着软件工程的理论是不停变化的。
什么是软件工程
软件,是指用于计算机的程序或操作信息;工程,就是应用科学的方法去设计、去构造事物,包括使用机器、运用构造学等。
所以,按照我的理解,软件工程就是运用科学的方法去设计程序或操作信息。
换句话说,软件工程就是运用某种“科学的方法”来设计软件。
它的反面就是不运用任何科学方法来设计软件。
我主张的“科学的方法”,就是前面几章叙述的“LCOD”。
不运用任何科学方法来设计软件,有很大可能将导致任务失败。
那么,除了设计以外,软件工程中还有哪些需要注意的问题呢所有的系统开发都将经过这样的阶段:
需求分析、设计、构造、验证和维护。
不同的公司有不同的方法来走这些流程,下面将简单叙述我们是怎么做这些的。
我们这样做是因为它行之有效。
实际上,我们在本章讨论的软件工程方面,都是我们在工作中直接运用的方法。
我们需要注意的是软件童话和悲剧。
避免童话,从悲剧中学习经验,它们对软件工程都没有帮助。
童话故事:
最近的软件太容易编了,谁都能完成它;
最近出来的xx语言功能强大,能保证这个项目的成功;
这种新设计方法将能保证所有的软件都成功实现;
xx公司新出的项目管理软件能使失败的软件开发成为过去式;
最新的案例工具包将使软件开发人员显得多余
采用xx模型能解决所有问题;
软件需求是固定不变的。
恐怖故事:
这份清单可能够详细了。
最近出版了很多的新书,告诉我们软件开发过程中各种糟糕的情况。
但是猜猜看发生了什么大多数的悲剧都与直接或间接相信童话故事有关。
RobertGlass的书《SoftwareRunaways》(又名《软件开发的滑铁卢》),就充分阐述了这一点:
不要被凶恶的大灰狼吃掉,也不要相信童话故事。
那么怎样保证我们的项目不会成为悲剧呢应该从实际出发,选择正确的软件工程方法。
我们经常遇到那些对某种工具、方法等迷信的人(经验主义者),但是我们更应该相信“不管黑猫白猫,抓到老鼠就是好猫”。
普遍的质疑声
任一本书或者文章在讨论软件生存周期的时候,总会以质疑经典的瀑布式生存周期为开头。
如图5-1所示
图5-1Waterfalllifecycle
生命周期显示了一个基本的项目由开始到交付直至以后的全过程。
瀑布式只是最早几个用书面表达的生存周期之一。
但是,没过多久时间,瀑布式生存周期就被实践证明是难以实现的模式。
瀑布式生存周期,从表面看起来没有问题,但是它要求周期中前一个阶段完全实现后才能进入下一个阶段。
不管是需求改变、设计改变、执行还是单元测试,都能反映出存在的问题:
只要在生命周期过程中产生变化,那