SUMTEC -- There's a thing in my bloglet.

But it's not only one. It's many. It's the same as other things but it exactly likes nothing else...

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  263 随笔 :: 19 文章 :: 3009 评论 :: 74万 阅读
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

上一次随便说了一点皮毛,这一次我再来讲一点皮毛——因为我目前也就仅仅懂得这一点皮毛!呵呵!说之前让我们来看看这本书里面所讲到的设计模式:

-- 结构型(Structural),类或对象的组成
※ Adapter, Bridge, Composite, Facade, Flyweight, Proxy
-- 行为型(Behavioral),对象交互和职责分配
※ Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor
-- 创建型(Creational),对象创建
※ Abstract Factory, Builder, Factory Method, Prototype, Singleton

实际上有一些模式对于设计模式的初级菜鸟们来说经常使用但自己却不知道,就比如Composite(你的Menu-MenuItem,Form-Control),Chain of Responsibility(Message Chain以及更古老的INT 2FH),以及Observer(据说Event可以算做是的)。相比较起来,另外一些我们应该经常使用的模式却没有被认真重视,如果在设计的时候能够认真考虑是否是用一些这样的模式,将会给我们带来非常明显的好处。就比如——嗒当(拟声词)SINGLETON”、嗒当“Proxy”以及嗒当“Flyweight”。为什么我会重点突出这三者呢?因为这三中涉及模式在一些很小的应用程序或者一个很小的类库里面就能够应用起来,并且适用的场合是非常多的。而其他的模式要么就是在这个场合不适用,要么就是其规模太庞大了,很多很小的场合根本没有使用的必要性。当然,这部代表没有用,其实这里的每一个模式都是透着一种灵气,要是真的深入理解并且用活了,那么对于整个系统来说就会有很大的帮助。

那么今天我来讲什么呢?就讲第一个:“SINGLETON”。

挑选Singleton作为第一个仔细去分析的模式,不仅仅因为这个模式适用范围非常广泛,并且理解起来非常容易,还因为我个人认为这个模式在《设计模式》一书当中并没有把这个模式分析透彻,非常容易被“胡乱使用”并导致严重后果。

首先,Singleton是什么样的一个模式呢?他解决什么样的问题呢?怎么解决的?
大家知道,很多时候我们需要一种“服务”,这个“服务”或者应当只由一个对象提供,或者根本就不关心到底由那一个对象提供。比如说Application类型的对象对于一个程序本身肯定只有一个,比如Math内部的各个函数根本就和对象这个概念没有关系。如果说我刚才的解释太过平白,可能让大家误解的话,那么我再用另外一个方式解说:我们很可能需要一些这样的函数,他们:
A、或者根本没有任何状态,因为这些函数之间可能仅仅存在概念上的联系(例如Math里面的东西),要么虽然自成体系但是没有状态。
B、或者成为一个有状态的系统,但是对于整个应用程序而言,有且仅有一个这样的系统(例如Application)。

对于这样的函数集,我们需要用一个特定的方式去集合起来,集合的模式有两种,一种叫做Monostate(没记错吧?),另外一种就是Singleton。Monostate是什么呢?怎么开头没有提到这个模式呢?这么模式太常见太直白了,以至于根本不会又人花心思去分析他。所谓的Monostate就是VB.NET里面的Module以及C# 2.0里面的static class,也就是一个仅仅包含静态函数的、无法构造的静态类。Singleton跟他不一样,虽然两者都能够适用于上面所说的两种情况。Singleton是一个“公开场合”不可以被构造、但是“私底下”却可以构造的、但只产生唯一实例的非静态类。用C#来写一个最简单的Singleton给大家看看也许就彻底明白了:

public class SingleForm : Form
{
  // 需要将构造函数“隐蔽”起来。
  protected SingleForm()
  {
  }

  // 一些你需要记录下来的数据信息(状态),
  // 当然也可以是属性或者方法函数事件等,
  // 可以是public的,也可以是protected/private等。
  public ADataType AData, AnotherData;

  static protected SingleForm instance;
  static public SingleForm Instance
  {
    get
    {
      if (instance == null)
      {
        instance = new SingleForm();
      }
      return instance;
    }
  }
}

