LabVIEW工厂模式实战:用面向对象设计让代码可维护、可扩展
2026/9/15 10:14:46 网站建设 项目流程

如果你是用LabVIEW写过几年程序的人,大概率会遇到这种尴尬:项目做到一半,客户说要换硬件、换协议、换数据格式,然后你捏着鼻子把原来写好的代码翻出来,找到那一串又一串的条件分支,把新情况塞进去,再把旧分支注释掉。几次下来,代码越来越长,越来越乱,改一个地方牵一发动全身,最后连你自己都不想碰那个VI。这套路我太熟了,所以我一直在LabVIEW里折腾面向对象,而从面向对象走向可维护架构的关键一步,就是今天要聊的工厂模式。

工厂模式在纯文本语言里是经典设计模式,放到LabVIEW的G语言环境下,很多人觉得用不上,也有人觉得类都写不利索,搞什么工厂。但恰恰相反,LabVIEW的图形化数据流天然适合工厂模式,它能把“创建对象”和“使用对象”彻底拆开,让你的代码从“改需求就重写”变成“改需求就加一个类”。这个系列前面几篇我们聊了LabVIEW OOP的基础、封装继承和多态,到这一篇,终于轮到真正能上项目干活的设计模式了。

这篇内容适合谁?适合已经掌握LabVIEW类的基本概念(创建类、私有数据、方法VI),但还没想明白怎么在真实项目中组织代码的人。我会从“为什么需要工厂模式”讲起,然后用一个完整的波形生成器案例,带你把简单工厂、抽象工厂、注册式工厂一个个在LabVIEW里落地。每个步骤都会给出VI结构、接线思路、类的关系图,以及我实际调试时踩过的坑。

1. 为什么LabVIEW项目需要工厂模式

先别急着写代码,咱把问题掰扯清楚。工厂模式解决的核心痛点就一句话:把“创建谁”和“怎么用”解耦。在LabVIEW里,这个痛点会以几种非常具体的形式出现。

1.1 经典困境:条件结构堆积

假设你要写一个数据采集程序,支持三种采集卡:NI的PCIe-6321、USB-6009,还有一台串口仪器。你的顶层VI长什么样?大概率是一个大条件结构,Case 0里初始化6321,Case 1里初始化6009,Case 2里初始化串口。然后采集循环里又是一个条件结构,分别调用三种卡的读函数。最后停止清理又是一个条件结构。三个条件结构,分布在不同层级的VI里,每次新增一种硬件,你就要在三个地方同时加Case,漏了一个就是运行时才炸的雷。

这个问题的本质是:对象的创建逻辑和对象的使用逻辑耦合在一起了。你每加一个新产品,就要去翻所有使用产品的地方。如果项目有十个VI在用这个硬件接口,新增硬件就要改十个VI,这种项目谁敢维护?

1.2 工厂模式在LabVIEW中的形态

工厂模式的思想很朴素:你告诉工厂“我要一个什么类型的东西”,工厂把创建细节包下来,返回给你一个现成的对象,你只管用。在LabVIEW的G语言里,这个模式落地会有四个核心角色:

  • 抽象产品(Abstract Product):一个LabVIEW类,定义了接口方法(多态VI)。所有具体产品都继承它。
  • 具体产品(Concrete Product):继承抽象产品的子类,实现各自的接口细节。
  • 工厂(Factory):负责根据输入参数创建具体产品,返回时以父类类型输出。
  • 调用方(Client):只和抽象产品打交道,完全不感知具体子类存在。

在LabVIEW里实现这个模式,有一个利器是其他语言没有的——类描述符常量。你在函数面板的“类”选板里能拖出一个常量,每个类都有自己唯一的类描述符。这个常量可以放进Case Selector里做分支判断,也可以作为输入传递给其他VI。这意味着工厂可以根据“类型”而不是“字符串”或“枚举”来分支,这是LabVIEW里最优雅的工厂实现方式。

1.3 用类替代条件和枚举

有人会问,用枚举加条件结构不也能实现分支吗?为什么要费劲建类?区别在于扩展方式。用枚举,你新增一种硬件就要改枚举定义,然后所有用到这个枚举的条件结构全部要跟着改。用类,你新增一个子类,改的地方只有工厂那一处,所有调用方代码一行都不用动。

