在GOF
的23
种OO
设计模式中,在现实项目,尤其是C++
项目里,最为常见的当属Singleton
。
之所以出现这样的现象,是因为它的简单。完全不需要什么OO
思想就可以熟练使用。
因此,对于从面向过程程序员转过来的C++
程序员,更多的把Singleton
当做一个普通C
函数的集合,而这个集合往往是一个职责不明的上帝类:名字往往包含:Manager
, Controller
, Layer
等。
正是因为如此,Singleton
一直背负着恶名。其中最为人诟病的是它增加了耦合度。
Singleton的问题究竟在哪?
Singleton
作为一个创建型设计模式,其客户代码一般会这么写:
Foo::getInstance().doSomething();
这种情况下,一个Singleton
与普通的类的唯一差别是,其客户会同时依赖两项知识:
- 类所提供的服务;
- 类是一个单例。
而普通的类,只会造成第一类耦合。
我们知道耦合最大的问题,在于被依赖方的变化会导致依赖方跟着变化。因而,当Foo
某天从单实例变为多实例时,就会造成Foo
的所有客户代码都会跟着修改。
怎样降低耦合?
既然Singleton
仅仅是在第二点处增加了耦合,那么降低其耦合的手段也就呼之欲出:让客户不依赖类是一个单例这个事实。
具体做法则是:依赖注入。即让客户的工厂将Singleton
实例注入给客户。比如:
struct Client
{
Client(Foo* foo) : foo(foo) {}
void f()
{
foo->doSomething();
}
private:
Foo* foo;
};
struct ClientFactory
{
static Client* create()
{
return new Client(Foo::getInstance());
}
};
Or DON'T...
这样的做法确实降低了Client
与Singleton
之间的耦合度。但同时,这也增加了实现的复杂度,另外,也会让每个Client
实例都会增加一个指针开销。得失之间,我们需要Think Twice
。
我们知道耦合所带来的危害:即被依赖方的变化导致依赖方的变化。但我们也同样知道,按照向着稳定的方向依赖原则,如果被依赖方是稳定的,那么客户代码也不会受到变化的影响。
因而,如果一个Singleton
从本质上就是单例,从可以设想的范围内,这项知识不会变化。那么系统的耦合度事实上并未因此而增加。比如:
Log::getInstance().trace("blah, blah...");
全局变量
另外,Foo::getInstance()
这样的写法,和一个全局变量g_Foo
的写法无异。我们都知道全局变量是邪恶的,因为它们会大大增加系统的耦合度。
但是,全局变量必然会增加系统的耦合度吗?
让我们再次回到同样的出发点:只有不稳定的依赖才会真正增加耦合度。
而很多全局变量,都是把实现细节,而不是对问题的抽象暴露给其客户。
同时,作为全局变量,整个系统随处均可访问,从而造成大面积的对于实现细节的耦合。一旦这些不稳定的实现细节发生变化,则会导致整个系统大面积的受到影响。这正是全局变量原罪的由来。
而作为一个Singleton
,客户对其造成的耦合点:
- 类的服务接口;
- 类是一个单例。
如果这两点都是稳定的,那么用户对其的依赖并不会造成真正的问题。
在很多现实项目中,正是因为设计素养的缺乏,从而造成Singleton
本身是一个职责不明的上帝类,因而其服务接口本身就是不稳定的;同时也违背的缩小依赖范围原则,强迫客户依赖了很多它并不真正需要的接口。这种耦合给客户造成的恶劣影响,要远远大于类是一个Singleton所带来的影响。
另外,一个上帝类自身也是高度不稳定的,这会更加恶化系统的正交度。
而反过来,如果Singleton
类本身已经对实现细节做了很好的信息隐藏,同时也做到了单一职责,其API
定义又非常合理,那么客户对于类本身接口的依赖就是稳定的。
另外,如果从本质上看,这个类本来就应该只有一个实例,那么客户对于第二点的依赖也是稳定的。
结论
Singleton
本身并不必然会增加系统的耦合度。糟糕的类设计,以及类本身并不必然是个单例,这两个不稳定的设计,才会导致系统的耦合度的增加。
因而,我们运用任何一个具体规则时,都要回到更高层,更本质的层面去衡量,这样才能看清每个现象背后的原因,从而有助于我们做好每一个设计决策。
网友评论