有些类也需计划生育——单例模式

有些类也需计划生育——单例模式

有些类也需计划生育——单例模式

继上一篇“想走?可以!先买票——迭代器模式”后,本文继续讲解《大话设计模式》第21章“有些类也需计划生育——单例模式”。喜欢本书请到各大商城购买原书,支持正版。

大话设计模式前期读书系列:

本文正式开始


1 类也需要计划生育

时间:5月29日20点 地点:小菜大鸟住所的客厅 人物:小菜、大鸟

“大鸟,今天我在公司写一个MDI窗体程序,当中有一个是‘工具箱’的窗体,问题就是,我希望工具箱要么不出现,出现也只出现一个,可实际上却是我每点击菜单,实例化‘工具箱’,它就会出来一个,这样点击多次就会出现很多个‘工具箱’,你说怎么办?”

“哈,显然,你的这个‘工具箱’类需要计划生育呀,你让它超生了,当然是不好的。”

“大鸟,你又在说笑了,不过计划生育的说法也算是贴切吧,现在我就是希望它要么不要有,有就只能一个,如何办?”

“其实这就是一个设计模式的应用,你先说说你是怎么写的?”

“代码是这样的,首先我建立了一个windows应用程序,默认的窗体为Form1.cs,我设置了它的IsMdiContainer属性为true,表示它是多文档界面MDI子窗体的容器。然后建立了一个菜单,菜单第一项ToolStripMenultemToolbox 就是启动‘工具箱’的按钮。我另建立一个窗体叫FormToolbox.cs,也就是‘工具箱’窗体,里面有一些相关工具按钮。”

private void Form1_Load (object sender, EventArgs e)
{
  // 也可以直接设置 Form1窗体的IsMdiContainer属性为 true
  this.IsMdiContainer = true;
}

private void ToolStripMenuItemToolbox_Click(object sender,EventArgs e)
{
  // “工具箱”启动的代码,实例化FormToolbox,并设置其Mdi父窗体为Forml
  FormToolbox ftb = new FormToolbox();
  ftb. MdiParent = this;
  ftb. Show ();
}

代码执行后的样子如下

有些类也需计划生育——单例模式

“我每点击一次‘工具箱’的菜单项,就产生一个新的‘工具箱’窗体,但实际上,我只希望它出现一次,或者干脆不出现。”

2 判断对象是否是null

“这个其实不难办呀,你判断一下,这个 FormToolbox有没有实例化过不就行了。”

“什么叫Form Toolbox 有没有实例化过﹖我是在按了菜单的按钮时,才去 FormToolbox ftb = newFormToolbox O;那当然是新实例化了。”

“问题就在于此呀,为什么要在点击按钮时才声明FormToolbox对象呢,你完全可以把声明的工作放到类的全局变量中完成。这样你就可以去判断这个变量是否被实例化过了。”

“哦,明白,原来如此。我改一改。”

// 类变量声明
private FormToolbox ftb;

private void ToolStripMenuItemToolbox_click (object sender,EventArgs e)
{
  // 判断是否实例化过,如果为null 说明没有实例化过
  if (ftb==null)
  {
    ftb = new FormToolbox();
    ftb.MdiParent = this;
    ftb.Show();
  }
}

“原来如此,就这么简单,好了,这个功能就算是完成了。”

“小菜,你也太知足了吧,这就算是好了?如果做任何事情不求完美,只求简单达成目标,那你又如何能有提高。”

“这样的一个小程序还可以再完善什么呢?”

“打个比方,你现在不但要在菜单里启动‘工具箱’,还需要在‘工具栏’上有一个按钮来启动‘工具箱’,你如何做?”

“这个不难,首先加一个工具栏控件toolStripl,然后再在按钮 toolStripButtonl的click事件代码里写同样的代码就可以了。”

有些类也需计划生育——单例模式

增加一个工具栏按钮的事件处理

private void toolStripButton1_click(object sender,EventArgs e)
{
  // 判断是否实例化过,如果为null 说明没有实例化过
  if (ftb==null)
  {
    ftb = new FormToolbox();
    ftb.MdiParent = this;
    ftb.Show();
  }
}

“小菜,我还正想提醒你,复制粘贴是最容易的编程,但也是最没有价值的编程。你现在将两个地方的代码复制在一起,这就是重复。这要是需求变化或有Bug时就需要改多个地方。”

“哈,你说得也是,最好是提炼出一个方法来让他们调用。不过这里应该不会有什么变化的。”