实际项目中这个差异有多明显?我带过一个测试系统项目,最初用枚举区分仪器类型,开发到第三个月,新增了第五种仪器,光改调用方就花了整整两天,还引入了两个只在特定配置下出现的Bug。后来在下一个项目里,我直接上了抽象类和工厂模式,同样是加第五种仪器,只花了四十分钟——新增一个子类,改一下工厂的注册表,就结束了,调用方代码从头到尾没碰过。两天对四十分钟,这就是工厂模式在LabVIEW里的价值。

2. LabVIEW OOP基础与工厂模式的关系

想玩转工厂模式,你得先把LabVIEW里“类”的几个关键机制搞透彻。这一节我会把与工厂模式直接相关的OOP特性掰开揉碎讲清楚,这些都是后面实操的地基。

2.1 接口约定:LabVIEW中的“协议”履行之道

面向对象设计里有个概念叫“针对接口编程,而非针对实现编程”。这句话在Java、C#里很直白,就是写一个interface关键字。但在LabVIEW里没有interface这个语法,我们用什么来实现“接口”?

答案是用抽象类。LabVIEW的类可以定义方法是“Abstract VI”,这种VI只有连线板和函数签名(输入输出端子),没有具体实现代码。子类继承抽象类后,必须重写这个VI,提供实际实现。

在LabVIEW里定义抽象方法很简单:右键方法VI,选择“Override VI”—“Abstract”。一旦你把一个方法设为Abstract,这个类在程序运行时不能被直接实例化(不能直接new),只能作为父类被继承。

这就构成了LabVIEW版“接口”的标准做法:

  • 父类里定义所有子类必须实现的方法,全部设为Abstract
  • 方法只定义输入输出接口,不写实现
  • 子类通过“Override”来提供具体实现

在工厂模式的语境里,抽象产品类就是一套接口约定。它规定了“一个产品必须能做什么”,至于每个具体产品怎么做,那是子类的事。调用方只需要知道“我在用一个能采集数据的设备”,完全不需要关心“这个设备是USB的还是PCIe的”。

2.2 类描述符:工厂模式的灵魂机制

LabVIEW的类描述符(Class Control)是最容易被忽视但最强大的OOP机制。你在程序框图上放一个类的常量,它的底层就是一个唯一的类标识符。这个标识符可以参与比较、可以进Case Selector、可以传递到子VI。

为什么这玩意对工厂模式很重要?因为工厂的核心就是“根据类型创建对象”。在其他语言里,你得用字符串或枚举来标识类型,然后在工厂里做映射。LabVIEW里你直接用类描述符本身作为分支条件:

// 伪代码示意,实际是图形化的 // Case Selector 接类描述符常量 // Case "DAQmx" -> 创建DAQmx采集器对象 // Case "Serial" -> 创建串口采集器对象 // Case "Simulator" -> 创建模拟器对象

这样做的好处是,类描述符是编译器级别的标识,不会出现字符串拼写错误;同时它自带继承关系信息,你可以写代码判断“这个类是不是某个类的子类”,这就给动态注册模式提供了基础。

我强烈建议LabVIEW开发者认真研究类描述符。你用好了它,很多高级架构都做得出来。不用它,你永远在跟字符串和枚举搏斗。

2.3 多态VI与动态派发的正确理解

LabVIEW的多态VI(Polymorphic VI)和OOP的动态派发(Dynamic Dispatch)是两码事,但经常被混为一谈。我在这里把两者分清楚,因为工厂模式依赖的是动态派发。

  • 多态VI:同一个VI名,对不同数据类型的输入自动匹配不同实现。这是静态的,编译期就确定了。
  • 动态派发:同一个VI,调用时根据对象的实际类型,在运行时决定执行哪个子类的方法。这是动态的。

工厂模式返回的是一个父类类型的对象,但对象内部的实际类型是子类。你把这个父类引用往动态派发VI的输入端一接,LabVIEW会自己去查这个对象的真实类型,然后调用对应的子类方法。这就是“多态”的威力所在——调用方代码只需要和父类打交道,运行时却自动执行正确的子类实现。

动态派发VI的创建步骤:在类的方法VI上右键,选择“Override”—不再选择“Dynamic Dispatch”。LabVIEW里默认的方法就是动态派发的,除非你特意设置成“Static Dispatch”。静态方法在调用时,绑定的是你接线端子的静态类型,哪怕实际对象是子类,也不会自动向下路由到子类的重写方法。这是新手最容易踩的坑之一。

