文章目录
- 一、前言
- 二、上位机单例应用场景
-
- [2.1 上位机](#2.1 上位机)
- [2.2 单例及其应用](#2.2 单例及其应用)
- [2.3 上位机中的应用](#2.3 上位机中的应用)
-
- [2.3.1 用户登录信息](#2.3.1 用户登录信息)
- [2.3.2 配置文件](#2.3.2 配置文件)
- [2.3.3 数据连接池](#2.3.3 数据连接池)
- [2.4 一个应用场景的思考](#2.4 一个应用场景的思考)
- 三、总结
一、前言
之前写过一篇关于单例的文------C#中单例模式的实现,讲了讲单例是什么以及在C#中的常见代码实现,那篇文的内容偏理论,并不实用。
最近在用WPF写上位机,发现我在实际开发中使用单例时,并不关心其底层实现,也不太会出现这样的单例类代码:
csharp
using System;
public sealed class Singleton
{
private static volatile Singleton instance;
private static object syncRoot = new Object();
private Singleton() {}
public static Singleton Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
}
而往往是创建一个服务容器(ServiceProvider),然后把想要实现单例的类以单例模式加入其中,并将服务容器公开(通常是放在App类中)以使整个程序代码都能访问之,在想要用到该单例时,从容器中取出即可:
csharp
public IServiceProvider Services { get; }
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<Class>();
...
return services.BuildServiceProvider();
}
如果你还没有用过这种容器的方式,可能会觉得很麻烦;而一旦接受了这种方式,你会发现它变成了一种定式。几乎所有应用程序都可以这么做(服务容器的这种方式本身也是一种设计模式Ioc)。
这些内容不是本文要讲的东西,本文主要想讲讲上位机程序中单例的应用,以及一个场景该怎样使用单例的思考。
二、上位机单例应用场景
2.1 上位机
先提一下上位机,
上位机通常不是什么庞大的程序,它主要用以:
- 提供界面,用户可友好操作;
- 与下位机通讯,将采集的数据加工,并呈现在界面上;
- 将部分数据存储至数据库,以供报表、查询、统计分析;
- 与更上层的系统(MES、ERP等)进行对接;
- 可能还会结合一些专业技术(如视觉、文档处理等)辅助生产。
这样一个体量不大的用于专门设备的程序,其涉及的技术还是挺广的。
2.2 单例及其应用
单例的目的是为了保证一个类在程序中只有一个实例,并提供一个访问它的全局访问点。
很明显,单例这样的设计使一个类只有一个实例,并且要易于外界访问,从而方便对实例进行控制并节约系统资源。
因此,它的应用场景通常为:
- 有频繁实例化(也就是频繁new)然后销毁的情况;
- 创建对象耗时过多或者耗资源过多的情况;
- 频繁访问IO资源的对象,如数据库连接池或文件。
我相信很多人第一次使用单例并不是因为性能的问题,而仅仅想要一个类似于C语言中全局变量的东西,希望有一个类的实例能被不同页面的代码访问到。这其实就是单例中提到的提供全局访问点的特性。
这边有一个大家非常熟悉的应用------Windows上的任务管理器。ctrl + shift + esc打开,并且无论你按多少次,都只会出现一个任务管理器。也就是说,在Windows系统这个程序中,任务管理器是唯一的。
那为什么这样设计,不这样设计会怎样呢?
- 如果弹出了多个任务管理器窗口,且这些窗口展示的内容完全一致,这样打开的就全是重复的对象了,就会造成系统资源的浪费,内存的损耗。实际使用中根本不需要多个呈现相同内容的窗口。
- 如果弹出了多个任务管理器窗口,且内容不一致,那就更糟糕了。这意味着,某一时刻应用的使用情况和进程、服务等信息存在多个状态,那到底哪个才是真实的呢?显然这更不可取。
由此可见,确保任务管理器在系统中有且仅有一个非常重要。
2.3 上位机中的应用
在上位机的开发中,也会经常遇到类似情况。下面举几个常见的例子:
2.3.1 用户登录信息
上位机有时需要权限功能,某些页面功能需要特定权限才能操作。
也就是在不同页面上,获取到的用户信息是一致的。要实现这个需求,用户信息就要全局唯一。往往是在用户登录时,将包含各种权限的用户信息加载到单例中。
2.3.2 配置文件
上位机程序中经常需要一些参数配置文件,比如设备相关的、用户习惯相关的。如果不使用单例,每次都要new对象,重新读一遍配置文件,很影响性能。如果使用单例,只需要开始时读一遍就好。
2.3.3 数据连接池
为什么要做池化?
因为新建连接很耗时,如果每次新的任务来了,都新建连接,那严重影响性能。所以一般做法是在一个应用中维护一个连接池,这样当任务来时,若有空闲连接,可以直接使用,省去了初始化的开销。
注意:
这里说的单例是对池做单例,而不是对单个数据库连接做单例。如果是把一个数据库连接对象封存在单例对象中,这样是错误的。如果对单个数据库连接做单例,那多方请求连接时,就只能用一个数据库连接,那不是死的很惨?
2.4 一个应用场景的思考
除了以上的常规使用,我还尝试在页面切换时保留状态的需求上使用单例。
具体场景是这样的,在一个MVVM模式的上位机中有多个页面,我希望切换页面后再切回原页面(页面即Page,可以看成View),其呈现内容仍是之前的。页面的内容可以理解为ViewModel中的属性、命令等。
针对该需求,可以有多种方式使用单例,
- 将ViewModel中的部分关键对象单例化(通常是ViewModel中聚合的Model);
- 将ViewModel单例化,使程序中仅有一份ViewModel;
- 将整个View单例化。
方式1
若将ViewModel中的关键对象单例化,切换回原页面时就重新创建ViewModel,并在其中加载这些单例对象。
方式2
若将整个ViewModel单例化,仅需将View的DataContext绑定到单例ViewModel即可。
方式3
若将整个View单例化,切换页面只需要导航到目标单例View即可。
那么问题来了,哪种方式比较好呢?
这种问题显然没有答案,得看更具体的场景。
你甚至可以不用单例,在主页类中聚合几个子页面,然后点击导航到子页面就好。
现在回到使用单例的情况,
如果你viewmodel中聚合了不少model,并且model可能在其他页面也有使用,那显然对于这些model是应该做单例化的。
如果你viewmodel中有许多独立的状态项,只记录该页面的情况,和model几乎无关。那将整个ViewModel单例也是合理的。
如果你View中有一些联动的对象,比如Canvas,你在Canvas上画了一些画,而Canvas是属于View的。那将View做单例也很合理。
最终到底是用哪种方式,没有一个明确的答案。目前只能根据实际情况选取一种看似合理的方式,通过实践来检验。
三、总结
单例是很基础的设计模式,记住它是为了 保证一个类在程序中只有一个实例,并提供一个访问它的全局访问点 即可。
常见的应用场景,用户状态、配置文件、数据库连接池等。
在多页面用到同一个model时也可以使用。有些场景的使用上不必过于纠结,可达到效果即可。