使用 C#/.NET Core 实现单体设计模式

本文的概念内容来自深入浅出设计模式一书

由于我在给公司做内培, 所以最近天天写设计模式的文章....

单体模式 Singleton

单体模式的目标就是只创建一个实例.

实际中有很多种对象我们可能只需要它们的一个实例, 例如: 线程池,缓存, 弹出的对话框, 用于保存设置的类, 用于logging的类, 硬件设备驱动对象等等.

一段对话:

A: 如何创建一个对象?

B: new MyObject()

A: 如果想创建另一个对象, 就再次new MyObject()?

B: 是的

A: 所以说我们有某个类, 我们就可以对它实例化很多次?

B: 是的, 但是它必须是public的类额

A: 如果不是public的呢?

B: 如果不是public的, 那么只有同一个包下的类才能对它实例化, 但是仍然可以实例化多次.

A: 嗯, 很有趣, 你只你可以这样做吗?

使用 C#/.NET Core 实现单体设计模式

B: 没见过, 但是语法是没问题的, 存在即合理.

A: 它是什么意思呢?

B: 我想它不能被实例化吧, 因为它的构造函数是private的啊.

A: 那么, 有没有哪个对象可以使用这个private的构造函数呢?

B: 额, 我认为只有MyClass里面的代码可以调用这个构造函数, 但是感觉那没什么用啊.

A: 为什么没用呢?

B: 因为对类进行实例化, 就是想要用它的实例, 而这样做的话, 别的类也无法对它进行实例化啊. 这是个鸡和蛋的问题: 我可以使用MyClass里面的构造函数, 但是我无法实例化这个对象, 因为其他的类无法使用 "new MyClass()".

A: 你着确实是一种观点, 那么下面代码是什么意思呢?

使用 C#/.NET Core 实现单体设计模式

B: MyClass有一个静态方法, 我们可以这样调用静态方法: MyClass.getInstance();

A: 为什么使用MyClass, 而不是某个对象的名?

B: 因为getInstance()是静态方法; 也就是说, 它是一个类方法, 你需要使用类名来调用方法.

A: 非常有趣, 那么我把实例化代码放里面呢?

使用 C#/.NET Core 实现单体设计模式

B: 确实可以有这种操作...

A: 那么, 现在你认为有第二种方法来实例化对象吗?

B: MyClass.getInstance();

A: 那么你现在能写出只允许创造一个MyClass实例的代码了吗?

B: 应该行.

经典单体模式的实现

使用 C#/.NET Core 实现单体设计模式

首先需要有个静态成员变量保留着实例的引用.

然后构造函数必须是私有的.

getInstance()方法可以该类进行实例化, 并且返回该实例.

另外, 该类也可以有其他方法.

里面最重要的一部分代码:
使用 C#/.NET Core 实现单体设计模式

如果该实例引用为null, 那么创建一个实例, 并把这个实例赋給类的那个成员变量. 这里要注意, 如果我们永远不需要这个类的实例, 那么这个类永远也不会被实例化, 这叫做懒初始化.

如果实例引用不是null, 那么就说明之前已经创建过该类的实例了, 那么就返回之前创建的实例就行了.

一道巧克力工厂锅炉的题

先看这个类:

使用 C#/.NET Core 实现单体设计模式

开始的时候, 锅炉是空的, 所以也没有煮沸.

fill()方法(填充), 填充锅炉的时候, 锅炉必须是空的, 一旦填满了, 那么empty就改为false, 表示填满了. 刚填满肯定不是煮沸状态, 所以boiled也是false.

drain()方法(抽取), 只有锅炉是满的并且煮沸之后才能抽取巧克力液体, 抽取完了, 锅炉就又空了 empty改为true.

boil()方法(煮), 煮混合液体, 要求锅炉的前提状态必须是满的 empty为false, 并且还没煮沸 boiled为false. 一旦煮沸了, 就把boiled改成true.

这个工序很好, 但是必须保证只有一个锅炉, 那么该怎么做? 请写出代码.

单体模式定义

单体模式保证一个类只有一个实例, 并提供一个全局访问该实例的方法.

类图:

使用 C#/.NET Core 实现单体设计模式

其他问题

上面巧克力锅炉那道题你可能写好了, 但是可能会出现这个问题:

锅炉可能在里面有液体的情况下又进行了fill填充动作. 这是怎么回事?

是不是其他线程引起的这个问题?

我们可能有两个线程都在执行这段代码:

使用 C#/.NET Core 实现单体设计模式

那么两个线程调用时是否有重叠, 代码执行是否有交错?  请看下图:

使用 C#/.NET Core 实现单体设计模式

处理多线程问题

为了解决这个多线程的问题问题, 可已使用synchronized方法:

使用 C#/.NET Core 实现单体设计模式

