1 说明
本文主要讲解单态设计模式(Monostate Pattern),单例设计模式是比较常见的,在我之前的文章2.3章节中详细阐述过它的多种不同实现(https://blog.csdn.net/weixin_37624828/article/details/105929809)。但是,单例模式也有一些缺点是需要考虑的。本文将对单例设计模式再次做出新的评价,进而引出一种新的设计模式供读者在设计时提供一种全新的思路。
2 单例模式-再评价
好处:
- 跨平台:适用合适的中间件(例如,RMI),可以把单例模式扩展为多个JVM和多个计算机工作。
- 适用于任何类:只需把一个类的构造函数变为私有的,并且在其中增加相应的静态函数和变量,就可以把这个类变为单例。
- 可以通过派生创建:给定一个类,可以创建它的一个单例子类。
- 延迟求值:如果单例从未使用过,那么就决不会创建它。
代价:
- 销毁方法未定义:没有好的方法去销毁(destory)一个单例,或者解除其职责。即使添加一个decommission方法把instance的值置为null,系统种的其他模块仍然持有对该单例实例的引用。这样,随后对getInstance方法的调用会创建另外一个实例,致使同时存在两个实例。这个问题在C++种尤为严重,因为实例可以被摧毁,可能会导致去提领一个已被正式停止使用(decommission)的对象。
- 不能继承:从单例类派生出来的类并不是单例。如果要使其成为单例,必须要增加所需的静态方法和变量。
- 效率问题:每次调用getInstance方法都会执行if语句。就大多数调用而言,if语句是多余的。
- 不透明性:单例的使用者知道它们正在使用一个单例,因为它们必须要调用getInstance方法。
在程序设计中,可以考虑使用单态模式,在单态模式中设置和获取一个值可以通过不同的对象来实现,看起来两个实例就好像是具有不同名字的同一个对象一样。
单态模式是通过将单例模式中保存的变量定义为静态变量,从而达到多个对象的状态保持一致。
3 单态模式
考虑一个例子,地铁十字转门简单的有限状态机。十字转门开始时处于Locked状态。如果投入一枚硬币,它就迁移到Unlocked状态,开启转门,复位可能出现的任何告警状态,并把硬币放到收集箱柜中。如果此时乘客通过了转门,转门就迁移回Locked状态并且把门锁上。其状态转移图如下所示:
代码清单:
- Turnstile.java
- TestTurnstile.java — 测试用例
1 Turnstile.java
public class Turnstile {
private static boolean isLocked = true;
private static boolean isAlarming = false;
private static int itsCoins = 0;
private static int itsRefunds = 0;
protected final static Turnstile LOCKED = new Locked();
protected final static Turnstile UNLOCKED = new Unlocked();
protected static Turnstile itsState = LOCKED;
public void reset(){
lock(true);
alarm(false);
itsCoins = 0;
itsRefunds = 0;
itsState = LOCKED;
}
public boolean locked(){
return isLocked;
}
public boolean alarm(){
return isAlarming;
}
public void coin(){
itsState.coin();
}
public void pass(){
itsState.pass();
}
protected void lock(boolean shouldLock){
isLocked = shouldLock;
}
protected void alarm(boolean shouldAlarm){
isAlarming = shouldAlarm;
}
public int coins(){
return itsCoins;
}
public int refunds(){
return itsRefunds;
}
public void deposit(){
itsCoins++;
}
public void refund(){
itsRefunds++;
}
}
class Locked extends Turnstile {
@Override
public void coin() {
itsState = UNLOCKED;
lock(false);
alarm(false);
deposit();
}
@Override
public void pass(){
alarm(true);
}
}
class Unlocked extends Turnstile {
@Override
public void coin(){
refund();
}
@Override
public void pass(){
lock(true);
itsState = LOCKED;
}
}
2 TestTurnstile.java
public class TestTurnstile extends TestCase {
public TestTurnstile(String name) {
super(name);
}
public void setUp() {
Turnstile t = new Turnstile();
t.reset();
}
public void testInit() {
Turnstile t = new Turnstile();
assert (t.locked());
assert (!t.alarm());
}
public void testCoin() {
Turnstile t = new Turnstile();
t.coin();
Turnstile t1 = new Turnstile();
assert (!t1.locked());
assert (!t1.alarm());
assertEquals(1, t1.coins());
}
public void testCoinAndPass() {
Turnstile t = new Turnstile();
t.coin();
t.pass();
Turnstile t1 = new Turnstile();
assert (t1.locked());
assert (!t1.alarm());
assertEquals("coins", 1, t1.coins());
}
public void testTwoCoins() {
Turnstile t = new Turnstile();
t.coin();
t.coin();
Turnstile t1 = new Turnstile();
// assertEquals("unlocked", !t1.locked());
assert (!t1.locked());
assertEquals("coins", 1, t1.coins());
assertEquals("refunds", 1, t1.refunds());
assert (!t1.alarm());
}
public void testPass() {
Turnstile t = new Turnstile();
t.pass();
Turnstile t1 = new Turnstile();
assert (t1.alarm());
assert (t1.locked());
}
public void testCancelAlarm() {
Turnstile t = new Turnstile();
t.pass();
t.coin();
Turnstile t1 = new Turnstile();
assert (!t1.alarm());
assert (!t1.locked());
assertEquals("coin", 1, t1.coins());
assertEquals("refund", 0, t1.refunds());
}
public void testTwoOperations() {
Turnstile t = new Turnstile();
t.coin();
t.pass();
t.coin();
assert (!t.locked());
assertEquals("coins", 2, t.coins());
t.pass();
assert (t.locked());
}
}
4 总结
使用单态模式可以在透明性、可派生性、多态性上面获得好处,但是要在不可转换、效率、内存、平台方面付出代价。
好处:
- 透明性:使用单态对象和使用常规的对象没有什么区别,使用者不需要知道对象是单态的。
- 可派生性:单态的派生类都是单态的。事实上,单态的所有派生类都是同一个单态的一部分,它们共享相同的静态变量。
- 多态性:由于单态的方法不是静态的,所以可以在派生类中重写(override)它们。因此,不同的派生类可以基于同样的静态变量表现出不同的行为。
代价:
- 不可转换性:不能透过派生把常规类转换成单态类。
- 效率问题:因为单态是真正的对象,所以会导致许多的创建和摧毁开销。
- 内存占用:即使从未使用单态,它的变量也要占据内存空间。
- 平台局限性:单态不能跨多个JVM或者多个平台工作。
从实现的角度来看,单例模式一般使用私有构造函数、一个静态变量和一个静态方法对实例化进行控制和限制。而单态模式只是简单地把对象的所有变量变成静态的。
如果希望通过派生去约束一个现存类,并且不介意它的所有调用者都必须调用getInstance方法来获取访问权,那么单例是最合适的。如果希望类的单一性本质对使用者透明,或者希望使用单一对象的多态派生对象,那么单态是最合适的。