“你就那么肯定不会有问题?”

“不会。”

“你把程序运行后,启动‘工具箱’,然后把‘工具箱’窗体关闭,你再点启动按钮看看。”

“啊,‘工具箱’怎么就不出现了?”

“哼哼,原因是当你关闭‘工具箱’时,它的实例并没有变为null而只是Disposed。你的判断只是是否等于null,当然就不会再实例化了。”

“哦,原来是需要再增加IsDisposed属性的判断。”

private void toolstripButton1_click(object sender,EventArgs e)
{
  if (ftb ==null || ftb.IsDisposed)
  {
    ftb = new FormToolbox();
    ftb.MdiParent =this;
    ftb.Show( );
  }
}

“如果我现在有五个地方需要实例化出‘工具箱’窗体,这个小 bug就需要改动五个地方,你说复制粘贴害不害人?”

“是的,那我将它提炼到一个方法里去就行了。”

private FormToolbox ftb;

private void ToolStripMenuItemToolbox_Click(object sender,EventArgs e)
{
  OpenToolbox();
}

private void toolstripButton1_click(object sender,EventArgs e)
{
  OpenToolbox();
}

// 提炼出“打开工具箱”的方法
private void openToolbox()
{

  if (ftb ==null || ftb.IsDisposed)
  {
    ftb = new FormToolbox();
    ftb.MdiParent = this;
    ftb.Show();
  }
}

“现在好了吧,应该没什么大问题了。”

3 生还是不生是自己的责任

“有,当然还有。夫妻已经有了一个小孩子,下面是否生第二胎,这是谁来负责呀?”

“当然是他们自己负责,要是超生了,违反了国家的政策,那也是他们自己的原因。”

“说得好,你再想想看这种场景:领导问下属,报告交了没有,下属可能说‘早交了’于是领导满意地点点头,下属也可能说‘还剩下一点内容没写,很快上交’,领导皱起眉头说‘要抓紧此时这份报告交还是没交,由谁来判断?”

“当然是下属自己的判断,因为下属最清楚报告交了没有,领导只需要问问就行了。”

“好了,同样的,现在‘工具箱’FormToolbox是否实例化都是在MDI主窗体Forml的代码里判断,你不觉得这不合逻辑吗?”

“你的意思是说,Form1里应该只是通知启动‘工具箱’,至于‘工具箱’窗体是否实例化过,应该由‘工具箱’自己来判断?”

“哈,当然,实例化与否的过程其实就和报告交了与否的过程一样,应该由自己来判断 这是它自 己的责任,而不是别人的责任。别人应该只是使用它就可以了。”

“哦,我想想看,实例化其实就是new的过程,但问题就是我怎么让人家不用new呢?“

“是的,如果你不对构造方法做改动的话,是不可能阻止他人不去用new 的。所以我们完全可以直接就把这个类的构造方法改成私有(private),你应该知道,所有类都有构造方法,不编码则系统默认生成空的构造方法,若有显示定义的构造方法,默认的构造方法就会失效。于是只要你将‘工具箱’类的构造方法写成是private的,那么外部程序就不能用new来实例化它了。”

“哈,私有的方法外界不能访问,这是对的,但是这样一来,这个类如何能有实例呢?“

”哈,我们的目的是什么?”

“让这个类只能实例化一次。没有new,我现在连一次也不能实例化了。”

“错,只能说,对于外部代码,不能用new来实例化它,但是我们完全可以再写一个 public方法,叫做GetInstance(),这个方法的目的就是返回一个类实例,而此方法中,去做是否有实例化的判断。如果没有实例化过,由调用private的构造方法new 出这个实例,之所以它可以调用是因为它们在同一个类中,private方法可以被调用的。”

“不是很懂,你把代码写出来吧。”

“好的,你看……”

public partial class FormToolbox : Form
{
  // 声明一个静态的类变量
  private static FormToolbox ftb = null;

  // 构造方法私有,外部代码不能直接new来实例化它
  private FormToolbox()
  {
    InitializeComponent ( ) ;
  }

  // 得到类实例的方法,返回值就是本类对象,注意也是静态的
  public static FormToolbox GetInstance()
  {
    // 当内部的ftb是null或者被 Dispose过,则new它。并且设计其
    // MdiParent为 Form1,此时将实例化的对象存在静态的变量fitb 中,以后就可以不用实例而得到它了
    if (ftb == null || ftb.IsDisposed)
    {
      ftb =new FormToolbox();
      ftb.MdiParent= Form1.ActiveForm;
    }
    return ftb;
  }
}

