C#上位机中的单例应用思考


一、前言

之前写过一篇关于单例的文——C#中单例模式的实现,讲了讲单例是什么以及在C#中的常见代码实现,那篇文的内容偏理论,并不实用。

最近在用WPF写上位机,发现我在实际开发中使用单例时,并不关心其底层实现,也不太会出现这样的单例类代码:

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类中)以使整个程序代码都能访问之,在想要用到该单例时,从容器中取出即可:

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系统这个程序中,任务管理器是唯一的。

那为什么这样设计,不这样设计会怎样呢?

  1. 如果弹出了多个任务管理器窗口,且这些窗口展示的内容完全一致,这样打开的就全是重复的对象了,就会造成系统资源的浪费,内存的损耗。实际使用中根本不需要多个呈现相同内容的窗口。
  2. 如果弹出了多个任务管理器窗口,且内容不一致,那就更糟糕了。这意味着,某一时刻应用的使用情况和进程、服务等信息存在多个状态,那到底哪个才是真实的呢?显然这更不可取。

由此可见,确保任务管理器在系统中有且仅有一个非常重要。

2.3 上位机中的应用

在上位机的开发中,也会经常遇到类似情况。下面举几个常见的例子:

2.3.1 用户登录信息

上位机有时需要权限功能,某些页面功能需要特定权限才能操作。
也就是在不同页面上,获取到的用户信息是一致的。要实现这个需求,用户信息就要全局唯一。往往是在用户登录时,将包含各种权限的用户信息加载到单例中。

2.3.2 配置文件

上位机程序中经常需要一些参数配置文件,比如设备相关的、用户习惯相关的。如果不使用单例,每次都要new对象,重新读一遍配置文件,很影响性能。如果使用单例,只需要开始时读一遍就好。

2.3.3 数据连接池

为什么要做池化?
因为新建连接很耗时,如果每次新的任务来了,都新建连接,那严重影响性能。所以一般做法是在一个应用中维护一个连接池,这样当任务来时,若有空闲连接,可以直接使用,省去了初始化的开销。

注意:
这里说的单例是对池做单例,而不是对单个数据库连接做单例。
如果是把一个数据库连接对象封存在单例对象中,这样是错误的。如果对单个数据库连接做单例,那多方请求连接时,就只能用一个数据库连接,那不是死的很惨?

2.4 一个应用场景的思考

除了以上的常规使用,我还尝试在页面切换时保留状态的需求上使用单例。
具体场景是这样的,在一个MVVM模式的上位机中有多个页面,我希望切换页面后再切回原页面(页面即Page,可以看成View),其呈现内容仍是之前的。页面的内容可以理解为ViewModel中的属性、命令等。

针对该需求,可以有多种方式使用单例,

  1. 将ViewModel中的部分关键对象单例化(通常是ViewModel中聚合的Model);
  2. 将ViewModel单例化,使程序中仅有一份ViewModel;
  3. 将整个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时也可以使用。有些场景的使用上不必过于纠结,可达到效果即可。

猜你喜欢

转载自blog.csdn.net/BadAyase/article/details/132523500