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程序设计思维(怎么简单怎么来)。但是一旦遇到那些系统性的项目(设备),尤其自动化设备包含工艺的,这样一来需要根据客户工艺做非常多的功能。长此以往随着程序的增加,后续维护/升级都是很痛苦的,可能做着做着自己都写不下去(严重的直接提桶跑路)。

优缺点

    1. 优点

    程序运行内存中只有一个实例,减少内存开销。

    PLC与高级语言稍有些不同,PLC程序(ST)一般只有变量声明区创建(声明)过的变量实例才能在程序中使用,不会频繁的创建对象和销毁对象。PLC的基本要求就是稳定,频繁创建/销毁对象,可能导致内存溢出或指针空引用,程序异常停机,因此内存开销在程序运行之初基本已经确定,这方面完全由PLC研发人员决定。

    避免资源的多重占用

    因为PLC不同于上位机,存在扫描周期,即使创建多个Task任务也只是根据任务优先级循环扫描周而复始。

    1. 缺点

    该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。

    高级编程语言中可能存在的问题,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!");
   }
}
    1. 在类中添加一个私有静态成员变量用于保存单例实例。

PLC结构化文本实现方式稍有不同,由于需要使用__New()创建对象指针,所以私有静态成员变量类型也为类的指针类型

    1. 声明一个公有静态构建方法用于获取单例实例。

这里和上面提到的提供一个全局访问点来访问该实例是同一个意思,所以这点在PLC编程上可能没什么太大意义。

    1. 将类的构造函数设为私有。类的静态方法仍能调用构造函数, 但是其他对象不能调用。

可惜的是FB的构造函数(内置方法FB_init)无法声明为Private私有类型,所以这种方式行不通,得采取其它实现方式。

内置方法设为Private强制报错:The access to the methods "FB_Init", "FB_Exit" and "FB_ReInit" must be PUBLIC。系统明确这三个内置方法必须是Public。

伪代码

伪代码以FB_AlarmManager报警管理器为例,FB_AlarmManagerFB_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

依次给bTest1bTest2bTest3True,查看日志输出结果。

[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:这里强调一下,示例中的fbAlarmManagerProxy1fbAlarmManagerProxy2fbAlarmManagerProxy3这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:没什么用,没什么优势,没什么好处,可以不学。

posted @ 2025-09-12 15:31  J_Sheng  阅读(51)  评论(2)    收藏  举报