如果我们要使用这个Singleton的对象,方法如下:

SingleForm.Instance.Show();

看到这里,相信大家应该明白Singleton的样子是什么样的了。用文字来描述就是,通过讲类的构造函数私有化,并且提供一个(一些)获得唯一实例的静态方法——这个方法会判断是否已经存在一个实例,不存在则创建,存在着返回存在的对象,用这种方式保证对象的唯一性和延续性。

有人就要说了,我用Monostate不一样也能够完成同样的功能吗?我把所有东西都静态了,包括函数字段属性方法事件,统统都静态了就OK了。当然,Monostate和Singleton对于AB两种情况都是完全适用的,但是Monostate有一些很大的缺陷。

首先,某些语言,至少对于C#和VB.NET等.NET语言来说,静态的方法是不能够被继承的,因此需要我们多花一点功夫来写代码。 如果说大部分的函数等,都可以通过指定类名的方式去访问,那么静态的构造函数却是没有办法直接访问的,因此对Monostate模式的静态类进行静态构造的时候,很可能出现静态构造顺序紊乱的情况。

其次,Monostate的资源没有办法自动释放。由于是静态的,所以他会在整个程序的生命周期都存在,即使是已经不再需要使用这些“服务”的时候。如果我们希望提前释放Monostate的资源,那就不得不手动进行,例如写一个Destroy的函数,在其中逐个资源进行释放。可以想想,如果这个类非常庞大非常的复杂,那么需要写的东西就会非常多,很容易出现写漏了或者顺序出现差错的情况。对于前者,造成释放不干净事小,造成下一次(程序没有推出之前)使用该对象的时候可能没有重新构造所有资源事情就大了。对于后者,则很可能造成一些莫名其妙的异常出现。

再次,对应于非常麻烦的释放过程,(二次)产生过程也非常让人头痛。假如我们考虑某个程序需要在一个资源非常有限的地方运行,那么很可能我们不得不尽可能释放一些暂时不可能用到的资源,甚至需要释放一个Singleton对象。这样的话我们就需要准备一个再次生成被强迫释放的资源的能力,Monostate的设计必然使得这一个过程变得比较复杂。

最后,Monostate的模式使得我们无法进行引用,更加没有办法进行传递,这是一个相当严重的问题。仍然假设我们在设计一棵树的各种节点,其中一种节点在整个程序的运行周期当中有且仅有一个。如果用Monostate设计这个节点,那么必然使得这个节点在使用上和其他节点完全不一样。假设有一个Print(Node n)着这样的函数,很难想象应该如何设计一个适应Monostate的版本,哪怕是重载的也行(就算给你设计出来了,十有八九会硬编码,对设计本身的伤害非常大),甚至连应该如何将这个节点加入到这棵树当中我都感到很茫然。

现在让我们回过头来看看Singleton怎么解决这些问题的。关键的地方在于,所有有效的东西都不是静态的,因此我们仅仅需要维护一个唯一的对象——相对于维护非常多的成员,这个任务显得非常的简单。如果对象不存在或者被消灭了,那么所有占用的空间将能够被GC自动清除;如果对象被创建了,那么所有需要的资源就自然的被非静态构造函数构造出来,一些隐含的构造顺序也不需要你亲自过问;同时由于是一个对象,就使得引用成为可能,同时这一个对象的绝大多数行为和效果都和普通的对象没有什么区别,很多函数不加改造就能够适应Singleton的对象。