“其实也就是把你之前写的代码搬到了‘工具箱’FormToolbox中,由于构造方法私有,就只能从内部去调用。然后当访问静态的公有方法GetInstance()时,它会先去查看内存中有没有这个类的实例,若有就直接返回,也就是不会超生了。”

“哦,我知道了。就拿计划生育的例子来说,刚解放时,国家需要人,人多力量大嘛,于是老百姓生!生!生!于是人口爆炸了。后来实行了计划生育,规定了一对夫妇最多只能生育一胎,并把判断的责任交给了夫妇,于是刚结婚时,想要孩子就生一个,而生好一个后,无论谁来要求,都不生了,因为有一个孩子,不可以再生了,否则无论对家庭还是国家都将是沉重的负担。”

“有点偏激,但也可以这么理解吧。客户端的代码如何写呢?”

“好说,好说!这就不难了。”

private void ToolstripMenuItemToolbox_Click(object sender,EventArgs e)
{
  FormToolbox.GetInstance().Show();
}

private void toolstripButton1_click(object sender,EventArgs e)
{
  FormToolbox.GetInstance().Show();
}

“这样一来,客户端不再考虑是否需要去实例化的问题,而把责任都给了应该负责的类去处理。其实这就是一个很基本的设计模式:单例模式。”

4 单例模式

单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。[DP]

“通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的办法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法。[DP]”

有些类也需计划生育——单例模式

Singleton类,定义一个GetInstance操作,允许客户访问它的唯一实例。GetInstance是一个静态方法,主要负责创建自己的唯一实例。

class Singleton
{
  private static Singleton instance;
  
  // 构造方法让其 private,这就堵死了外界利用new创建此类实例的可能
  private Singleton()
  (
  )
  
  // 此方法是获得本类实例的唯一全局访问点
  public static Singleton GetInstance()
  {
    // 若实例不存在,则new一个新实例,否则返回已有的实例
    if (instance == null)
    {
      instance = new Singleton();
    }
    
    return instance;
  }
}

客户端代码

static void Main(string[] args)
{
  Singleton s1 = Singleton.GetInstance();
  Singleton s2 = Singleton.GetInstance();

  // 比较两次实例化后对象的结果是实例相同
  if (s1 == s2)
  {
    Console.WriteLine("两个对象是相同的实例。");
  }
  
  Console.Read();
}

“单例模式除了可以保证唯一的实例外,还有什么好处呢?”

“好处还有呀,比如单例模式因为Singleton类封装它的唯一实例,这样它可以严格地控制客户怎样访问它以及何时访问它。简单地说就是对唯一实例的受控访问。

“我怎么感觉单例有点像一个实用类的静态方法,比如.Net框架里的Math类,有很多数学计算方法,这两者有什么区别呢?”

“你说得没错,它们之间的确很类似,实用类通常也会采用私有化的构造方法来避免其有实例。但它们还是有很多不同的,比如实用类不保存状态,仅提供一些静态方法或静态属性让你使用,而单例类是有状态的。实用类不能用于继承多态,而单例虽然实例唯一,却是可以有子类来继承。实用类只不过是一些方法属性的集合,而单例却是有着唯一的对象实例。在运用中还得仔细分析再作决定用哪一种方式。”

“哦,我明白了。”

5 多线程时的单例

“另外,你还需要注意一些细节,比如说,多线程的程序中,多个线程同时,注意是同时访问Singleton类,调用GetInstance()方法,会有可能造成创建多个实例的。”

“啊,是呀,这应该怎么办呢?”

“可以给进程一把锁来处理。这里需要解释一下lock 语句的涵义,lock 是确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。[MSDN]”

class Singleton
{
  private static Singleton instance;
  
  // 程序运行时创建一个静态只读的进程辅助对象
  private static readonly object syncRoot = new object();

  private Singleton()
  {}
  
  public static Singleton GetInstance()
  {
    // 在同一个时刻加了锁的那部分程序只有一个线程可以进入
    lock(syncRoot)
    {
      if (instance == null)
      {
        instance = new Singleton();
      }
    }
    return instance;
  }
}

“这段代码使得对象实例由最先进入的那个线程创建,以后的线程在进入时不会再去创建对象实例了。由于有了lock,就保证了多线程环境下的同时访问也不会造成多个实例的生成。”