(synchronized是java里的关键字, C#的请参考下面我写的代码)

使用synchronized关键字以后, 每个线程必须等到轮到它的时候才能进入方法. 这样两个线程就不可能同时进入该方法了.

但是这种方法开销很大, 这有时会成为一个问题. 而且可能比你想的更糟糕:

只有第一次执行该方法的时候synchronized才起作用, 一旦我们设定好了成员变量那个引用到具体的实例, 以后就不需要synchronized这个方法了, 除了第一次, 以后这就是额外的开销.

还能改进多线程吗

1. 如果性能不是那么重要, 就继续使用synchronized吧. 但是要记住使用synchronized之后运行速度可能会差100倍(JVM).

2. 那就不如早点把实例给创建出来, 而不是懒创建.

例如:

使用 C#/.NET Core 实现单体设计模式

使用静态的成员引用, 这样类在加载的时候就把实例创建出来了(保证在任何线程访问之前就会创建出来).

3. 使用"双重检查锁"来减少对sync的使用.

使用 C#/.NET Core 实现单体设计模式

这就是首先检查实例是否被创建了, 如果没有那么进入sync块. 第一创建实例的时候时sync的, 在块里面, 再检查一次实例是否为null, 然后创建实例.

volatile关键字会保证被单体实例化的时候多线程会正确的处理uniqueInstance变量.

所以如果性能是问题, 就可以使用这个方法.

其他问题

Q: 如果我创建一个类, 里面都是静态方法和静态变量, 那么它的效果和单体模式不是一样的吗?

A: 是的, 如果你类没有其他依赖并且初始化并不复杂的话.

Q: 可以继承单体模式吗?

A: 简单的回答就是: No.

Q: 为什么单体模式比全局变量好?

A: 全局变量会污染命名空间, 当然了单体模式写不好也很烂.

总结

使用 C#/.NET Core 实现单体设计模式

C# 实现

ChocolateBoiler:

namespace SingletonPattern
{
public class ChocolateBoiler
{
public bool Empty { get; private set; }
public bool Boiled { get; private set; } private static ChocolateBoiler _uniqueInstance; private ChocolateBoiler()
{
Empty = true;
Boiled = false;
} public static ChocolateBoiler GetInstance()
{
return _uniqueInstance ?? (_uniqueInstance = new ChocolateBoiler());
} public void Fill()
{
if (Empty)
{
Empty = false;
Boiled = false;
}
} public void Drain()
{
if (!Empty && Boiled)
{
Empty = true;
}
} public void Boil()
{
if (!Empty && !Boiled)
{
Boiled = true;
}
}
}
}

SynchronizedChocolateBoiler:

using System.Runtime.CompilerServices;

namespace SingletonPattern
{
public class SynchronizedChocolateBoiler
{
public bool Empty { get; private set; }
public bool Boiled { get; private set; } private static SynchronizedChocolateBoiler _uniqueInstance; private SynchronizedChocolateBoiler()
{
Empty = true;
Boiled = false;
} [MethodImpl(MethodImplOptions.Synchronized)]
public static SynchronizedChocolateBoiler GetInstance()
{
return _uniqueInstance ?? (_uniqueInstance = new SynchronizedChocolateBoiler());
} public void Fill()
{
if (Empty)
{
Empty = false;
Boiled = false;
}
} public void Drain()
{
if (!Empty && Boiled)
{
Empty = true;
}
} public void Boil()
{
if (!Empty && !Boiled)
{
Boiled = true;
}
}
}
}

DoubleCheckChocolateBoiler:

namespace SingletonPattern
{
public class DoubleCheckChocolateBoiler
{
public bool Empty { get; private set; }
public bool Boiled { get; private set; } private static volatile DoubleCheckChocolateBoiler _uniqueInstance;
private static readonly object LockHelper = new object(); private DoubleCheckChocolateBoiler()
{
Empty = true;
Boiled = false;
} public static DoubleCheckChocolateBoiler GetInstance()
{
if (_uniqueInstance == null)
{
lock (LockHelper)
{
if (_uniqueInstance == null)
{
_uniqueInstance = new DoubleCheckChocolateBoiler();
}
}
}
return _uniqueInstance;
} public void Fill()
{
if (Empty)
{
Empty = false;
Boiled = false;
}
} public void Drain()
{
if (!Empty && Boiled)
{
Empty = true;
}
} public void Boil()
{
if (!Empty && !Boiled)
{
Boiled = true;
}
}
}
}

由于这里面提到了多线程, 所以我会另写一篇关于C#/.NET Core异步和多线程的文章(也会是书上的内容, 这本书叫 C# 7 in a Nutshell, 我认为这是最好的C#/.NET Core参考书, 可是没有中文的, 所以我就是做一下翻译和精简)....

这个系列的代码我放在这里了: https://github.com/solenovex/Head-First-Design-Patterns-in-CSharp

上一篇:BZOJ1030——文本生成器


下一篇:html5语义化标签大全