需要指出的是,Singleton的适用场景是值得我们仔细想一想的。对于情况B,毫无疑问是Singleton的拿手好戏。但是对于一些非常小的或者是不需要二次进入能力的场合(并且不需要引用),使用Monostate也是一个可以考虑的方向,例如Application。但是对于情况A,也许不少朋友会不加考虑的选择Monostate。没错,一个没有状态的(并且不需要引用的)东西实际上完全没有使用Singleton的必要,就如Math,除非这个类需要很多的资源,并且有释放并且再次产生的可能。这个除非却很少人注意到,也许是这样的情况并不多或者没有这样的考虑(实际上比较可能的是后者)。就比如窗口(当然,用Monostate去设计一个窗口是非常困难甚至不可能的一件事情,并且窗口是一个有状态的对象,就请大家暂时假设这是一个可以用Monostate设计的没有状态的对象吧),就完全可以用Singleton的方式设计,并且这个Singleton的窗口很可能允许用户关闭了然后再次打开。假如这个窗口的使用效率并不是很高,那么当用户偶然使用了这个窗口之后,用Monostate模式设计的话,占用的资源就很难被释放掉了。这些资源也许占用你100KB,也许占用64MB,所以有时候我们得考虑用Singleton。当然,这个Singleton的模式会有一定的特殊要求,我将会在近期的某篇文章中给出一个实际的例子。

也许有高人一下子就看出来了,上面那个例子不够完善,这个SingleForm应该是sealed,要把他封闭起来,否则别人再这个上面一继承不小心把构造函数公开了就破坏了这个Singleton模式了。而我目前持有保留态度,因为很可能我们需要面对一个这样的问题:
我们有一个唯一的节点,例如根节点,这个节点有一些非常复杂的功能。同时我们还需要派生很多其他类型的节点,例如叶节点。如果说根节点被封闭了,那么其他节点就没有办法通过继承根节点获得那些已经实现了的复杂功能,而不得不从新写一遍。当然,也可以通过增加一个基类,把那些复杂的功能写到这个基类里面,然后根节点和叶节点分别从这个基类派生出来的方法解决部分问题。然而这种解决方案也不是十全十美的,至少这样会额外增加设计时功能划分的困难(需要判断那些东西需要划分到这个本来并不存在的类当中),并且需要增加一些本来不存在的基类,这些类本身的含义就比较令人困惑,因为它们太抽象了。如果我们的对象继承层次比较深,而不同层次的对象可能有不同的构造模式安排——有些是Singleton的有写不是,那么用增加基类的方法解决问题,必然会增加许多基类,这样很容易扰乱我们的视线。而另外一个问题则没有办法通过增加基类解决:如此改造之后的叶节点将不能够转换成根节点,因为它们是分别继承一各基类,而互相之间没有任何继承关系。比如说我希望将一棵树的一部分导出,以便其他人把这个“部分”当作一棵树进行其他处理,这个时候就没有办法直接把根节点转换出来了,或者说我们不得不费更多的脑筋去解决一些这样的本来根本不存在的问题。
那么,不封闭有什么坏处呢?主要就是可能被人不小心或者故意的在派生类当中公开构造函数,造成实例不唯一。对于前者,那是设计派生类人员的过失,虽然封闭了可以避免这样的低级错误,但是我认为防止发生低级错误不应该以牺牲某种东西为代价的。而对于后者,如果是善意的利用,那无可厚非,不应该禁止。如果是恶意的利用,那么仅仅是封闭很可能根本就达不到效果。因此,考虑再三,再我的设计里面都没有做封闭处理。

需要注意的问题

请大家看好这一节的内容了,我们公司就有人在看了Singleton模式之后,不加思考的应用Singleton模式,造成白白浪费全体人员两个工作日的时间去排除因为错误使用Singleton模式之后所造成的问题。而这个问题确实是《设计模式》这本书里面所没有强调的问题。

大家在决定使用Singleton模式的时候,一定要思考清楚整个系统本身(或者可能会和准备应用Singleton模式的对象相关联的部分)是否已经考虑到Singleton模式,或者是否能够适应Singleton模式。这句话是什么意思呢?我来问大家一个简单的问题:一个在整个程序运行周期当中只会出现一个唯一实例的窗口可不可以设计成Singleton模式呢?一个在整个程序运行周期当中只会出现在某一个窗口的控件(可能上一时刻在窗口A,下一时刻出现在窗口B,但是不会同时出现在A和B上面),是否可以设计成Singleton模式呢?