3. 简单工厂模式在LabVIEW中的实现

理论聊完了,开始动手。这一节我会带你把一个最基础的简单工厂完整做出来。这个案例是数据采集设备创建工厂,场景是:你的程序需要根据配置信息创建不同品牌的采集设备对象,然后所有上层代码只管调用统一接口。

3.1 架构设计:接口、具体类和工厂

我们先定义整个架构的骨架。假设要做一个多设备数据采集系统,支持三种设备:

  1. NI DAQmx设备:用NI官方驱动采集模拟量
  2. 串口采集仪:通过串口Modbus协议读取数据
  3. 虚拟模拟器:内部产生正弦波,用于开发和测试

架构分三层:

  • 抽象产品类“Device.lvclass”:定义所有采集设备的标准接口,方法包括“初始化”、“读取数据”、“关闭”。
  • 三个具体子类:“DAQmxDevice.lvclass”、“SerialDevice.lvclass”、“SimulatorDevice.lvclass”,分别继承Device并实现接口方法。
  • 工厂VI“Create Device.vi”:输入设备类型参数,输出对应的设备对象,返回类型是Device。

调用方(比如主程序或UI层)只跟Device打交道,完全不知道底层到底是哪家设备。

这个设计有一个意外好处:开发阶段用SimulatorDevice,测试逻辑不用碰硬件;现场部署时改成DAQmxDevice或SerialDevice,业务代码完全不动。

3.2 具体类创建:父类接口与子类重写

先在项目里创建一个类,命名为Device.lvclass。在这个类里添加三个方法VI:

  • Initialize.vi:输入设备参数(一个簇,包含通道名、采样率等),输出设备对象、错误簇。
  • Read.vi:输入设备对象、采样点数,输出波形数据(波形数据类型)、错误簇。
  • Close.vi:输入设备对象,输出错误簇。

创建完后,把这三个VI的“VI Setup”——“Category”——“Behavior”——“Override”。

全部设为“Abstract VI”(抽象VI)。这样Device类就成了抽象类,不能直接实例化,只能作为父类。实际上,我在做项目时习惯在抽象类名里加个标识,比如叫“Device_Abstract.lvclass”,这样项目浏览器里一眼就能看出这是抽象类,避免误用。

然后创建三个子类:

  • DAQmxDevice.lvclass:继承Device。Override这三个方法VI,在Initialize里调用DAQmx API,Read里用DAQmx Read,Close里清掉任务。
  • SerialDevice.lvclass:继承Device。Override方法,Initialize里配置VISA串口参数,Read里用VISA Read解析Modbus报文,Close里关闭串口。
  • SimulatorDevice.lvclass:继承Device。Override方法,Initialize里生成正弦波参数,Read里根据时间戳生成不同频率的波形,Close里做空操作。

注意,Override以后,这三个子类的方法VI的图标会被替换成覆盖标识,接线板完全继承父类的定义,不允许修改输入输出端子。这是LabVIEW OOP里非常重要的一条约束:接口一旦由父类定义,所有子类必须遵守相同的接口签名。

3.3 工厂VI的实现:类描述符分支

现在写工厂VI。新建一个Create Device.vi,输入是一个“设备类型”参数,输出是一个Device类型的对象。

设备类型参数用什么数据类型?最直观的方案是用枚举,比如DeviceType枚举里有DAQmxSerialSimulator三个值。这个方案简单直观,适合设备类型基本不变化的项目。

工厂VI内部结构:

  • 输入设备类型枚举,进入条件结构
  • CaseDAQmx:直接new一个DAQmxDevice对象(用“创建对象”函数面板里的类构造器New.vi),输出到输出端子
  • CaseSerial:创建SerialDevice对象
  • CaseSimulator:创建SimulatorDevice对象

输出的接线端定义成Device类型(父类),这样调用方拿到的就是一个“设备”对象,而不是具体的某个子类。

但这里我要给大家一个更高级的方案,直接用类型描述符作为分支条件。这个方案的推荐度更高,因为它的扩展性极好:

