PLC结构化文本设计模式——单例模式(Singleton Pattern)
PLC Structured Text Design Patterns
PLC结构化文本设计模式——单例模式(Singleton Pattern)
介绍
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
1. 确保一个类只有一个实例;
2. 提供一个全局访问点来访问该实例。
考虑到PLC结构化文本面向对象编程的特殊性,上述第2点应用在PLC编程上可能并没有什么意义(作用),后续将会解释为什么。
使用场景
在实际自动化设备项目中,常常会需要设计一个类(FB)封装一些功能,并且在整个系统中只有一个实例。比如:设备管理器、报警管理器、轴管理器、IO管理器等。一般的做法就是将类声明(创建)在全局变量列表或者PRG程序里,由其它任意位置直接通过全局变量限定路径访问(直接调用)。
当然上述场景基本上是由PLC工程师人为控制类的实例数量,整个PLC程序各个细节只有当时写的人清楚,但是如果换其它不太了解该程序的人维护/升级,可能会重复创建实例。PLC程序的可读性和可维护性很差,或许后续人员花点时间就可以接手,但是为什么不在程序设计之初避免这些可能存在代码风险的情况呢?
已经尽可能从PLC应用的角度介绍了,毕竟之前也是做过PLC的,所以相对比较了解PLC程序设计思维(怎么简单怎么来)。但是一旦遇到那些系统性的项目(设备),尤其自动化设备包含工艺的,这样一来需要根据客户工艺做非常多的功能。长此以往随着程序的增加,后续维护/升级都是很痛苦的,可能做着做着自己都写不下去(严重的直接提桶跑路)。
优缺点
-
- 优点
程序运行内存中只有一个实例,减少内存开销。
PLC与高级语言稍有些不同,PLC程序(ST)一般只有变量声明区创建(声明)过的变量实例才能在程序中使用,不会频繁的创建对象和销毁对象。PLC的基本要求就是稳定,频繁创建/销毁对象,可能导致内存溢出或指针空引用,程序异常停机,因此内存开销在程序运行之初基本已经确定,这方面完全由PLC研发人员决定。
避免资源的多重占用
因为PLC不同于上位机,存在扫描周期,即使创建多个Task任务也只是根据任务优先级循环扫描周而复始。
-
- 缺点
该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
高级编程语言中可能存在的问题,PLC运行属于单线程,不会存在以上情况。
实现方式
以下是菜鸟教程Java示例:
public class SingleObject {
//创建 SingleObject 的一个对象
private static SingleObject instance = new SingleObject();
//让构造函数为 private,这样该类就不会被实例化
private SingleObject(){}
//获取唯一可用的对象
public static SingleObject getInstance(){
return instance;
}
public void showMessage(){
System.out.println("Hello World!");
}
}
-
- 在类中添加一个私有静态成员变量用于保存单例实例。
PLC结构化文本实现方式稍有不同,由于需要使用
__New()创建对象指针,所以私有静态成员变量类型也为类的指针类型。
-
- 声明一个公有静态构建方法用于获取单例实例。
这里和上面提到的
提供一个全局访问点来访问该实例是同一个意思,所以这点在PLC编程上可能没什么太大意义。
-
- 将类的构造函数设为私有。类的静态方法仍能调用构造函数, 但是其他对象不能调用。
可惜的是FB的构造函数(内置方法
FB_init)无法声明为Private私有类型,所以这种方式行不通,得采取其它实现方式。
内置方法设为Private强制报错:The access to the methods "FB_Init", "FB_Exit" and "FB_ReInit" must be PUBLIC。系统明确这三个内置方法必须是Public。
伪代码
伪代码以FB_AlarmManager报警管理器为例,FB_AlarmManager和FB_AlarmManagerProxy共同搭配实现了单例模式。
INTERFACE I_Interface EXTENDS __SYSTEM.IQueryInterface
创建报警管理器接口继承I_Interface,包含两个方法和两个属性。
INTERFACE I_AlarmManager EXTENDS I_Interface
------
METHOD CheckForAlarms : HResult
VAR_INPUT
END_VAR
------
METHOD CheckForWarnings : HResult
VAR_INPUT
END_VAR
------
PROPERTY IsFaulted : BOOL
Get()
Set()
------
PROPERTY IsWarningActived : BOOL
Get()
Set()
创建FB_AlarmManager实现I_AlarmManager接口,为FB添加特性{attribute 'enable_dynamic_creation' := ''}该特性是为了使用__New动态创建对象,详细资料请查询官网。
{attribute 'enable_dynamic_creation' := ''}
FUNCTION_BLOCK FB_AlarmManager IMPLEMENTS I_AlarmManager
VAR
_IsFaulted : BOOL;
_IsWarningActived : BOOL;
END_VAR
------
METHOD CheckForAlarms : HResult
// 检查报警逻辑
------
METHOD CheckForWarnings : HResult
// 检查警告逻辑
------
PROPERTY IsFaulted : BOOL
Get:
IsFaulted := THIS^._IsFaulted;
Set:
THIS^._IsFaulted := IsFaulted;
ADSLOGSTR(msgCtrlMask :=ADSLOG_MSGTYPE_LOG, msgFmtStr :='[IsFaulted]:Write value to logs: %s', strArg := TO_STRING(THIS^._IsFaulted));
------
PROPERTY IsWarningActived : BOOL
Get:
IsWarningActived := THIS^._IsWarningActived;
Set:
THIS^._IsWarningActived := IsWarningActived;
ADSLOGSTR(msgCtrlMask :=ADSLOG_MSGTYPE_LOG, msgFmtStr :='[IsWarningActived]:Write value to logs: %s', strArg := TO_STRING(THIS^._IsFaulted));
这里使用了代理模式,FB_AlarmManagerProxy代理类同样实现I_AlarmManager接口,与FB_AlarmManager由同样的功能并在此基础上扩展一个Cyclic方法用于循环扫描。
代理模式:结构型设计模式, 能够提供对象的替代品或其占位符。代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。其余章节会详细介绍该设计模式。
代理类
FB_AlarmManagerProxy局部声明了静态pAlarmManager指针类型变量。静态变量(Static Variable)是属于类本身而不是类的实例的变量。这意味着无论创建多少个类的实例,静态变量都只有一份副本。根据静态变量的特点,代理类FB_AlarmManagerProxy无论声明(创建)多少个实例,静态变量成员pAlarmManager都只有一个。
FUNCTION_BLOCK FB_AlarmManagerProxy IMPLEMENTS I_AlarmManager
VAR_STAT
bInitalize : BOOL; // 初始化Flag
pAlarmManager : POINTER TO FB_AlarmManager; // 报警管理器指针
END_VAR
------
METHOD CheckForAlarms : HResult
pAlarmManager^.CheckForAlarms();
------
METHOD CheckForWarnings : HResult
pAlarmManager^.CheckForWarnings();
------
METHOD Cyclic : HResult
THIS^.CheckForAlarms();
THIS^.CheckForWarnings();
------
METHOD FB_exit : BOOL
VAR_INPUT
bInCopyCode : BOOL; // if TRUE, the exit method is called for exiting an instance that is copied afterwards (online change).
END_VAR
// 程序退出时,释放(删除)指针
IF THIS^.bInitalize OR_ELSE pAlarmManager <> 0 THEN
__DELETE(pAlarmManager);
ADSLOGSTR(msgCtrlMask :=ADSLOG_MSGTYPE_LOG, msgFmtStr :='[FB_exit]:Function block disposed', strArg := '');
END_IF
------
METHOD FB_init : BOOL
VAR_INPUT
bInitRetains : BOOL; // if TRUE, the retain variables are initialized (warm start / cold start)
bInCopyCode : BOOL; // if TRUE, the instance afterwards gets moved into the copy code (online change)
END_VAR
// 构造函数初始化时判断,指针不为空,即为已经创建完对象。
IF THIS^.bInitalize OR_ELSE pAlarmManager <> 0 THEN
// 此处无需做任何处理
ELSE
// 程序运行,若未初始化或指针为空,创建FB_AlarmManager实例
pAlarmManager := __NEW(FB_AlarmManager);
THIS^.bInitalize := TRUE;
END_IF
------
PROPERTY IsFaulted : BOOL
Get:
IsFaulted := pAlarmManager^.IsFaulted;
Set:
pAlarmManager^.IsFaulted := IsFaulted;
------
PROPERTY IsWarningActived : BOOL
Get:
IsWarningActived := pAlarmManager^.IsWarningActived;
Set:
pAlarmManager^.IsWarningActived := IsWarningActived;
主程序调用,声明/创建3个FB_AlarmManagerProxy实例,测试单例模式是否生效。
Tips:函数ADSLOGSTR是倍福Twincat3 PLC库官方的日志函数,可将信息输出到输出窗口。
PROGRAM MAIN
VAR
// 创建(声明)三个报警管理器实例,主要用于测试报警管理器是否为同一实例。
fbAlarmManagerProxy1 : FB_AlarmManagerProxy;
fbAlarmManagerProxy2 : FB_AlarmManagerProxy;
fbAlarmManagerProxy3 : FB_AlarmManagerProxy;
bTest1 : BOOL;
bTest2 : BOOL;
bTest3 : BOOL;
END_VAR
fbAlarmManagerProxy1.Cyclic();
IF bTest1 THEN
bTest1 := FALSE;
fbAlarmManagerProxy1.IsFaulted := TRUE;
fbAlarmManagerProxy1.IsWarningActived := TRUE;
END_IF
IF bTest2 THEN
bTest2 := FALSE;
fbAlarmManagerProxy2.IsFaulted := FALSE;
fbAlarmManagerProxy2.IsWarningActived := FALSE;
END_IF
IF bTest3 THEN
bTest3 := FALSE;
fbAlarmManagerProxy3.IsFaulted := TRUE;
fbAlarmManagerProxy3.IsWarningActived := TRUE;
END_IF
依次给bTest1、bTest2、bTest3置True,查看日志输出结果。
[IsFaulted]:Write value to logs: TRUE
[IsWarningActived]:Write value to logs: TRUE
[IsFaulted]:Write value to logs: FALSE
[IsWarningActived]:Write value to logs: FALSE
[IsFaulted]:Write value to logs: TRUE
[IsWarningActived]:Write value to logs: TRUE
显然只输出了3次,在线监控对应的变量状态也都是同步变更的,单例模式测试成功。
Tips:这里强调一下,示例中的fbAlarmManagerProxy1、fbAlarmManagerProxy2、fbAlarmManagerProxy3这3个变量不是同一个实例,而是指针(地址)不同的对象实例,之所以实现所谓的
单例是因为类FB_AlarmManagerProxy内部pAlarmManager以静态变量方式声明的。
问题解答
问题1:为什么仿照Java示例,在
FB_AlarmManager内静态变量生命区直接声明本身类型的静态变量?这样就不需要FB_AlarmManagerProxy代理类代为执行FB_AlarmManager功能。
{attribute 'enable_dynamic_creation' := ''}
FUNCTION_BLOCK FB_AlarmManager IMPLEMENTS I_AlarmManager
VAR
_IsFaulted : BOOL;
_IsWarningActived : BOOL;
END_VAR
VAR_STAT
fbAlarmManager : FB_AlarmManager;
END_VAR
回答1:PLC与高级语言不太一样,可以自己尝试声明一下,编译会进入死循环,一直处于编译过程中。
问题2:既然上面声明类型变量编译不通过,为什么不直接声明成指针类型的变量?在
FB_AlarmManager构造函数内创建pAlarmManager指针。这样就不需要FB_AlarmManagerProxy代理类代为执行FB_AlarmManager功能。
{attribute 'enable_dynamic_creation' := ''}
FUNCTION_BLOCK FB_AlarmManager IMPLEMENTS I_AlarmManager
VAR
_IsFaulted : BOOL;
_IsWarningActived : BOOL;
END_VAR
VAR_STAT
pAlarmManager : POINTER TO FB_AlarmManager;
END_VAR
------
METHOD FB_init : BOOL
VAR_INPUT
bInitRetains : BOOL; // if TRUE, the retain variables are initialized (warm start / cold start)
bInCopyCode : BOOL; // if TRUE, the instance afterwards gets moved into the copy code (online change)
END_VAR
IF pAlarmManager <> 0 THEN
//
ELSE
pAlarmManager := __NEW(FB_AlarmManager);
END_IF
// 额外添加一个方法获取指针实例
METHOD GetInstance : POINTER TO FB_AlarmManagerV1
VAR_INPUT
END_VAR
GetInstance := pAlarmManager;
PROGRAM MAIN
VAR
// 创建(声明)三个报警管理器实例,主要用于测试报警管理器是否为同一实例。
fbAlarmManager1 : FB_AlarmManager;
fbAlarmManager2 : FB_AlarmManager;
fbAlarmManager3 : FB_AlarmManager;
bTest1 : BOOL;
bTest2 : BOOL;
bTest3 : BOOL;
END_VAR
IF bTest1 THEN
bTest1 := FALSE;
alarmManager1.GetInstance()^.IsFaulted := TRUE;
alarmManager1.GetInstance()^.IsWarningActived := TRUE;
END_IF
IF bTest2 THEN
bTest2 := FALSE;
alarmManager2.GetInstance()^.IsFaulted := FALSE;
alarmManager2.GetInstance()^.IsWarningActived := FALSE;
END_IF
IF bTest3 THEN
bTest3 := FALSE;
alarmManager3.GetInstance()^.IsFaulted := TRUE;
alarmManager3.GetInstance()^.IsWarningActived := TRUE;
END_IF
回答2:编译可以通过,程序可以正常运行,但是同样会报错内存方面的错误,严重时电脑会直接蓝屏,Win11是绿屏。
问题3:既然静态变量在系统中只有唯一副本,那么为什么不把类的成员变量全部定义为静态变量成员?这样也可以实现单例类,而且不需要使用代理类进行包装。
{attribute 'enable_dynamic_creation' := ''}
FUNCTION_BLOCK FB_AlarmManagerV1 IMPLEMENTS I_AlarmManager
VAR_STAT
_IsFaulted : BOOL;
_IsWarningActived : BOOL;
// 其它局部变量
// ...
END_VAR
回答3:从实现效果来说,这样当然可以。可有没有想过,类内部全是静态成员,若类内部比较庞大,静态变量声明区会变得很庞大,而且无法保证类内部成员是否一定需要静态变量。最主要的是违背了
单例模式的初衷/定义,确保一个类只有一个实例,需要明白的是类的唯一性,而不是成员的唯一性。代理类FB_AlarmManagerProxy内部确保了FB_AlarmManager实例的唯一,并且FB_AlarmManager内部成员可根据实际需要定义各种类型的成员变量。
问题4:最后解释一下
提供一个全局访问点来访问该实例,这个在实现在PLC编程是否有意义?
// 为FB_AlarmManagerProxy代理类添加方法GetInstance
METHOD GetInstance :FB_AlarmManagerProxy
VAR_INPUT
END_VAR
// 返回FB_AlarmManagerProxy当前对象实例
GetInstance := THIS^;
或者
// 为FB_AlarmManagerProxy代理类添加方法GetInstance
METHOD GetInstance : POINTER TO FB_AlarmManagerProxy
VAR_INPUT
END_VAR
// 返回FB_AlarmManagerProxy当前对象实例
GetInstance := THIS;
或者
// 为FB_AlarmManagerProxy代理类添加方法GetInstance
METHOD GetInstance : I_AlarmManager
VAR_INPUT
END_VAR
// 返回FB_AlarmManagerProxy当前对象实例
GetInstance := THIS^;
或者
// 为FB_AlarmManagerProxy代理类添加方法GetInstance
METHOD GetInstance :REFERENCE TO FB_AlarmManagerProxy
VAR_INPUT
END_VAR
// 返回FB_AlarmManagerProxy当前对象实例
GetInstance := THIS^;
// 或者
// GetInstance REF= THIS^;
回答4:当然不管上述哪种方法都是可以实现
提供一个全局访问点来访问该实例,与高级语言C#或Java等不同的是PLC类型变量只有在变量声明区创建才能使用,那么既然FB_AlarmManagerProxy中的FB_AlarmManager已经实现单例了,在任何地方都可直接声明使用,那还有必要设计GetInstance方法返回自身实例吗?
问题5:PLC应用工程师学习常见的夺命三连问:(面向对象OOP,设计模式Design Patterns) 1.有什么用?2.有什么优势?3.有什么好处?
回答5:没什么用,没什么优势,没什么好处,可以不学。

浙公网安备 33010602011771号