轉:http://blog.csdn.net/lovelion/article/details/8522982
“人有悲歡離合,月有陰晴圓缺”,包括人在內,很多事物都具有多種狀態,而且在不同狀態下會具有不同的行爲,這些狀態在特定條件下還將發生相互轉換。就像水,它可以凝固成冰,也可以受熱蒸發後變成水蒸汽,水可以流動,冰可以雕刻,蒸汽可以擴散。我們可以用UML狀態圖來描述H2O的三種狀態,如圖1所示:
圖1 H2O的三種狀態(未考慮臨界點)
在軟件系統中,有些對象也像水一樣具有多種狀態,這些狀態在某些情況下能夠相互轉換,而且對象在不同的狀態下也將具有不同的行爲。爲了更好地對這些具有多種狀態的對象進行設計,我們可以使用一種被稱之爲狀態模式的設計模式,本章我們將學習用於描述對象狀態及其轉換的狀態模式。
1. 銀行系統中的賬戶類設計
Sunny軟件公司欲爲某銀行開發一套信用卡業務系統,銀行賬戶(Account)是該系統的核心類之一,通過分析,Sunny軟件公司開發人員發現在該系統中,賬戶存在三種狀態,且在不同狀態下賬戶存在不同的行爲,具體說明如下: (1) 如果賬戶中餘額大於等於0,則賬戶的狀態爲正常狀態(Normal State),此時用戶既可以向該賬戶存款也可以從該賬戶取款; (2) 如果賬戶中餘額小於0,並且大於-2000,則賬戶的狀態爲透支狀態(Overdraft State),此時用戶既可以向該賬戶存款也可以從該賬戶取款,但需要按天計算利息; (3) 如果賬戶中餘額等於-2000,那麼賬戶的狀態爲受限狀態(Restricted State),此時用戶只能向該賬戶存款,不能再從中取款,同時也將按天計算利息; (4) 根據餘額的不同,以上三種狀態可發生相互轉換。 |
Sunny軟件公司開發人員對銀行賬戶類進行分析,繪製瞭如圖2所示UML狀態圖:
圖2 銀行賬戶狀態圖
在圖2中,NormalState表示正常狀態,OverdraftState表示透支狀態,RestrictedState表示受限狀態,在這三種狀態下賬戶對象擁有不同的行爲,方法deposit()用於存款,withdraw()用於取款,computeInterest()用於計算利息,stateCheck()用於在每一次執行存款和取款操作後根據餘額來判斷是否要進行狀態轉換並實現狀態轉換,相同的方法在不同的狀態中可能會有不同的實現。爲了實現不同狀態下對象的各種行爲以及對象狀態之間的相互轉換,Sunny軟件公司開發人員設計了一個較爲龐大的賬戶類Account,其中部分代碼如下所示:
class Account {
private String state; //狀態
private int balance; //餘額
......
//存款操作
public void deposit() {
//存款
stateCheck();
}
//取款操作
public void withdraw() {
if (state.equalsIgnoreCase("NormalState") || state.equalsIgnoreCase("OverdraftState ")) {
//取款
stateCheck();
}
else {
//取款受限
}
}
//計算利息操作
public void computeInterest() {
if(state.equalsIgnoreCase("OverdraftState") || state.equalsIgnoreCase("RestrictedState ")) {
//計算利息
}
}
//狀態檢查和轉換操作
public void stateCheck() {
if (balance >= 0) {
state = "NormalState";
}
else if (balance > -2000 && balance < 0) {
state = "OverdraftState";
}
else if (balance == -2000) {
state = "RestrictedState";
}
else if (balance < -2000) {
//操作受限
}
}
......
}
分析上述代碼,我們不難發現存在如下幾個問題:
(1) 幾乎每個方法中都包含狀態判斷語句,以判斷在該狀態下是否具有該方法以及在特定狀態下該方法如何實現,導致代碼非常冗長,可維護性較差;
(2) 擁有一個較爲複雜的stateCheck()方法,包含大量的if…else if…else…語句用於進行狀態轉換,代碼測試難度較大,且不易於維護;
(3) 系統擴展性較差,如果需要增加一種新的狀態,如凍結狀態(Frozen State,在該狀態下既不允許存款也不允許取款),需要對原有代碼進行大量修改,擴展起來非常麻煩。
爲了解決這些問題,我們可以使用狀態模式,在狀態模式中,我們將對象在每一個狀態下的行爲和狀態轉移語句封裝在一個個狀態類中,通過這些狀態類來分散冗長的條件轉移語句,讓系統具有更好的靈活性和可擴展性,狀態模式可以在一定程度上解決上述問題。
2 狀態模式概述
狀態模式用於解決系統中複雜對象的狀態轉換以及不同狀態下行爲的封裝問題。當系統中某個對象存在多個狀態,這些狀態之間可以進行轉換,而且對象在不同狀態下行爲不相同時可以使用狀態模式。狀態模式將一個對象的狀態從該對象中分離出來,封裝到專門的狀態類中,使得對象狀態可以靈活變化,對於客戶端而言,無須關心對象狀態的轉換以及對象所處的當前狀態,無論對於何種狀態的對象,客戶端都可以一致處理。
狀態模式定義如下:
狀態模式(State Pattern):允許一個對象在其內部狀態改變時改變它的行爲,對象看起來似乎修改了它的類。其別名爲狀態對象(Objects for States),狀態模式是一種對象行爲型模式。 |
在狀態模式中引入了抽象狀態類和具體狀態類,它們是狀態模式的核心,其結構如圖3所示:
圖3 狀態模式結構圖
在狀態模式結構圖中包含如下幾個角色:
● Context(環境類):環境類又稱爲上下文類,它是擁有多種狀態的對象。由於環境類的狀態存在多樣性且在不同狀態下對象的行爲有所不同,因此將狀態獨立出去形成單獨的狀態類。在環境類中維護一個抽象狀態類State的實例,這個實例定義當前狀態,在具體實現時,它是一個State子類的對象。
● State(抽象狀態類):它用於定義一個接口以封裝與環境類的一個特定狀態相關的行爲,在抽象狀態類中聲明瞭各種不同狀態對應的方法,而在其子類中實現類這些方法,由於不同狀態下對象的行爲可能不同,因此在不同子類中方法的實現可能存在不同,相同的方法可以寫在抽象狀態類中。
● ConcreteState(具體狀態類):它是抽象狀態類的子類,每一個子類實現一個與環境類的一個狀態相關的行爲,每一個具體狀態類對應環境的一個具體狀態,不同的具體狀態類其行爲有所不同。
在狀態模式中,我們將對象在不同狀態下的行爲封裝到不同的狀態類中,爲了讓系統具有更好的靈活性和可擴展性,同時對各狀態下的共有行爲進行封裝,我們需要對狀態進行抽象,引入了抽象狀態類角色,其典型代碼如下所示:
abstract class State {
//聲明抽象業務方法,不同的具體狀態類可以不同的實現
public abstract void handle();
}
在抽象狀態類的子類即具體狀態類中實現了在抽象狀態類中聲明的業務方法,不同的具體狀態類可以提供完全不同的方法實現,在實際使用時,在一個狀態類中可能包含多個業務方法,如果在具體狀態類中某些業務方法的實現完全相同,可以將這些方法移至抽象狀態類,實現代碼的複用,典型的具體狀態類代碼如下所示:
class ConcreteState extends State {
public void handle() {
//方法具體實現代碼
}
}
環境類維持一個對抽象狀態類的引用,通過setState()方法可以向環境類注入不同的狀態對象,再在環境類的業務方法中調用狀態對象的方法,典型代碼如下所示:
class Context {
private State state; //維持一個對抽象狀態對象的引用
private int value; //其他屬性值,該屬性值的變化可能會導致對象狀態發生變化
//設置狀態對象
public void setState(State state) {
this.state = state;
}
public void request() {
//其他代碼
state.handle(); //調用狀態對象的業務方法
//其他代碼
}
}
環境類實際上是真正擁有狀態的對象,我們只是將環境類中與狀態有關的代碼提取出來封裝到專門的狀態類中。在狀態模式結構圖中,環境類Context與抽象狀態類State之間存在單向關聯關係,在Context中定義了一個State對象。在實際使用時,它們之間可能存在更爲複雜的關係,State與Context之間可能也存在依賴或者關聯關係。
在狀態模式的使用過程中,一個對象的狀態之間還可以進行相互轉換,通常有兩種實現狀態轉換的方式:
(1) 統一由環境類來負責狀態之間的轉換,此時,環境類還充當了狀態管理器(State Manager)角色,在環境類的業務方法中通過對某些屬性值的判斷實現狀態轉換,還可以提供一個專門的方法用於實現屬性判斷和狀態轉換,如下代碼片段所示:
……
public void changeState() {
//判斷屬性值,根據屬性值進行狀態轉換
if (value == 0) {
this.setState(new ConcreteStateA());
}
else if (value == 1) {
this.setState(new ConcreteStateB());
}
......
}
……
(2) 由具體狀態類來負責狀態之間的轉換,可以在具體狀態類的業務方法中判斷環境類的某些屬性值再根據情況爲環境類設置新的狀態對象,實現狀態轉換,同樣,也可以提供一個專門的方法來負責屬性值的判斷和狀態轉換。此時,狀態類與環境類之間就將存在依賴或關聯關係,因爲狀態類需要訪問環境類中的屬性值,如下代碼片段所示:
……
public void changeState(Context ctx) {
//根據環境對象中的屬性值進行狀態轉換
if (ctx.getValue() == 1) {
ctx.setState(new ConcreteStateB());
}
else if (ctx.getValue() == 2) {
ctx.setState(new ConcreteStateC());
}
......
}
……
|
3 完整解決方案
Sunny軟件公司開發人員使用狀態模式來解決賬戶狀態的轉換問題,客戶端只需要執行簡單的存款和取款操作,系統根據餘額將自動轉換到相應的狀態,其基本結構如圖4所示:
圖4 銀行賬戶結構圖
在圖4中,Account充當環境類角色,AccountState充當抽象狀態角色,NormalState、OverdraftState和RestrictedState充當具體狀態角色。完整代碼如下所示:
//銀行賬戶:環境類
class Account {
private AccountState state; //維持一個對抽象狀態對象的引用
private String owner; //開戶名
private double balance = 0; //賬戶餘額
public Account(String owner,double init) {
this.owner = owner;
this.balance = balance;
this.state = new NormalState(this); //設置初始狀態
System.out.println(this.owner + "開戶,初始金額爲" + init);
System.out.println("---------------------------------------------");
}
public double getBalance() {
return this.balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public void setState(AccountState state) {
this.state = state;
}
public void deposit(double amount) {
System.out.println(this.owner + "存款" + amount);
state.deposit(amount); //調用狀態對象的deposit()方法
System.out.println("現在餘額爲"+ this.balance);
System.out.println("現在帳戶狀態爲"+ this.state.getClass().getName());
System.out.println("---------------------------------------------");
}
public void withdraw(double amount) {
System.out.println(this.owner + "取款" + amount);
state.withdraw(amount); //調用狀態對象的withdraw()方法
System.out.println("現在餘額爲"+ this.balance);
System.out.println("現在帳戶狀態爲"+ this. state.getClass().getName());
System.out.println("---------------------------------------------");
}
public void computeInterest()
{
state.computeInterest(); //調用狀態對象的computeInterest()方法
}
}
//抽象狀態類
abstract class AccountState {
protected Account acc;
public abstract void deposit(double amount);
public abstract void withdraw(double amount);
public abstract void computeInterest();
public abstract void stateCheck();
}
//正常狀態:具體狀態類
class NormalState extends AccountState {
public NormalState(Account acc) {
this.acc = acc;
}
public NormalState(AccountState state) {
this.acc = state.acc;
}
public void deposit(double amount) {
acc.setBalance(acc.getBalance() + amount);
stateCheck();
}
public void withdraw(double amount) {
acc.setBalance(acc.getBalance() - amount);
stateCheck();
}
public void computeInterest()
{
System.out.println("正常狀態,無須支付利息!");
}
//狀態轉換
public void stateCheck() {
if (acc.getBalance() > -2000 && acc.getBalance() <= 0) {
acc.setState(new OverdraftState(this));
}
else if (acc.getBalance() == -2000) {
acc.setState(new RestrictedState(this));
}
else if (acc.getBalance() < -2000) {
System.out.println("操作受限!");
}
}
}
//透支狀態:具體狀態類
class OverdraftState extends AccountState
{
public OverdraftState(AccountState state) {
this.acc = state.acc;
}
public void deposit(double amount) {
acc.setBalance(acc.getBalance() + amount);
stateCheck();
}
public void withdraw(double amount) {
acc.setBalance(acc.getBalance() - amount);
stateCheck();
}
public void computeInterest() {
System.out.println("計算利息!");
}
//狀態轉換
public void stateCheck() {
if (acc.getBalance() > 0) {
acc.setState(new NormalState(this));
}
else if (acc.getBalance() == -2000) {
acc.setState(new RestrictedState(this));
}
else if (acc.getBalance() < -2000) {
System.out.println("操作受限!");
}
}
}
//受限狀態:具體狀態類
class RestrictedState extends AccountState {
public RestrictedState(AccountState state) {
this.acc = state.acc;
}
public void deposit(double amount) {
acc.setBalance(acc.getBalance() + amount);
stateCheck();
}
public void withdraw(double amount) {
System.out.println("帳號受限,取款失敗");
}
public void computeInterest() {
System.out.println("計算利息!");
}
//狀態轉換
public void stateCheck() {
if(acc.getBalance() > 0) {
acc.setState(new NormalState(this));
}
else if(acc.getBalance() > -2000) {
acc.setState(new OverdraftState(this));
}
}
}
編寫如下客戶端測試代碼:
class Client {
public static void main(String args[]) {
Account acc = new Account("段譽",0.0);
acc.deposit(1000);
acc.withdraw(2000);
acc.deposit(3000);
acc.withdraw(4000);
acc.withdraw(1000);
acc.computeInterest();
}
}
編譯並運行程序,輸出結果如下:
段譽開戶,初始金額爲0.0 --------------------------------------------- 段譽存款1000.0 現在餘額爲1000.0 現在帳戶狀態爲NormalState --------------------------------------------- 段譽取款2000.0 現在餘額爲-1000.0 現在帳戶狀態爲OverdraftState --------------------------------------------- 段譽存款3000.0 現在餘額爲2000.0 現在帳戶狀態爲NormalState --------------------------------------------- 段譽取款4000.0 現在餘額爲-2000.0 現在帳戶狀態爲RestrictedState --------------------------------------------- 段譽取款1000.0 帳號受限,取款失敗 現在餘額爲-2000.0 現在帳戶狀態爲RestrictedState --------------------------------------------- 計算利息! |
4 共享狀態
在有些情況下,多個環境對象可能需要共享同一個狀態,如果希望在系統中實現多個環境對象共享一個或多個狀態對象,那麼需要將這些狀態對象定義爲環境類的靜態成員對象。
下面通過一個簡單實例來說明如何實現共享狀態:
如果某系統要求兩個開關對象要麼都處於開的狀態,要麼都處於關的狀態,在使用時它們的狀態必須保持一致,開關可以由開轉換到關,也可以由關轉換到開。 |
可以使用狀態模式來實現開關的設計,其結構如圖5所示:
圖5 開關及其狀態設計結構圖
開關類Switch代碼如下所示:
class Switch {
private static State state,onState,offState; //定義三個靜態的狀態對象
private String name;
public Switch(String name) {
this.name = name;
onState = new OnState();
offState = new OffState();
this.state = onState;
}
public void setState(State state) {
this.state = state;
}
public static State getState(String type) {
if (type.equalsIgnoreCase("on")) {
return onState;
}
else {
return offState;
}
}
//打開開關
public void on() {
System.out.print(name);
state.on(this);
}
//關閉開關
public void off() {
System.out.print(name);
state.off(this);
}
}
抽象狀態類如下代碼所示:
abstract class State {
public abstract void on(Switch s);
public abstract void off(Switch s);
}
兩個具體狀態類如下代碼所示:
//打開狀態
class OnState extends State {
public void on(Switch s) {
System.out.println("已經打開!");
}
public void off(Switch s) {
System.out.println("關閉!");
s.setState(Switch.getState("off"));
}
}
//關閉狀態
class OffState extends State {
public void on(Switch s) {
System.out.println("打開!");
s.setState(Switch.getState("on"));
}
public void off(Switch s) {
System.out.println("已經關閉!");
}
}
編寫如下客戶端代碼進行測試:
class Client {
public static void main(String args[]) {
Switch s1,s2;
s1=new Switch("開關1");
s2=new Switch("開關2");
s1.on();
s2.on();
s1.off();
s2.off();
s2.on();
s1.on();
}
}
輸出結果如下:
開關1已經打開! 開關2已經打開! 開關1關閉! 開關2已經關閉! 開關2打開! 開關1已經打開! |
從輸出結果可以得知兩個開關共享相同的狀態,如果第一個開關關閉,則第二個開關也將關閉,再次關閉時將輸出“已經關閉”;打開時也將得到類似結果。
5 使用環境類實現狀態轉換
在狀態模式中實現狀態轉換時,具體狀態類可通過調用環境類Context的setState()方法進行狀態的轉換操作,也可以統一由環境類Context來實現狀態的轉換。此時,增加新的具體狀態類可能需要修改其他具體狀態類或者環境類的源代碼,否則系統無法轉換到新增狀態。但是對於客戶端來說,無須關心狀態類,可以爲環境類設置默認的狀態類,而將狀態的轉換工作交給具體狀態類或環境類來完成,具體的轉換細節對於客戶端而言是透明的。
在上面的“銀行賬戶狀態轉換”實例中,我們通過具體狀態類來實現狀態的轉換,在每一個具體狀態類中都包含一個stateCheck()方法,在該方法內部實現狀態的轉換,除此之外,我們還可以通過環境類來實現狀態轉換,環境類作爲一個狀態管理器,統一實現各種狀態之間的轉換操作。
下面通過一個包含循環狀態的簡單實例來說明如何使用環境類實現狀態轉換:
Sunny軟件公司某開發人員欲開發一個屏幕放大鏡工具,其具體功能描述如下: 用戶單擊“放大鏡”按鈕之後屏幕將放大一倍,再點擊一次“放大鏡”按鈕屏幕再放大一倍,第三次點擊該按鈕後屏幕將還原到默認大小。 |
可以考慮使用狀態模式來設計該屏幕放大鏡工具,我們定義三個屏幕狀態類NormalState、LargerState和LargestState來對應屏幕的三種狀態,分別是正常狀態、二倍放大狀態和四倍放大狀態,屏幕類Screen充當環境類,其結構如圖6所示:
圖6 屏幕放大鏡工具結構圖
本實例核心代碼如下所示:
//屏幕類
class Screen {
//枚舉所有的狀態,currentState表示當前狀態
private State currentState, normalState, largerState, largestState;
public Screen() {
this.normalState = new NormalState(); //創建正常狀態對象
this.largerState = new LargerState(); //創建二倍放大狀態對象
this.largestState = new LargestState(); //創建四倍放大狀態對象
this.currentState = normalState; //設置初始狀態
this.currentState.display();
}
public void setState(State state) {
this.currentState = state;
}
//單擊事件處理方法,封轉了對狀態類中業務方法的調用和狀態的轉換
public void onClick() {
if (this.currentState == normalState) {
this.setState(largerState);
this.currentState.display();
}
else if (this.currentState == largerState) {
this.setState(largestState);
this.currentState.display();
}
else if (this.currentState == largestState) {
this.setState(normalState);
this.currentState.display();
}
}
}
//抽象狀態類
abstract class State {
public abstract void display();
}
//正常狀態類
class NormalState extends State{
public void display() {
System.out.println("正常大小!");
}
}
//二倍狀態類
class LargerState extends State{
public void display() {
System.out.println("二倍大小!");
}
}
//四倍狀態類
class LargestState extends State{
public void display() {
System.out.println("四倍大小!");
}
}
在上述代碼中,所有的狀態轉換操作都由環境類Screen來實現,此時,環境類充當了狀態管理器角色。如果需要增加新的狀態,例如“八倍狀態類”,需要修改環境類,這在一定程度上違背了“開閉原則”,但對其他狀態類沒有任何影響。
編寫如下客戶端代碼進行測試:
class Client {
public static void main(String args[]) {
Screen screen = new Screen();
screen.onClick();
screen.onClick();
screen.onClick();
}
}
輸出結果如下:
正常大小! 二倍大小! 四倍大小! 正常大小! |
6 狀態模式總結
狀態模式將一個對象在不同狀態下的不同行爲封裝在一個個狀態類中,通過設置不同的狀態對象可以讓環境對象擁有不同的行爲,而狀態轉換的細節對於客戶端而言是透明的,方便了客戶端的使用。在實際開發中,狀態模式具有較高的使用頻率,在工作流和遊戲開發中狀態模式都得到了廣泛的應用,例如公文狀態的轉換、遊戲中角色的升級等。
1. 主要優點
狀態模式的主要優點如下:
(1) 封裝了狀態的轉換規則,在狀態模式中可以將狀態的轉換代碼封裝在環境類或者具體狀態類中,可以對狀態轉換代碼進行集中管理,而不是分散在一個個業務方法中。
(2) 將所有與某個狀態有關的行爲放到一個類中,只需要注入一個不同的狀態對象即可使環境對象擁有不同的行爲。
(3) 允許狀態轉換邏輯與狀態對象合成一體,而不是提供一個巨大的條件語句塊,狀態模式可以讓我們避免使用龐大的條件語句來將業務方法和狀態轉換代碼交織在一起。
(4) 可以讓多個環境對象共享一個狀態對象,從而減少系統中對象的個數。
2. 主要缺點
狀態模式的主要缺點如下:
(1) 狀態模式的使用必然會增加系統中類和對象的個數,導致系統運行開銷增大。
(2) 狀態模式的結構與實現都較爲複雜,如果使用不當將導致程序結構和代碼的混亂,增加系統設計的難度。
(3) 狀態模式對“開閉原則”的支持並不太好,增加新的狀態類需要修改那些負責狀態轉換的源代碼,否則無法轉換到新增狀態;而且修改某個狀態類的行爲也需修改對應類的源代碼。
3. 適用場景
在以下情況下可以考慮使用狀態模式:
(1) 對象的行爲依賴於它的狀態(如某些屬性值),狀態的改變將導致行爲的變化。
(2) 在代碼中包含大量與對象狀態有關的條件語句,這些條件語句的出現,會導致代碼的可維護性和靈活性變差,不能方便地增加和刪除狀態,並且導致客戶類與類庫之間的耦合增強。
|
7 練習
(1) 分析如下代碼:
class TestXYZ {
int behaviour;
//Getter and Setter
......
public void handleAll() {
if (behaviour == 0) { //do something }
else if (behaviour == 1) { //do something }
else if (behaviour == 2) { //do something }
else if (behaviour == 3) { //do something }
... some more else if ...
}
}
爲了提高代碼的擴展性和健壯性,可以使用( )設計模式來進行重構。
A. Visitor(訪問者) B. Facade(外觀)
C. Memento(備忘錄) D. State(狀態)
(2) 傳輸門是傳輸系統中的重要裝置。傳輸門具有Open(打開)、Closed(關閉)、Opening(正在打開)、StayOpen(保持打開)、Closing(正在關閉)五種狀態。觸發狀態的轉換事件有click、complete和timeout三種。事件與其相應的狀態轉換如圖7所示。
圖7 傳輸門響應事件與其狀態轉換圖
試使用狀態模式對傳輸門進行狀態模擬,要求繪製相應的類圖並編程模擬實現。