把输入改成Device类描述符常量(Device Class Control),条件结构的Case Selector接这个类描述符。添加新设备时,你只需要新加一个Case,拖入新子类的类描述符常量,然后创建对应对象,其他的Case不用动。

用类描述符做分支的最大好处是:LabVIEW的类描述符支持“类”的比较,你甚至可以在分支里判断“输入是不是某个类的子类”,这为后面要讲的注册式动态工厂埋下了伏笔。

3.4 调用方代码的干净利落

调用方代码简单得令人舒适:

  1. 根据配置参数,把设备类型信息(枚举、类描述符或者字符串都能转换)传给Create Device.vi
  2. 拿到Device对象
  3. 直接调用Initialize.viRead.viClose.vi

你需要关注的只有Device类定义的接口方法。整个调用方代码里没有任何条件结构,没有Switch Case,完全是对Device对象的操作。

此时如果你再增加一个新的设备类型,需要做的只有:

  1. 新增一个子类继承Device
  2. 在工厂VI里新增一个Case

调用方代码一行都不用改。这在需求多变的项目里能省下多少返工时间,试过的人都知道。

4. 实际案例:从零搭建一个多设备波形采集系统

为了让你看得更明白,我干脆把我在一个数据采集项目里实际做过的东西完整拆解一遍。这个案例用的是前面讲的方案,但我会把创建类和VI的实际操作步骤、几个关键参数怎么设、调用链怎么走,全部过一遍。

4.1 项目需求与类设计

需求背景:要给一个产线写一个波形采集与显示系统,采集振动传感器信号。产线上有两种硬件方案,一种是用NI的cDAQ机箱配NI 9234模块采集,另一种是给客户的老设备加了一个串口转发模块,用Modbus RTU协议吐数据。开发阶段用模拟器生成数据。

类层次设计:

  • WaveformDevice.lvclass(抽象产品)

    • Configure.vi:输入通道名(波形)、采样率(S/s)、采样点数,输出设备对象、错误簇
    • Fetch.vi:输入设备对象、每次读取的点数,输出一维波形数组、错误簇
    • Close.vi:输入设备对象,输出错误簇
  • DAQmxWaveformDevice.lvclass(具体产品A)

    • Configure里用DAQmx Create Virtual Channel、Timing、Start Task
    • Fetch里用DAQmx Read
    • Close里Clear Task
  • ModbusWaveformDevice.lvclass(具体产品B)

    • Configure里用VISA串口配置函数,发送Modbus命令
    • Fetch里用VISA Read,按照Modbus寄存器解析成波形数据
    • Close里关闭VISA会话
  • SimWaveformDevice.lvclass(具体产品C)

    • Configure里保存采样率,产生初始化数据
    • Fetch里利用当前时间生成不同频率的正弦波、方波、三角波
    • Close里什么都不用做,但必须重写方法,保持接口一致

4.2 一步步创建抽象类和抽象方法

打开LabVIEW,新建项目,右键项目树,选择“新建”—“类”,命名为WaveformDevice.lvclass

在这个类里新建三个方法VI,都保持默认的动态派发设置。然后把这些VI的“Behavior”改为“Abstract”:

  1. 双击打开Configure.vi,在VI属性里选“类别”中的“行为”,勾选“覆盖”—“抽象方法”
  2. 连接板设定输入输出:设备参数簇(自定义类型)、采样率(双精度)、采样点数(整数)、错误输入/输出,输出是一个设备引用(接线到设备类)
  3. 保存并关闭

注意,抽象VI的连线板必须被完整定义,而且这个VI不会包含框图代码。它的作用是“占位”——告诉所有的子类:你们必须实现一个叫Configure.vi的VI,输入输出跟我定义的一模一样。

然后创建三个子类:右键项目—“新建”—“类”,在“父类”下拉里选择WaveformDevice.lvclass。创建完毕后,你会看到子类里自动出现三个灰色的方法VI,它们就是继承来的抽象方法。双击它们,LabVIEW会提示你是“调用父类方法”还是“覆盖方法”,在这里选择“覆盖”并创建新的实现。

这样每个子类就有一套自己的Configure、Fetch、Close实现了。

4.3 工厂VI的完整代码逻辑

新建一个VI,命名为Create Waveform Device.vi,接线板设置:

  • 输入:设备标识符(字符串或枚举,比如“DAQmx”“MODBUS”“SIM”),采样率(双精度)、通道名(字符串)
  • 输出:设备对象(WaveformDevice类型)、错误簇