对于前者,大家一定认为可以。嗯,事实上确实是可以的,虽然Form本身并没有特别考虑到Singleton模式的设计需要,但是事实上他是可以适应Singleton模式的。对于后者,大家也许会疑惑为什么需要设计成Singleton模式,但是对于是否能够设计成Singleton模式的问题应该会表示肯定意见。首先给大家解释为什么有设计成Singleton模式的需要:如果一个控件动辄占用10MB的资源,对于总共只有64MB的PPC来说就不是一件令人愉快的事情,因此需要通过Singleton模式限制他在整个程序的运行周期中只会产生最多一个实例。关于是否能够设计成Singleton模式的问题,我先不回答,大家看代码:

public class SingleControl : Control
{
  // 需要将构造函数“隐蔽”起来。
  protected SingleControl()
  {
  }

  static protected SingleControl instance;
  static public SingleControl Instance
  {
    get
    {
      if (instance == null)
      {
        instance = new SingleControl();
      }
      return instance;
    }
  }
}

各位有没有看出什么问题?没有什么问题嘛,就跟前面的那个SingleControl差不多嘛。对,代码是没有问题的,甚至如果你希望在多个Form里面使用都没有问题——只要你在各个Form之间转移的时候添加一个设置SingleControl.Instance.Parent值的语句就可以了,顶多只是原来窗口上的SingleControl忽然消失了,不过这个对于一个极其占用资源的东西来说算不上一个缺陷。但是除了一种情况:
A窗口不使用SingleControl但是B使用,A窗口会弹出B窗口,B窗口每次关闭都会Close(也就是说会被Dispose掉),问题就出来了。 你会沮丧的发现,在第二次打开B窗口的时候会莫名其妙的抛出异常,如果这个SingleControl使用了一些特定的东西的时候。然后你就不得不花很长的时间:或者是去Debug,找出第一次不出异常但是第二次缺会出现的原因,并且修正这个错误;或者是干脆假设是Singleton惹的祸,把所有“SingleControl.Instance.XXXX”的地方修改成“SingleControl xxx; xxx.XXXX;”的形式,并承担万一不是Singleton的问题的风险(您还得继续找问题啊,甚至找出来之后还得头痛是否再改回去,改回去会不会再出问题……)。

给大家一个明确的答复,确实就是Singleton的错。如果您真的把你设计的一个什么PictureButton这样的有着复杂逻辑的控件改造成Singleton模式,然后按我说的那个特殊情况试验一番,你就会发现抛出来的异常是ObjectDisposedException,并且是PictureButton.instance引起的。奇怪吧!在你的PicutreButton里面根本就没有进行任何Dispose操作的地方,甚至使用这个PictureButton的那些Form的代码里面也没有。别忙活了,就算你把你的所有程序代码全部翻烂了你也不会找出来的,因为这个代码确实不在你的程序当中,而在System.Windows.Forms.Control里面。如果一个Control被Dispose了,那么Control.Controls里面的没有个东西都会被Dispose掉。不信的话可以用ILDasm看看Control.Dispose(bool)函数。也就是说只要窗口B被Dispose了,那么窗口B所包含的任何控件都会被调用Dispose。实际上如果不考虑Singleton模式,这种自动调用所包含控件的Dispose的设计是正确的,因为本来每一个Control的实例都只可能存在于最多一个Control当中(即只有唯一的一个Parent),这个是Control里面其他代码所保证的,那么当Parent被释放之后,就意味着不可能有任何其他的地方会使用到这个Control了。尽管有可能仍然被一些变量所引用,但是“表象”已经被消灭了,不可能再次被显示出来了,强行Dispose能够尽早释放一些不再需要的资源,而这种操作完全是安全的。因此,当窗口B被Dispose之后,PictureButton.instance就会被Dispose了,但是PictureButton.instance仍然不是null,所以当你访问instance.Visible等成员的时候,ObjectDisposedException就抛出来了。

