单件模式
- 5.1 Singleton as Global Object
- 5.2 经典实现
- 5.3 线程安全
- 5.4 The Trouble with Singleton
- 5.5 单件和控制反转
- 5.6 Monostate 单态
- 5.7 总结
Singleton设计模式是设计模式历史上最招人恨的模式之一 (?)
5.1 Singleton as Global Object
最naive的方法是答应不创建一个以上的对象实例:). 例如:
1 | struct Database |
当然, 风险也很大.
最容易想到的做法是提供唯一的, 静态的全局变量. 例如:
1 | static Database database{}; |
静态全局变量的问题在于其初始化顺序在不同的编译单元中是不确定的. 这会带来一些很麻烦的事情. 例如一个全局对象引用了另一个全局对象, 而后者还没有初始化.
另一个问题是发现行的问题: 使用者如何才能知道有这个全局变量存在?
一种方法是提供一个全局(或成员)函数来暴露必要的对象:
1 | Database& get_database() |
这种做法的一个问题是, 只有在支持C++11的编译器上才能保证线程安全. 你必须检查自己的编译器是否在静态对象初始化时插入了锁以阻止并发访问.
还有很多其他的问题.
5.2 经典实现
前面的实现有一个最根本的问题, 它不能阻止创建对象的其他实例.
最直观的想法是加一个计数:
1 | struct Database |
这种做法是十分具有恶意的一种实现: 虽然阻止了创建多个对象实例, 但是并没有表达出我们真正的意图: 我们不想让人调用构造函数多次.
解决这个问题的唯一途径是把构造函数隐藏起来, 并提供一个前面提到过的返回一个且唯一的一个对象实例的成员函数. 例如:
1 | struct Database |
在C++11之前, 你可以简单地将拷贝构造函数和赋值构造函数设置为私有的来达到类似的效果.
你也可以使用boost::noncopyable
达到类似的效果.
再一次强调的是, 如果database
依赖于其他的静态或全局变量, 在析构函数中使用它们将是不安全的, 因为析构函数的调用顺序是不确定的, 很有可能你调用的对象已经不存在了.
最后, 也可以把get()
实现为在堆中创建对象(即, 只有对象指针是静态的).
1 | static Database& get() |
这个实现依赖于这样的假定: Database
会一直存在到程序结束, 使用指针而不是引用可以确保它的析构函数永远不会被调用—-当然, 它也不会导致内存泄漏.
5.3 线程安全
从C++11开始, 单间的初始化是线程安全的. 这意味着, 若两个线程同时调用get()
, Database
不会被实例化两次.
在C++11之前, 你可能需要利用称为**双检查加锁(double-checked locking)**的模式来来解决线程安全问题. 一个典型的实现可能类似如下:
1 | struct Database |
本书讲的是现代C++, 我们不会对这个问题做更多的讨论.
5.4 The Trouble with Singleton
假设我们的数据库中包含了一组城市和它们的人口数据. 接口大概是这样的:
1 | class Database |
考虑一个具体类实现了这个接口:
1 | class SingletonDatabase : public Database |
Singleton的真正的问题在于当其他的组件使用它的时候. 我们假设有一个组件要计算所有城市的总人口:
1 | class SingletonRecordFinder |
这里的问题在于, SingleRecordFinder
紧密依赖于SingletonDatabase
. 这给测试带来一个问题: 如果我们要测试SingleRecordFinder
, 我们就需要使用真实的数据库:
1 | TEST(RecordFinderTest, SingletonTotalPopulationTest) |
但是如果我们不想用实际数据库做测试该怎么办? 如果我们想用其他的dummy组件代替怎么办? 在当前的设计中, 这是不可能的. 这就是Singleton模式的缺点.
该怎么办? 首先, 我们需要停止堆单件数据库的显式依赖. 我们需要的只是实现了数据库接口的某个组件而已, 我们可以创建一个新的ConfigurableRecordFinde
, 它可以让我们来配置数据来自哪里:
1 | struct ConfigurableRecordFinder |
我们使用引用而不是显式的使用单件, 这样我们可以创建一个dummy数据库来做测试:
1 | class DummyDatabase : public Database |
而之前的测试代码就可以使用DummyDatabase
而不是实际的数据库了.
1 | TEST(RecordFinderTest, SingletonTotalPopulationTest) |
5.5 单件和控制反转
单件是强烈的侵入式编程, 不好. 而将现有的单件模式取消代价又很高昂.
一种替代的方式是使用控制反转(IoC)容器
下面使用Boot.DI
定义一个单件组件:
1 | auto injector = di::make_injector( |
上面, 使用I
表示这个类是一个接口类型, di::bind
一行的意思是, 当我们需要一个具有IFoo
类型的成员的组件时, 我们用一个Foo
的单件实例来初始化它.
按照很多人的说法, 在DI容器中使用单件是单件模式唯一可被接受的使用方式. 至少在这里, 当我们想用其他的东西来替换这个单件的时候, 我们可以在容器的配置代码中很容易的实现. 而另一个好处是, 你自己不需要去实现单件的代码, 从而避免了可能出现的错误.
另外, Boost.DI
也同样是线程安全的.
5.6 Monostate 单态
Monostate是单件的一个变体. 它形式上是一个普通的类, 但是行为上类似于单件:
1 | class Printer |
用户可以实例化Printer
类的多个实例, 但是他们实际上引用同一份数据! 但是用户怎么才能知道?
Monostate有一些好处, 比如, 很容易继承, 能够平衡多态? 它的生命周期也被定义得很好(这个可不一定). 最大的好处是你能够访问系统中已经使用的对象.
它的缺点也很明显: 它是侵入式的, 使用static
意味着它总是占用资源, 即使我们不再需要它.
它最大的问题在于, 它乐观地假设类的成员总是通过getter和setter来暴露的. 如果它们被直接访问, 你几乎是无法对代码进行重构的!
5.7 总结
…