VI内部框图逻辑(伪代码风格描述):

  • 使用条件结构,选择器接设备标识符
  • Case “DAQmx”:调用DAQmxWaveformDevice.lvclass的构造器(右键类名选择“新建”—“VI”来创建构造VI,或者使用类私有数据初始化),输出连接到一个WaveformDevice类型的输出端子
  • Case “MODBUS”:创建ModbusWaveformDevice对象
  • Case “SIM”:创建SimWaveformDevice对象
  • 错误处理:如果输入标识符无法匹配,生成错误并输出空对象

这里有个关键操作:三个Case的输出都连到同一个输出端子,这个端子的类型设定为WaveformDevice。LabVIEW会自动做向上类型转换(upcast),子类对象赋值给父类类型的端子,完全没有问题。

4.4 调用方主程序集成

主程序是一个生产者/消费者架构,UI线程(生产者)读面板上的按钮事件,采集线程(消费者)跑采集循环。

  • 启动事件里,根据INI文件里的配置决定用哪种设备,调用Create Waveform Device.vi,拿到WaveformDevice对象
  • 把对象通过Shift Register传给采集循环
  • 采集循环里每轮调用Fetch.vi,把获取的波形送到波形图控件更新
  • 停止时调用Close.vi,然后清空移位寄存器

这套代码的爽点在于:切换硬件时,你只改INI文件里的配置字符串,程序重新启动后自动用新设备,主程序、采集循环、UI代码一行不改。连“设备是否连接”的判断都不用写,因为在初始化阶段如果设备不存在,Configure.vi会通过错误簇往外抛错。

4.5 典型耗时与调试经验

这部分是我最想分享的实操经验。第一次做这套架构时,我预估花两天,结果用了一个星期。卡壳点全在一些小的细节上。

一是抽象方法没设对。我最初把Configure.vi设为普通动态派发方法,而不是Abstract,这导致设备类可以被直接实例化,整个架构就变味了。后来改成Abstract,LabVIEW会在任何地方阻止直接创建父类对象,编译器就把这种错误挡在门外。这个约束反而是一种保护。

二是子类构造器的位置。LabVIEW没有自动生成“New.vi”的习惯,你要自己创建。很多新手会直接new子类然后给私有数据赋值,这是可以的,但更好的做法是让工厂VI负责构造并调用初始化方法,这样调用方永远不会看到“半个初始化”状态的对象。

三是错误簇的传递。工厂VI里如果设备创建失败(比如串口打开失败),错误簇要干净地往上传。我见过有人把错误簇吞掉只输出一个空对象,导致后面调用方法时产生“无效对象”错误,排查半天才发现是工厂那里埋了坑。

四是内存管理。LabVIEW是自动内存管理的,但你掌握不住引用生命周期时,容易在循环里反复创建对象而不清理。生产者消费者架构里,采集线程用的设备对象必须在停止事件里显式调用Close,否则下次启动时硬件资源可能还被占着。在实际调试中,我曾因为没关VISA会话,导致第二次启动时串口报“资源已被占用”,这个问题就是工厂模式解耦后,调用方责任边界不清楚导致的。谁创建,谁释放是铁律,工厂创建对象,调用方用完负责调用关闭方法,这个约定要在项目规范里写清楚。

5. 从简单工厂到抽象工厂与注册式工厂

如果你只学一个模式,那简单工厂基本够了。但LabVIEW项目一旦变大,简单工厂开始力不从心,这时候该往高级模式进阶了。

5.1 抽象工厂模式:一套设备,多个产品

抽象工厂解决的是“产品族”问题。比如你要给测试系统配“数据采集设备”和“信号发生器设备”,这两个设备在物理上属于同一家供应商的同一系列,但接口不同。你希望用同一个配置项(比如“设备品牌A”)一键创建整套配置,而不是分别管两个工厂。

落地方法是创建两个抽象产品类:AcquisitionDevice.lvclassSignalGeneratorDevice.lvclass,然后创建一个抽象工厂类DeviceFactory.lvclass,它定义两个抽象方法:CreateAcquisition.viCreateSignalGenerator.vi

