文章目录
状态(State)模式
隶属类别——对象行为模式
1. 意图
允许一个对象在其内部状态发生改变时改变它的行为。对象看起来似乎修改了它的类。
2. 别名
状态对象(Object for States)
3. 动机
考虑一个表示网络连接的类TCPConnection.一个TCPConnection对象的状态处于若干不同状态之一:连接已建立(Established)、正在监听(Listening)、连接已关闭(Closed).当一个TCPConnection对象收到其他对象的请求时,它根据自己的当前状态作出不同的反应。例如,一个Open请求的结果依赖于该连接时处于连接已关闭状态还是连接已建立状态。State模式描述了TCPConnection如何在每一种状态下表现出不同的行为。
这一模式的关键四小时引入一个称为TCPState的抽象类来表示网络的连接状态。TCPState类为各表示不同的操作状态的子类声明了一个公共的接口。TCPState的子类实现与特定状态相关的行为。例如,TCPEstabilished和TCPClosed类分别实现了特定于TCPConnection的连接已建立状态和连接已关闭的行为。
TCPConnection类维护一个表示TCP连接当前状态的状态对象(一个TCPState子类的实例)。TCPConnection类将所有与状态相关的请求委托给这个状态对象。TCPConnection使用它的TCPState子类实例来执行特定于连接状态的操作。
一旦连接状态改变,TCPConnection对象就会改变它所使用的状态对象。例如当连接从已建立状态转变为已关闭状态是,TCPConnection会用一个TCPClosed的实例来代替原来的TCPEstablished的实例。
4. 适用性
在以下两种情况下均可使用State模式:
- 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为。
- 一个操作中含有庞大的多分支的条件语句,切这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常,有多个操作包含这一相同的条件结构。State模式将每一个条件分支分入一个独立的类中。这使得你可以根据对象的自身情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。
5. 结构
6. 参与者
- Context(环境,如TCPConnection)
- 定义客户感兴趣的接口。
- 维护一个ConcreteState子类的实例,这个实例定义当前状态。
- State(状态,如TCPState)
- 定义一个接口以封装与Context的一个特定状态相关的一些行为.
- ConcreteState subclasses(具体状态子类,如TCPEstablished,TCPListen,TCPClosed)
- 每一个子类实现了与Context的一个状态相关的一些行为。
7. 协作
- Context将与状态相关的请求委托给当前的ConcreteState对象处理。
- Context可将自身作为一个参数传递给处理该请求的状态对象。这使得状态对象在必要时可以访问Context.
- Context是客户使用的主要接口。客户可用状态对象来配置一个Context,一旦一个Context配置完毕,它的客户不再需要直接与状态对象打交道。
- Context或ConcreteState都可决定哪个状态是另外那一个的后继者,以及在是在何种条件下进行状态转换。
8. 效果
State模式有下面一些效果:
-
-
它将特定状态相关的行为局部化,并将不同的状态分割开来 State模式将所有与一个特定的状态相关的行为都放入一个对象中。因为所有与状态相关的代码都存在于某一个State子类中,所以通过定义新的子类可以很容易的增加新的状态和转换。
另一个方法是使用数据值定义内部状态并且让Context来显式地检查这些数据,但这样将会使整个Context的实现看起来很相似的条件语句或Case语句,增加一个新的状态可能需要改变若干个操作,这使得维护变得复杂了。
State模式避免了这个问题,但可能会引入另一个问题,因为该模式将不同状态的行为分布在多个State子类中,这就增加了子类的数目,相对于单个类的实现来说不够紧凑。但是如果有许多状态时这样的分布实际上更好一些,否则需要使用巨大的条件语句。
正如很长的过程一样,巨大的条件语句是不受欢迎的。它们形成一大整块并且使得代码不够清晰,这又使得它们修改和扩展。State模式提供了一个更好的方法来组织与特定状态相关的代码。决定状态转移的逻辑不在单块的if或switch语句中,而是分布在State子类之间。将每一个状态转化和动作封装到一个类中,就把着手点从执行状态提高到整个对象的状态。这将使代码结构化并使意图更加清晰。
-
-
- 它使得状态转化显式化 当一个对象仅以内部数据值来定义当前状态时,其状态仅表现为对一些变量的赋值,这不够明确,为不同的状态引入独立的对象使得转换更加明确了。而且,State对象可保证Context不会内部发生内部状态不一致的情况,因为从Context的角度看,状态转换时原子的——只需重新绑定一个变量(即Context的State对象变量),而无需为多个变量赋值。
-
- State对象可被共享 如果State对象没有实例变量——即它们表示的状态完全以它们的类型来编码——那么各Context对象就可以共享一个State对象。当状态以这种方式被共享,它们必然是没有内部状态,只有行为的轻量级对象。
-
- 类的数目急剧增加(缺点) 当我们把状态转换和动作封装到一个类中时,类的数目也在不断增多。
9. 实现
实现State模式有多方面的考虑:
-
-
谁定义状态转换 State模式不指定哪一个参与者定义状态转换准则。如果该准则是固定的,那么它们可在Context中完全实现。然而若让State子类自身制定它们的后继状态以及何时进行转化,通常更灵活更合适。这需要Context增加一个接口,让State对象显式地设定Context的当前状态。
用这种方法分散转换逻辑可以很容易地定义新的State子类来修改和扩展该逻辑。这样做的一个缺点是,一个State子类至少拥有一个其他子类的信息,这就在各子类之间产生了实现依赖。
-
-
-
基于表的另一种方法 在C++ Programming Style中,Cargil描述了另一种将结构加载在状态驱动的代码上的方法:他使用表将输入映射到状态转化。对每一个状态,一张表将每一个可能的输入映射到一个后继状态。实际上,这种方法将条件代码映射为查找表(How to do this???)。
表的主要好处是它们的规则性:你可以通过更改数据而不是更改程序代码来改变状态转换的准则、然而它也有一些缺点:
- 对表的查找通常不如函数调用效率高。
- 用统一的、表格形式表示转换逻辑是的转换准则变得不够明确而难以理解。
- 通常难以加入伴随状态的一些动作。表驱动的方法描述了状态和他们之间转换,但必须扩充这个机制以便在每一个转换上能够进行任意的计算。
表驱动的状态机和State模式的主要区别可以被终结如下:State模式对状态相关的行为进行建模,而表驱动的方法着重与定义状态转换。
-
-
-
创建和销毁State对象 一个常见的值得考虑的实现上的权衡是,究竟是 1. 仅当需要State对象时才创建它们并随后销毁它们,还是2.提前创建它们并且始终不销毁它们?
当将要进入的状态在运行时是不可知的,并且上下文不经常改变时,第一种选择较为可取。这种方法避免创建不会被用到的对象,如果State对象存储大量的信息是这一点很重要。当状态改变很频繁是,第二种方法较好。在这种情况下最好避免销毁状态,因为可能很快再次需要用到它们。此时可以预先一次付清各个状态的开销,并且在运行过程中根本不存在销毁状态对象的开销。但是这种方法可能不太方面,因为Context必须保存对所有可能会进入的那些状态的引用。
-
-
- 使用动态继承 改变一个响应特定请求的行为可以用在运行时刻改变这个对象的类的方法实现,但这在大多数面向对象程序设计语言中都是不可能的。Self和其他一些基于委托的语言的却是例外,它们提供了这种机制,从而直接支持State模式。Self中的对象可将操作委托给其他对象以达到某种形式的动态继承。在运行时刻改变委托的目标有效地改变了继承的结构。这一机制允许对象改变它们的行为,也就是改变他们的类。
10. 代码示例
首先设置State——State.java
public interface State {
void insertQuarter();
void ejectQuarter();
void turnCrack();
void dispense();
void refill(int count);
}
以及ConcreteState——SoldState.java & SoldOutState.java & NoQuarterState.java & HasQuarterState.java
SoldState.java
public class SoldState implements State{
GumballMachine gumballMachine;
public SoldState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("Please wait, we're already giving you a gumball");
}
@Override
public void ejectQuarter() {
System.out.println("Sorry, you already turned the crank");
}
@Override
public void turnCrack() {
System.out.println("Turning twice doesn't get you another gumball");
}
@Override
public void dispense() {
gumballMachine.releaseBall();
if (gumballMachine.getCount() > 0) {
gumballMachine.setState(gumballMachine.getNoQuarterState());
}else {
System.out.println("Oops, out of gumballs !");
gumballMachine.setState(gumballMachine.getSoldOutState());
}
}
@Override
public void refill(int count) {
System.out.println("The machine is giving a gumball, now can't refill");
}
@Override
public String toString() {
return "soldState";
}
}
SoldOutState.java
public class SoldOutState implements State{
GumballMachine gumballMachine;
public SoldOutState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("Sorry,Please don't insert quarter,there is no gumball in the machine");
}
@Override
public void ejectQuarter() {
System.out.println("You don't insert quarter at all");
}
@Override
public void turnCrack() {
System.out.println("You truned , but there is no gumball in the machine");
}
@Override
public void dispense() {
System.out.println("No gumball in the machine can be dispense");
}
@Override
public void refill(int count) {
System.out.println("Adding " + count + " gumballs to the machine");
gumballMachine.setCount(count);
gumballMachine.setState(gumballMachine.getNoQuarterState());
}
@Override
public String toString() {
return "soldState";
}
}
HasQuarterState.java
public class HasQuarterState implements State{
GumballMachine gumballMachine;
public HasQuarterState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("You can't insert another quarter");
}
@Override
public void ejectQuarter() {
System.out.println("Quarter returned ");
gumballMachine.setState(gumballMachine.getNoQuarterState());
}
@Override
public void turnCrack() {
System.out.println("You turned ...");
gumballMachine.setState(gumballMachine.getSoldState());
}
@Override
public void dispense() {
System.out.println("No gumball dispensed");
}
@Override
public void refill(int count) {
System.out.println("You don't need to refill");
}
@Override
public String toString() {
return "HasQuarterState";
}
}
NoQuarterState.java
public class NoQuarterState implements State{
GumballMachine gumballMachine;
public NoQuarterState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("You inserted a quarter");
gumballMachine.setState(gumballMachine.getHasQuarterState());
}
@Override
public void ejectQuarter() {
System.out.println("You haven't inserted a quarter");
}
@Override
public void turnCrack() {
System.out.println("You turned , but there's no quarter");
}
@Override
public void dispense() {
System.out.println("You need to pay first");
}
@Override
public void refill(int count) {
System.out.println("Machine don't need to refill");
}
@Override
public String toString() {
return "NoQuarterState";
}
}
接下来是Context——GumballMachine.java
public class GumballMachine {
private State soldOutState;
private State noQuarterState;
private State hasQuarterState;
private State soldState;
private State state = soldOutState;
private int count = 0;
public GumballMachine(int numberGumballs) {
soldOutState = new SoldOutState(this);
noQuarterState = new NoQuarterState(this);
hasQuarterState = new HasQuarterState(this);
soldState = new SoldState(this);
this.count = numberGumballs;
if (numberGumballs > 0) {
state = noQuarterState;
}
}
public void insertQuarter() {
state.insertQuarter();
}
public void ejectQuarter() {
state.ejectQuarter();
}
public void turnCrank() {
state.turnCrack();
state.dispense();
}
public void refill(int count) {
state.refill(count);
}
public void setState(State state) {
this.state = state;
}
public void setCount(int count) {
this.count = count;
}
public int getCount() {
return count;
}
public void releaseBall() {
System.out.println("A gumball comes rolling out the slot...");
if (count != 0) {
count = count - 1;
}
}
public State getNoQuarterState() {
return noQuarterState;
}
public State getHasQuarterState(){
return hasQuarterState;
}
public State getSoldOutState() {
return soldOutState;
}
public State getSoldState() {
return soldState;
}
public String toString() {
StringBuffer result = new StringBuffer();
result.append("\nMighty Gumball, Inc.");
result.append("\nJava-enabled Standing Gumball Model #2004");
result.append("\nInventory: " + count + " gumball");
if (count != 1) {
result.append("s");
}
result.append("\n");
result.append("Machine is " + state + "\n");
return result.toString();
}
}
以及测试类Client——GumballMachineTestDrive.java
public class GumballMachineTestDrive {
public static void main(String[] args) {
// TODO Auto-generated method stub
GumballMachine gumballMachine = new GumballMachine(4);
System.out.println(gumballMachine);
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
System.out.println(gumballMachine);
gumballMachine.insertQuarter();
gumballMachine.ejectQuarter();
gumballMachine.turnCrank();
System.out.println(gumballMachine);
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.ejectQuarter();
System.out.println(gumballMachine);
gumballMachine.insertQuarter();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
System.out.println(gumballMachine);
gumballMachine.refill(10);
System.out.println(gumballMachine);
}
}
以及测试结果
Mighty Gumball, Inc.
Java-enabled Standing Gumball Model #2004
Inventory: 4 gumballs
Machine is NoQuarterState
You inserted a quarter
You turned ...
A gumball comes rolling out the slot...
Mighty Gumball, Inc.
Java-enabled Standing Gumball Model #2004
Inventory: 3 gumballs
Machine is NoQuarterState
You inserted a quarter
Quarter returned
You turned , but there's no quarter
You need to pay first
Mighty Gumball, Inc.
Java-enabled Standing Gumball Model #2004
Inventory: 3 gumballs
Machine is NoQuarterState
You inserted a quarter
You turned ...
A gumball comes rolling out the slot...
You inserted a quarter
You turned ...
A gumball comes rolling out the slot...
You haven't inserted a quarter
Mighty Gumball, Inc.
Java-enabled Standing Gumball Model #2004
Inventory: 1 gumball
Machine is NoQuarterState
You inserted a quarter
You can't insert another quarter
You turned ...
A gumball comes rolling out the slot...
Oops, out of gumballs !
Sorry,Please don't insert quarter,there is no gumball in the machine
You truned , but there is no gumball in the machine
No gumball in the machine can be dispense
You truned , but there is no gumball in the machine
No gumball in the machine can be dispense
Sorry,Please don't insert quarter,there is no gumball in the machine
You truned , but there is no gumball in the machine
No gumball in the machine can be dispense
Mighty Gumball, Inc.
Java-enabled Standing Gumball Model #2004
Inventory: 0 gumballs
Machine is soldState
Adding 10 gumballs to the machine
Mighty Gumball, Inc.
Java-enabled Standing Gumball Model #2004
Inventory: 10 gumballs
Machine is NoQuarterState
最后是UML类图:
11. 已知应用
State模式在TCP连接协议上有着的应用。
HotDraw和Unidraw中的绘图编辑器框架都使用了这一技术。它使得客户可以很容易地定义新类型的工具。在HotDraw中,DrawingController类将请求转发给当前的Tool对象。在Unidraw中,相应的类是Viewer和Tool。下图简要地描述了Tool和DrawingController的接口。
12. 相关模式
- Flyweight模式: Flyweight解释了何时共享状态对象。
- Singleton模式:一般状态对象都是Singleton。
13. 设计原则口袋
- 封装变化
- 类应该对修改关闭,对扩展开放
- 针对接口编程,不针对实现编程
- 为交互对象的松耦合设计而努力
- 多用组合少用继承
- 依赖抽象,不要依赖具体类
- 只和密友聊天
- 好莱坞原则——别找我,我会找你
- 单一责任原则——类应该只有一个改变的理由。
14. 参考文献
《设计模式:可复用面向对象软件的基础》
《HeadFirst设计模式》