怎么办呢?重写Dispose(bool),在里面仅仅需要增加一句话:instance = false;
现在大家对于控件是否能够使用Singleton设计应该有一个比较明确的答案了吧,我估计大概会说:可以,但是需要重写Dispose。如果是这样的答案的话,我只好奉劝您啊,赶紧去买保险,因为一不小心就可能造成项目失败。我再来给大家举另外一个例子:.NET CF里面的InputPanel就是一个相当失败的例子。如果你仔细看看这个InputPanel的IL,就会发现他实际上是Singleton模式,虽然它可以构造出不同的实例,但是每个实例的核心都是使用的同一个核心对象。这样的设计如果说考虑了我所说的System.Windows.Forms.Control会自动Dispose对象的问题,那么应该还是OK的。但是事实上他根本就没有考虑这个问题,可以说是微软的一个失败。对于InputPanel来说,仅仅是简单的重写Dispose根本就解决不了问题,因为InputPanel有EnabledChanged事件,考虑如下代码:

public class Form1 : Form
{
    // 略去构造函数和InitializeComponents函数
    private InputPanel sip;
    private TextBox txBoxInBottom;

    private sip_EnabledChanged(object sender, EventArgs e)
    {
        if (sip.Enabled)
        {
            txBoxInBottom.Top = 0;
        }
        else
        {
            txBoxInBottom.Top = 100;
        }
    }
}

当一个Form1的实例被注销之后,txBoxInBottom也被注销了(用Dispose),sip也应该被注销了,但是sip的核心对象并没有被注销,而核心对象没有办法知道EnabledChanged事件里面有哪些是已经Dispose的对象的,因此所有注册了的EnabledChanged事件都会被激发。也就是说,那一个被注销的Form1上面的sip的EnabledChanged事件也会被激活,但是这个时候txBoxInBottom已经被注销了,所以访问txBoxInBottom.Top就会引发异常ObjectDisposedException。这是一个很失败的设计,至少我是这么认为的。因为这个问题需要最终设计人员自己去解决,例如重写Form的Dispose函数并手动注销sip.EnabledChanged事件,或者在sip_EnabledChanged里面写一个try...catch块(非常消耗性能)。而本来不用Singleton设计的话,sip随着Form1的注销被注销,也就不会在Dispose之后错误的激活Form.sip_EnabledChanged事件,然后引发异常了。或者就算必须使用Singleton事件,也应该特别注意这一类的问题,比如在引发EnabledChanged事件之前察看一下这个事件所对应的sip是否被Dispose了。由于M$在.NET CF中去掉了IsDisposed,使得我们去判断一个Control是否被Disposed变得非常困难,要么用Reflection要么用try...catch,无论哪一个都是非常消耗CPU的选择,甚至非常的不雅观。我认为,M$在InputPanel的设计上是非常失败的,甚至是非常的不道德的。(把自己的问题和困难丢给用户去解决,但是又把解决问题所需要的东西封闭起来了。)

好了,关于Singleton的分析到此告一段落了。在此奉劝所有刚刚看完《设计模式》的朋友们,不要随便把刚学到的东西到处乱用,先进的东西需要用合适的方法用在合适的场合,才能够体现他的先进性,否则反而会体现出你的愚蠢。最后,我给出目前我的Singleton模式的实现细节,至于里面的一些为什么,我以后在别的文章里面将很可能会提到,目前只好各位自己思考了:

// 我不建议将类封闭
public class Singleton
{
    // 需要将构造函数封闭。
    protected Singleton()
    {
    }

    // 唯一的一个实例。
    static protected instance;
    // 公开的获得唯一一个实例的方法(属性)。
    static public Singleton Instance
    {
        get
        {
            CreateInstance();
            return instance;
        }
    }

    // 构造一个唯一的实例,用于特殊的目的。
    // 这个函数不一定需要公开,但是我建议公开。
    static public void CreateInstance()
    {
        if (instance == null)
        {
            instance = new Singleton();
        }
    }

    // 允许释放这个实例,用于再资源紧缺的情况下释放资源。
    // 这个函数不是必需的,可以不实现。
    static public void DestroyInstance()
    {
        if (instance != null)
        {
            // 可以加入一些其它的操作,例如:instance.Dispose();
            instance = null;
        }
    }

    // 其它的非static的成员,在这里就不写了,那些不是关键的。
}


文章来源:http://dotnet.blogger.cn/sumtec/articles/581.aspx
posted on   Sumtec  阅读(1358)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
点击右上角即可分享
微信分享提示