每个具体厂商(NI、串口设备、模拟器)继承工厂类,在Override方法里返回对应的产品对象。调用方只依赖DeviceFactory.lvclass,你给它哪一个工厂子类,它就给你生产整套配套设备。

这个模式在LabVIEW里的实现并不复杂,真正的难点是命名规范。我建议工厂类命名以Factory结尾,产品类命名体现其角色,多个类的项目里一定要用项目库(Library)管理,避免同名VI互相污染。

5.2 注册式工厂(依赖注入)

有一种更灵活的做法,是让工厂不再硬编码Case分支,而是靠“注册表”——运行时把“类描述符”和“创建函数”注册到工厂里。

这个模式在LabVIEW里能实现的核心原因是:LabVIEW支持通过类描述符在运行时调用类的构造VI。你可以用Call by Reference节点,把一个VI的引用传给工厂,工厂在运行时根据注册表创建对象。

注册式工厂的好处是,新增设备时你不需要改工厂VI本身,只需要在项目启动代码里多一行注册调用。这对大型项目、插件架构、以及多个团队并行开发时特别有价值。LabVIEW做不到Java的注解和类自动扫描,但已经可以做到通过一个全局注册VI来集中登记所有可用产品。

我做过的项目中,有一个平台的采集插件从10种扩展到了40多种,靠的就是注册式工厂。关于实现细节,你需要研究LabVIEW的“VI Scripting”或“Call by Reference”功能,把创建VI的引用存储在全局数据里,在工厂需要时动态调用。

5.3 工厂模式与Actor Framework的搭配

有LabVIEW经验的开发者肯定知道Actor Framework(AF),它是NI官方的消息传递架构。很多人觉得AF跟设计模式是两条路,但我在实际项目中经常把工厂模式用在AF里。

具体场景是:AF里有一个“设备管理Actor”,它负责创建和管理所有硬件设备子Actor。原来我在这个Actor的“启动”方法里用条件结构判断配置,决定start哪个子Actor。后来我改成设备子Actor用工厂模式创建:

  • 定义一个“硬件设备Actor”抽象类
  • 各种硬件对应一个继承类
  • 在父Actor的启动方法里,调用工厂VE获取具体设备Actor,然后发送启动消息

这个组合的好处是,AF的Actor消息机制和工厂模式的对象创建机制互补:工厂负责“产生合适的Actor实例”,AF负责“管理Actor的生命周期和消息路由”。两个模式一起用,系统扩展起来几乎不费力气。

6. 常见问题与排查技巧实录

所有模式在LabVIEW里落地的过程中,都会遇到一堆奇怪的问题。我把这些年做工厂模式时踩过的坑整理成一个排查表,这些经验比任何理论讲解都值钱。

问题现象根本原因排查思路解决方案
调用子类方法时执行了父类代码方法VI设为“静态派发”而不是“动态派发”打开子类的方法VI,右键查看“属性”—“行为”把静态方法改为动态派发,或重新Override
工厂输出对象无法连到子类特有方法输出端子的静态类型是父类检查工厂VI输出端子的数据类型在调用方用“To More Specific Class”转换节点向下转型
创建对象时报“类为抽象类,无法实例化”直接new了一个抽象类检查调用的是哪个类的构造器在工厂里创建具体子类对象,再向上转型输出
重复创建设备导致第二次初始化失败对象资源未释放查找是否在停止时调用Close在生产者消费者架构里,把关闭动作绑定在停止事件里
工程文件里一堆同名VI互相覆盖类的方法VI未用项目库管理检查项目树里是否有重名VI所有类方法放在一个Library里,或用LVLIB管理
程序运行很久后才报“找不到方法”类文件路径变化或类名冲突查看调用链中是否存在模糊引用的VI梳理项目文件路径,删除多余的旧类副本

6.1 动态派发还是静态派发,分清楚省一天时间

动态派发是LabVIEW OOP多态的基石。如果你在子类里重新实现了一个方法,却发现调用的似乎还是父类的版本,九成原因是这个方法被设置成了静态派发。

区分方法很简单:右键方法VI—属性—类别—行为,如果显示“静态派发”,运行时就是按接线端的静态数据类型来绑定方法。把抽象产品类型接线端传入,调用的就是父类的方法,即便对象内部是子类也白搭。

6.2 类描述符比较与错误分支处理

