尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

LabVIEW工厂模式实战:优雅管理多型号仪器对象创建

LabVIEW工厂模式实战:优雅管理多型号仪器对象创建 你在做测试系统上位机的时候一定碰过这种场面代码里到处是“如果型号是A就创建A设备如果型号是B就创建B设备”的Case结构每加一款新仪器主程序就要跟着改一遍界面逻辑和驱动逻辑搅成一锅粥。这个问题的答案就是设计模式里的工厂模式。这篇是LabVIEW OOP系列第四篇我打算把工厂模式怎么在LabVIEW里真正落地一次讲透。这篇文章适合已经会建类、知道继承和动态分发但还没系统用过设计模式的LabVIEW开发者。看完你能搞懂两件事工厂模式到底解决什么问题在LabVIEW这种图形化语言里怎么用最自然的方式实现简单工厂和工厂方法模式。我还会带一个完整的“多型号万用表管理”案例以及我在实际项目中踩过的坑争取让你少走几个月的弯路。1. 为什么LabVIEW里的“对象创建”比别的语言更值得讲清楚1.1 没有构造函数类是值类型很多从C、Java转过来的朋友最开始会被LabVIEW的类搞懵。Java里new DMM()会返回一个对象LabVIEW里没有这种语法类的实例不是靠“调用构造函数”产生的而是靠数据流本身。你往框图上拖一个DMM.lvclass的类常量这个常量就是一个对象实例你把一个类控件放到前面板或者自定义控件里这也是一种对象。这带来一个很关键的差异类在LabVIEW里默认是值类型。除非你内部使用引用句柄、队列、用户事件这些机制来承载重数据否则一个对象从子VI传到调用方涉及的是数据拷贝。这对于我们设计工厂方法很重要。因为工厂返回的对象其实是一个“父类类型”的输出端子上挂着“子类类型”的数据系统会在运行时保留它真正的类型身份。1.2 动态分发 重写是多态的基础要让工厂模式起作用必须依赖LabVIEW动态分发Dynamic Dispatch机制。父类里定义一个动态方法比如Read.vi子类继承后右键选择“重写Override”在子类里写自己的行为。当调用方持有一个父类类型的对象调用Read.vi时LabVIEW会根据这个对象运行时真正的类型去调用子类版本的方法。这个机制有一个前提方法必须是动态分发的而不是静态调用Static Dispatch。静态调用会在编译期就绑定到父类方法不管你传入的是什么子类。所以用工厂模式时第一步要回去检查一下你那个基类方法是不是右键选择了“支持动态分发Dynamic Dispatch”。很多人的工厂“没生效”第一反应是找模式问题其实根子在这上面。1.3 工厂模式解决的核心痛点没有工厂的时候调用方同时承担三件事知道有哪些具体型号、负责创建具体对象、再调用业务接口。第二件事最恶心因为“创建哪一类对象”的判断逻辑会散落在界面事件、初始化代码、配置读取各处。每加一个新型号你至少要改三四处。工厂模式做的就是把“创建对象”这个动作收口让调用方只面对一个统一的父类接口。你要加新型号只加一个子类和一个工厂分支主流程基本不动。用一句话概括它把“变化”隔离在工厂这一层不让变化扩散到整个系统。2. 简单工厂模式最快落地的LabVIEW写法2.1 搭一个抽象基类和两个具体子类简单工厂是工厂模式里最朴素的一种。先设计一个基类我这里拿万用表举例。项目浏览器里新建一个DMM.lvclass作为父类再新建Agilent34401.lvclass、Keithley2000.lvclass两个子类都继承自DMM。父类里定义动态方法Initialize.vi、Configure.vi、Read.vi、Close.vi。子类里分别右键这些方法选择“重写”。比如Keithley2000的Read.vi里面可能发的是MEAS:VOLT:DC?Agilent34401的Read.vi可能走的是另一套命令。这些细节全部封装到子类里调用方完全不需要关心你用的是SCPI还是VISA。2.2 用“转换为更通用类”节点实现父类输出接下来是最关键的一步写工厂VI。不一定要建工厂类简单工厂可以直接用一个独立VI实现比如Create DMM.vi。打开VI放置一个条件结构Case Structure输入选一个枚举DeviceType枚举里包含你支持的仪器型号。然后在这个条件结构的每个分支里把你需要的子类常量拖进来。比如在Agilent34401这个分支里放一个Agilent34401.lvclass的常量这个常量就是一个该类的实例。然后从函数面板找到“编程 - 类 - 转换为更通用类To More Generic Class”把它放在框图上选择目标类型为DMM。子类常量连到转换节点的输入转换节点的输出再连到条件结构外部的输出隧道。这个输出隧道在连线板上对应的输出控件类型要设成DMM父类类型。这样一来不管内部Case进了哪个分支外部拿到统一都是“WMM父类”的对象但运行时类型还是各自具体的子类。注意不要在Case结构里直接连线子类常量到输出隧道。各个分支的输出类型不一致必然断线。LabVIEW不会像C那样允许子类直接作为父类引用返回必须显式做一次向上转换。这是LabVIEW实现工厂模式时最容易卡住的地方。2.3 调用侧写起来有多舒服调用侧代码会清爽得多。假设前面板有个下拉框用户选了KEITHLEY_2000你只需要把枚举喂给Create DMM.vi拉回来的DMM对象直接连到后面所有测流程方法上。DeviceType枚举 - Create DMM.vi - DMM对象 DMM对象 - Initialize.vi DMM对象 - Configure.vi DMM对象 - Read.vi以后用户要换型号界面下拉框换个选项就行主流程代码一行都不用动。我再强调一下在这个流程里调用方接触的永远是父类公开出来的方法它甚至不需要知道具体返回的是哪个子类。这不仅仅是“好维护”更重要的是能让你把测试流程的代码稳定下来流程才是业务的核心资产仪器型号只是随时会变的配置项。2.4 简单工厂的边界在哪简单工厂好写、直观但有个缺点所有产品的创建逻辑都集中在一个VI里分支多了以后这个VI会膨胀。每加一个型号除了建新的子类你还要打开Create DMM.vi去增加一个Case分支这个动作本质上是对已有代码的修改。虽然只是小改但如果不同的设备在创建阶段的初始化逻辑特别复杂比如有些要校准有些要加载预设配置有些要启动单独的监控线程那这个简单的Case结构很快就会变成一坨乱麻。所以简单工厂更适合型号数量不多、创建逻辑差异不大、且短期内不会频繁扩展的场景。如果你的设备类型已经开始呈现出明显的“族”的特征下一节这种工厂方法模式会是更好的选择。3. 工厂方法模式把“生产决策”拆到子工厂3.1 思路转换每个产品配一个工厂工厂方法模式不再用一个“万能开关”去判断该创建谁而是把“创建产品”这个动作定义在抽象工厂类里让每个具体工厂子类去决定到底创建哪种产品。调用方拿到的是一个工厂对象它不需要知道工厂内部在造什么只管调用工厂的“Create”方法拿产品。对应到LabVIEW结构是这样的新建DMMFactory.lvclass抽象工厂类定义一个动态方法Create DMM.vi输出类型是DMM父类。新建AgilentFactory.lvclass和KeithleyFactory.lvclass都继承自DMMFactory。两个具体工厂类重写Create DMM.vi前者返回Agilent34401实例后者返回Keithley2000实例。如果把类结构用文本列出来大概是这样DMM产品抽象类 |-- Agilent34401 |-- Keithley2000 DMMFactory工厂抽象类 |-- AgilentFactory |-- KeithleyFactory3.2 LabVIEW里的具体实现步骤第一步创建DMMFactory.lvclass在类里新建VICreate DMM.vi。记得把这个方法设为动态分发并且输出端类型选DMM父类而不是具体子类。第二步新建AgilentFactory.lvclass。右键父类DMMFactory.lvclass选择“新建子类”让新类继承父类。然后在项目浏览器里选中Create DMM.vi右键选“重写”会自动生成一个同名重写VI。第三步在重写VI里把一个Agilent34401.lvclass常量拖到框图上连到To More Generic Class节点目标类型选DMM再连到输出接线端。KeithleyFactory同理。调用的方式也变了。主程序不再直接依赖Create DMM.vi而是先持有一个工厂对象。这个工厂对象从哪来可以来自配置文件可以来自一个简单的初始化函数也可以在最外层用简单工厂来创建工厂。然后调用工厂对象的Create DMM.vi工厂对象 - Create DMM.vi - DMM产品对象这样一层套一层核心测试代码完全没有Case判断真正把“变化”推到了系统最边缘。3.3 简单工厂还是工厂方法我的判断标准有些文章喜欢把它俩分开说工厂方法要优于简单工厂。我的观点更务实按项目规模来。如果你只有两三种设备创建过程又差不多老老实实用简单工厂。多建四五个类文件代价大于收益。如果设备型号开始向十几个发展并且型号之间创建逻辑差异很大比如有的设备连接以后需要做固件升级检查有的是网口通信需要先建立TCP连接有的是GPIB需要设置主从地址那必须考虑工厂方法模式把每种设备完整的“装配过程”收进各自工厂里。还有一个信号当你发现简单工厂的条件结构里每个Case分支超过20行并且有大量分支间的共性逻辑想复用的时候赶紧切换成工厂方法模式。工厂方法天然支持每个子类工厂在父类基础上扩展你可以把公共流程放到父类工厂里把型号差异留在子类重写方法里这其实就是模板方法模式的一种变体。4. 实战案例一套支持多种万用表的上位机4.1 需求与类结构设计假设我们在做一个产测上位机被测产品需要测量电压、电阻、电流硬件配置选了两种万用表Agilent 34401 和 Keithley 2000。程序启动后从配置文件里读取当前用的型号用户界面也能手动切换。后续测试流程包括校准、检定、数据记录全部走同一个测量接口。我设计的类结构就是前面讲的那套DMM.lvclass父类动态方法有Initialize、Configure、Read、CloseAgilent34401.lvclass继承DMMKeithley2000.lvclass继承DMMCreate DMM.vi作为简单工厂入口根据枚举创建具体的DMM对象这里我先用简单工厂因为两种仪器创建过程都是“调Initialize”差异不大没必要上工厂方法。4.2 工厂VI内部的连线细节打开Create DMM.vi前面板连接板放两个端子一个是枚举输入Device Type一个是DMM类输出。框图上放一个条件结构Case选择器接到枚举输入。两个分支里分别做这样的事Keihtley2000分支放置Keithley2000.lvclass常量连接To More Generic Class节点目标类型DMM输出到隧道。Agilent34401分支放置Agilent34401.lvclass常量同样转换成DMM输出到隧道。这个VI本身不包含任何仪器通信逻辑它只负责“根据枚举返回正确的对象”。仪器通信逻辑全部在子类重写的Initialize.vi/Read.vi里。所以从职责上看这个工厂VI可以被当作整个系统的“注册表”来理解系统里支持哪些设备、每个设备返回什么对象在这里一目了然。4.3 主界面和测试流程里的用法主界面可以做成一个状态机。初始化状态里读配置文件拿到Device Type调Create DMM.vi把返回对象放到移位寄存器里。后面的测量状态直接把这个对象连到Read.vi拿到的电压值显示到前面板同时写入TDMS文件。事件结构里处理“切换设备型号”这个事件时也只需要做一件事重新调用Create DMM.vi把新返回的对象替换掉旧对象再调用一次Initialize.vi。至于仪器是GPIB还是串口子类内部自己处理主界面完全不知道。如果你在做labview控制6221与2182同步采集这类多设备同步的软件工厂模式一样有用。6221是电流源2182是纳伏表你完全可以把它们各自封装成产品子类然后由一个同步控制类去协调。初始化时用工厂批量创建对象再统一调用Initialize等到所有设备都Ready了再统一触发采集。这样同步逻辑不用关心具体仪器命令异常处理也集中在各自的子类里查问题的时候会节省大量时间。5. 进阶扩展注册表式工厂与按需动态加载5.1 为什么要走向注册表式简单工厂和工厂方法有一个共同的隐含前提所有产品类型在编译期就是已知的。但有些系统做得比较大比如一个通用测试平台会允许第三方模块通过配置文件或插件的方式接入新设备。你在编译的时候根本不知道插件里有哪些类怎么让工厂也支持这种扩展LabVIEW不像Java有反射但只要思路转换一下依然可以做到一种“注册表式”的动态工厂。做法是维护一个全局的数据结构比如功能全局变量FGV或者队列里面存的是“设备类型字符串 - 创建VI引用”的映射关系。各个设备模块启动时把自己的创建VI引用注册进去工厂要创建对象时先去查这个表再通过“按引用调用Call by Reference”来执行对应的创建VI。5.2 用VI引用实现键值对工厂具体做法我先说个大概。定义一个DeviceRegistry.lvclass内部有一个私有数据可以是MapString, VI Ref或者自定义的“名称-引用”簇数组。类方法包括Register.vi和Lookup.vi。Register.vi输入一个设备名字字符串和一个VI引用把引用保存进去Lookup.vi输入设备名字返回对应的VI引用。创建VI引用的时候注意你是调用不连线的VI所以要把“创建VI引用”的连线板配置成输入一个枚举或字符串输出一个DMM父类对象。这样调用方只要拿到引用就能统一用某个传入参数去执行最终拿到一个DMM对象。DeviceType枚举 - Lookup.vi - VI引用 - Call by Reference - DMM对象这个方案的好处是真正做到了“开闭原则”新增设备不需要改动任何已有的Case结构只需要新写一个创建VI然后在启动时调一次Register.vi把它注册进去。整个工厂核心逻辑从此不需要再动。5.3 这块的适用边界我必须泼点冷水注册表式工厂很灵活但在LabVIEW里的代价也不小。VI引用创建时要求连线板签名严格匹配一旦输入输出类型不一致只能在运行时才发现错误信息还往往不够直观。另外VI引用管理不当容易造成内存泄漏特别是在频繁创建引用、又不关闭引用的场景下内存涨得很快。我也见过团队为了追求“完全可插拔”把项目搞得异常复杂最后新同事接手根本不敢动那个全局注册表。所以我的建议是常规的工控上位机、产测软件用简单工厂和工厂方法组合就足够了。只有当你确实面临多套独立开发的模块需要集成到同一平台而且模块边界稳定、团队规模也足够支撑这种抽象时再考虑注册表式工厂。设计模式是解决问题的手段不是用来炫耀的装饰品。6. 常见问题与排查技巧实录6.1 常见问题速查表症状可能原因解决办法接线端断线错误提示“类型不匹配”Case分支里子类常量没有转成父类类型在分支里加To More Generic Class目标选父类方法调了半天执行的都是父类逻辑方法不是动态分发或子类没重写右键方法勾选“动态分发”子类里右键选“重写”拿到了对象但看不到子类特有方法这是多态的正常限制变量静态类型是父类先用运行时类型判断再使用To More Specific Class转成具体类反复调用工厂内存涨得厉害LVClass是值类型对象内部含大数组/大字符串大数据用队列、用户事件、引用句柄承载不要直接放类里Case结构单个分支标签不匹配运行进默认分支枚举不是严格类型定义标签值不同步用type_def.ctl严格类型定义枚举子类在项目里明明存在但框图上找不到类常量类文件没保存或者项目浏览器未刷新保存全部右键项目浏览器“刷新”6.2 关于动态分发、转换节点的几个细节使用To More Generic Class时如果数据到达这个节点的类型已经是父类那么转换节点不会报错但也起不了什么作用。真正的场景一定是Case分支里的局部数据是子类需要向上统一。反过来向下转换To More Specific Class更危险如果源数据实际运行时不是目标子类是会直接报错的。所以向下转换前一定要先做类型判断可以在父类里设计一个动态方法Get Class Name.vi子类重写返回自己的类名判断完再转换。还有一个细节容易被忽视子类重写后的方法要保持输入输出端子的数量、类型、顺序与父类一致。LabVIEW虽然允许子类重写版本增加自己的其他输入输出但一旦不一致很容易在调用时出现无法匹配的问题。我的习惯是重写时直接复制父类方法作为模板只改内部逻辑不断动连线板签名。6.3 工程架构层面的建议一个工厂模式用的舒服的项目通常类结构非常稳定。我在实际项目里有个坚持工厂类里不放业务逻辑它只负责创建对象类的私有数据只放这个对象自己的状态比如VISA会话、设备名称、量程配置业务流程比如“先测电压再测电流然后计算功率”放在状态机或者独立的业务VI里不要塞进类方法里。这样类才能保持职责单一工厂模式才有意义。另外别在初始化的时候把所有设备资源都打开一套。工厂的好处是你可以延迟创建对象等真正用到某台设备的时候再去创建、占用资源。对工控上位机来说这种按需创建的思路特别重要。我见过一个程序一启动就把所有支持的仪器型号都初始化了一遍结果某台仪器没开机程序直接卡死在等待响应上。换成工厂模式以后设置为“只初始化配置里启用的设备”问题彻底消失。在具体编码过程中我还会把所有设备型号枚举定义成严格类型定义并在枚举的“标签”和“数值”保持一致。这样在Case结构选择器上写分支时不会出现同名不同值的情况。严格类型定义还能保证调用方和工厂VI用的是同一个枚举避免将来界面下拉框和工厂内部Case各维护一套数值。最后一个建议给类方法命名要克制。Create DMM、Initialize、Read、Close这种一看就懂别起什么ExecutePrecisionMeasurementProcess这种又长又模糊的名字。命名清晰了工厂模式的阅读成本能再降一半。我个人在实际项目里最直观的体会是从用了工厂模式以后我再也没有在下位机固件版本变更、仪器型号调整这件事上熬夜加班过。以前改一次型号要动界面、动流程、动驱动三个地方现在只动配置文件和一个新子类。这才是设计模式应该带来的改变。
返回列表