“为什么不直接lock (instance),而是再创建一个syncRoot来 lock呢?”

“小菜呀,加锁时,instance实例有没有被创建过实例都还不知道,怎么对它加锁呢?“

“我知道了,原来是这样。但这样就得每次调用GetInstance方法时都需要lock,好像不太好吧。”

“说得非常好,的确是这样,这种做法是会影响性能的,所以对这个类再做改良。”

6 双重锁定

class Singleton
{
  private static Singleton instance;
  private static readonly object syncRoot = new object();

  private Singleton()
  {}
  
  public static Singleton GetInstance()
  {
    // 先判断实例是否存在,不存在再加锁处理
    if (instance == null)
    {
      lock(syncRoot)
      {
        if (instance == null)
        {
          instance = new Singleton();
        }
      }
    }
    return instance;
  }
}

“现在这样,我们不用让线程每次都加锁,而只是在实例未被创建的时候再加锁处理。同时也能保证多线程的安全。这种做法被称为Double-Check Locking(双重锁定)。”

“我有问题,我在外面已经判断了instance实例是否存在,为什么在 lock 里面还需要做次instance 实例是否存在的判断呢?”小菜问道。

“那是因为你没有仔细分析。对于instance存在的情况,就直接返回,这没有问题。当instance为 nul并且同时有两个线程调用GetInstance()方法时,它们将都可以通过第一重instance-null 的判断。然后由于lock机制,这两个线程则只有一个进入,另一个在外排队等候,必须要其中的一个进入并出来后,另一个才能进入。而此时如果没有了第二重的 instance是否为null 的判断,则第一个线程创建了实例,而第二个线程还是可以继续再创建新的实例,这就没有达到单例的目的。你明白了吗?”

“哦,明白了,原来有这么麻烦呀。”

7 静态初始化

“其实在实际应用当中,C#与公共语言运行库也提供了一种‘静态初始化’方法,这种方法不需要开发人员显式地编写线程安全代码,即可解决多线程环境下它是不安全的问题。[MSDN]

“哦,还有更好的办法?”

“谈不上更好,只不过实现更简单。我们来看代码。”

有些类也需计划生育——单例模式

“这样的实现与前面的示例类似,也是解决了单例模式试图解决的两个基本问题:全局访问和实例化控制,公共静态属性为访问实例提供了一个全局访问点。不同之处在于它依赖公共语言运行库来初始化变量。由于构造方法是私有的,因此不能在类本身以外实例化Singleton类;因此,变量引用的是可以在系统中存在的唯一的实例。不过要注意,instance变量标记为readonly,这意味着只能在静态初始化期间或在类构造函数中分配变量[MSDN]。由于这种静态初始化的方式是在自己被加载时就将自己实例化,所以被形象地称之为饿汉式单例类,原先的单例模式处理方式是要在第一次被引用时,才会将自己实例化,所以就被称为懒汉式单例类。[J&DP]

“懒汉饿汉,哈,很形象的比喻。它们主要有什么区别呢?”

“由于饿汉式,即静态初始化的方式,它是类一加载就实例化的对象,所以要提前占用系统资源。然而懒汉式,又会面临着多线程访问的安全性问题,需要做双重锁定这样的处理才可以保证安全。所以到底使用哪一种方式,取决于实际的需求。从C#语言角度来讲,饿汉式的单例类已经足够满足我们的需求了。

“没想到小小的单例模式也有这么多需要考虑的问题。”小菜感叹道。

“刚接触时,都会觉得比较复杂,但其实用多了,也就这样了。就像70年代末时,老百姓都不太能接受一对夫妇只生一个小孩的计划生育政策,但是随着这近三十年的社会发展,教育和生活成本的大大提高,生两个都养不起,别说七个八个。甚至还干脆出现了‘丁客’一族,不生了,有钱自己用。这在以前能想象吗?”大鸟类比说。

“哈,这都哪跟哪呀,计划生育和单例模式根本就是两回事,大鸟又在胡说八道。好了,我再去研究一下单例的程序,bye-bye。”


每天学一点,不贪多。

下一篇我们接着读“第22章 手机软件何时统一——桥接模式”,欢迎关注微信公众号【乐趣课堂】。

原文出处:微信公众号【乐趣课堂】

原文链接:

本文观点不代表Dotnet9立场,转载请联系原作者。

发表评论

登录后才能评论