在注册式工厂里,类描述符的比较是常见操作。LabVIEW的类描述符常量可以直接比较是否相等,也可以和“类名”字符串互相转换。这里我提醒两个坑:

第一,不要用类名做分支。类名是字符串,万一项目重构时改了类名(我干过这蠢事),分支会全部失效。用类描述符常量做Case Selector,类名怎么改都没问题。

第二,工厂VI里要预留“未知类型”的处理分支。输入一个不在注册表里的类型时,输出一个空对象加错误,不要直接宕机。这个习惯在调试时能让你立刻定位到“配置写错了”,而不是整个程序卡死。

6.3 可维护性怪圈:因为“弹性”所以更好维护

工厂模式带来的最实际价值是代码的修改范围可控。没有工厂模式时,加一种设备要改多个VI;有了工厂模式,加一种设备只改新增类和工厂注册两处。在团队协作里,这个价值还会放大:张三负责新增设备类,李四负责工厂注册,调用方代码不受影响,两个人互不干扰。

KPI型的老板可能会问:工厂模式多写了好多类,是不是过度设计?我的回答是:类数量多了,但每个类的代码量和复杂度直线下降,整体可维护性完全是两个量级。项目规模越小,工厂模式的收益越不明显,但如果你的LabVIEW程序生命周期超过半年,或预计会有需求变更,工厂模式就是刚性需求。

7. 动手练一练:把现有代码改造成工厂模式

说了这么多,最后给你一个可以周末练手的改造练习。我假设你手头有一个“用条件结构区分设备类型”的老项目,改造步骤是这样:

7.1 第一步:识别变化点

先找出程序中所有按设备类型分支的条件结构。每个条件结构都是潜在的变化点。记录下分支条件(枚举值或字符串)和每个分支里做的事情。一般来说会出现这几个规律:

  • 初始化阶段的分支(如何配置硬件)
  • 数据读写阶段的分支(如何获取数据)
  • 清理阶段的分支(如何释放资源)

这三个阶段对应的就是抽象产品类的三大方法。

7.2 第二步:抽象类和子类的提取

根据第一阶段找出的变化点,定义抽象产品类的方法。把每个分支里的代码复制到对应的子类方法实现里。遇到分支里无法复用的部分(比如某分支特有状态变量),就把它们变成子类的私有数据。

这一步最耗时间,但也最能锻炼你对类的设计能力。我的经验是,先不要追求一次成型,先做到代码能跑,然后逐步提炼,比一次性设计完美架构效率高得多。

7.3 第三步:引入工厂,替换调用点

完成子类实现后,写工厂VI,把原本的条件结构替换成工厂调用。改动顺序很重要:先从调用次数最少的地方开始替换,一步步扩大范围,每替换完一个点就运行一次测试,确保不破坏已有功能。

全部替换完后,你会看到调用方代码里不再有设备类型的条件分支,只剩对抽象产品方法的调用。这时候再回头看,你就能直观感受到“面向对象”和“面向需求写死代码”的区别。

我在改造完第一个项目后,自己都惊讶于调用方代码的简洁程度。以前翻代码要翻半天,现在一个子类一个工厂,结构清清楚楚。

7.4 移植经验:工厂模式不是终点

工厂模式不是解决所有问题的银弹。它是你在LabVIEW面向对象之路上的一站。下一步你可以去研究更有意思的话题:观察者模式(UI和数据采集解耦)、状态模式(设备状态机)、策略模式(不同算法切换)。这些模式会在你掌握工厂模式后变得异常顺手,因为它们都建立在“多态和接口约定”之上。

LabVIEW开发者经常被误解为“写不了复杂软件”,但用过LabVIEW OOP和设计模式的人都知道,这套组合拳打好了,代码的可维护性、扩展性完全能跟文本语言掰手腕。工厂模式就是你把工具从“能用”升级到“好用”的关键一步。

最后分享一个真实感言:我见过太多LabVIEW项目死在“代码没人敢改”上。接手的人打开密密麻麻的条件结构就头大,每改一个分支都怕影响其他逻辑。而用了工厂模式之后,代码从“迷宫”变成“一组积木”,哪个积木坏了换哪个,替换积木不影响其他部分。这是LabVIEW工程化道路上非常值得投入的一件事,强烈推荐所有用LabVIEW做中大型项目的人认真搞一搞。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询