設計模式:狀態(State)模式
一、前言
狀態模式在某些場合中使用是非常方便的,什么叫做狀態,如果大家學過《編譯原理》就會明白DFA M和NFA M,在確定有限狀態機和非確定有限狀態機中,狀態就是最小的單元,當滿足某種條件的時候,狀態就會發生改變,我們可以把時間中的一個時刻當做一個狀態,那么其實整個社會都是有狀態組成的,前一時刻到下一時刻,整個社會上的物質(空間)發生了什么樣的變化,因此狀態可以非常的大也可以非常的小,天氣變化情況是狀態,白天和黑夜也是狀態,人的生活作息等等都是狀態,因此狀態無處不在。那么狀態模式就是將一個狀態看做一個類,這與以往我們對類的理解不一樣,以往我們認為類是對對象的抽象,用來表示對象的,對象一般是具體的事物,而現在我們將狀態這種非具體的看不見的但又真實存在的事物當做類描述的東西,這一點可能需要大家理解。
那么為什么必須要狀態模式,不用狀態模式可以嗎?當然可以,但是還是回到了代碼的可維護性、可擴展性、可復用性這個層面上來考慮問題,比如我們本例的內容,考慮一個銀行系統,可以用來取款、打電話、報警、記錄這四種功能,但是考慮如下需求:在白天如果我們去取款是正常的,晚上取款就要發出警報;在白天打電話有人接,晚上打電話啟動留言功能;白天和晚上按警鈴都會報警。那么我們應該如何設計這個程序呢,當然我們可以對每一個動作(作為一個函數),在這個函數內部,我們進行判斷是白天還是黑夜,然后根據具體的情況做出反應。這樣當然是可以的,但是假如我們的狀態(白天和黑夜)非常的多呢,比如將24小時分成24個時間段(24個狀態),那么我們對於每一個函數就要判斷24遍,這無疑是非常糟糕的代碼,可讀性非常的差,並且如果需求發生了改變,我們很難去修改代碼(很容易出現錯誤),但是如果我們考慮將這些狀態都作為一個類,在每一個類內部進行處理、判斷和相應的切換,這樣思路就非常的清晰,如果再增加一種狀態,代碼需要修改的地方會非常的少,對於狀態非常多的情景來說非常的方便。
二、代碼
Context 接口:
1 package zyr.dp.state; 2 3 public interface Context { 4 5 public abstract void setClock(int hour); 6 public abstract void changeState(State state); 7 public abstract void callSecurity(String str); 8 public abstract void recordLog(String msg); 9 10 }
SafeFrame 實現類:
1 package zyr.dp.state; 2 3 import java.awt.*; 4 import java.awt.Frame; 5 import java.awt.event.ActionEvent; 6 import java.awt.event.ActionListener; 7 8 public class SafeFrame extends Frame implements Context,ActionListener { 9 10 private static final long serialVersionUID = 1676660221139225498L; 11 12 private Button btnUse=new Button("使用"); 13 private Button btnAlarm=new Button("警鈴"); 14 private Button btnPhone=new Button("打電話"); 15 private Button btnExit=new Button("退出"); 16 17 private TextField tfClock=new TextField(60); 18 private TextArea taAlarm=new TextArea(10,60); 19 20 private State state=DayState.getInstance(); 21 22 public SafeFrame(String title){ 23 super(title); 24 setBackground(Color.BLUE); 25 setLayout(new BorderLayout()); 26 27 add(tfClock,BorderLayout.NORTH); 28 tfClock.setEditable(false); 29 add(taAlarm,BorderLayout.CENTER); 30 taAlarm.setEditable(false); 31 32 Panel panel=new Panel(); 33 panel.add(btnUse); 34 panel.add(btnAlarm); 35 panel.add(btnPhone); 36 panel.add(btnExit); 37 add(panel,BorderLayout.SOUTH); 38 39 pack(); 40 show(); 41 42 btnUse.addActionListener(this); 43 btnAlarm.addActionListener(this); 44 btnPhone.addActionListener(this); 45 btnExit.addActionListener(this); 46 } 47 48 public void setClock(int hour) { 49 tfClock.setText(hour<10 ? "現在時間是:" + "0"+hour : "現在時間是:" +hour); 50 state.doClock(this, hour); 51 } 52 53 public void changeState(State state) { 54 System.out.println("從狀態"+this.state+"轉變到了"+state); 55 this.state=state; 56 } 57 58 public void callSecurity(String str) { 59 taAlarm.append("Call..."+str+"\n"); 60 } 61 62 public void recordLog(String msg) { 63 taAlarm.append("record..."+msg+"\n"); 64 } 65 66 public void actionPerformed(ActionEvent e) { 67 if(e.getSource()==btnUse){ 68 state.doUse(this); 69 }else if(e.getSource()==btnAlarm){ 70 state.doAlarm(this); 71 }else if(e.getSource()==btnPhone){ 72 state.doPhone(this); 73 }else if(e.getSource()==btnExit){ 74 System.exit(0); 75 }else{ 76 System.out.print("未預料錯誤!"); 77 } 78 } 79 80 }
State 接口:
1 package zyr.dp.state; 2 3 public interface State { 4 5 public abstract void doClock(Context context,int hour); 6 public abstract void doUse(Context context); 7 public abstract void doAlarm(Context context); 8 public abstract void doPhone(Context context); 9 10 }
NightState實現類:
1 package zyr.dp.state; 2 3 public class NightState implements State { 4 5 private NightState(){ 6 7 } 8 private static NightState nightState=new NightState(); 9 10 public static NightState getInstance() { 11 return nightState; 12 } 13 14 public void doClock(Context context, int hour) { 15 if(hour>=6 && hour <18){ 16 //白天 17 context.changeState(DayState.getInstance()); 18 } 19 } 20 21 public void doUse(Context context) { 22 context.callSecurity("晚上使用"); 23 } 24 25 public void doAlarm(Context context) { 26 context.callSecurity("晚上警鈴"); 27 } 28 29 public void doPhone(Context context) { 30 context.recordLog("晚上打電話"); 31 } 32 33 }
DayState實現類:
1 package zyr.dp.state; 2 3 public class DayState implements State { 4 5 private DayState(){ 6 7 } 8 private static DayState dayState=new DayState(); 9 10 public static DayState getInstance() { 11 return dayState; 12 } 13 14 public void doClock(Context context, int hour) { 15 if(hour<6 || hour >=18){ 16 //晚上 17 context.changeState(NightState.getInstance()); 18 } 19 } 20 21 public void doUse(Context context) { 22 context.callSecurity("白天使用"); 23 } 24 25 public void doAlarm(Context context) { 26 context.callSecurity("白天警鈴"); 27 } 28 29 public void doPhone(Context context) { 30 context.recordLog("白天打電話"); 31 } 32 }
Main類:
1 package zyr.dp.state; 2 3 public class Main { 4 5 public static void main(String[] args) { 6 7 SafeFrame f=new SafeFrame("狀態模式"); 8 while(true){ 9 for(int hour=1;hour<=24;hour++){ 10 f.setClock(hour); 11 try { 12 Thread.sleep(1000); 13 } catch (InterruptedException e) { 14 e.printStackTrace(); 15 } 16 } 17 } 18 19 } 20 21 }
運行結果:
三、總結
可以看到狀態模式的強大威力,是用最簡潔的代碼通過接口、抽象類、普通類、繼承、委托、代理模式等方式,將狀態抽象為類,然后通過控制狀態的邏輯委托不同的狀態去做不同的事情,對於每一個狀態來說又再次委托控制狀態的邏輯做出相應的動作和修改,這樣看起來比較復雜,其實仔細閱讀就會發現因為接口(抽象類)的原因,使得程序非常的簡潔,各個狀態分工明確,密切配合。
但是狀態模式也有一些缺點,正是因為各個狀態密切配合,在一個狀態之中要知道其他狀態的對象,這就造成了一定的關聯,狀態與狀態之間是一種緊耦合的關系,這是狀態模式的一點缺點,針對於這一點,我們可以將狀態遷移的代碼統一交給SafeFrame來做,這樣就要使用到了Mediator仲裁者模式了。
使用單例的原因是如果一直創造新的對象會對內存產生浪費,因此單例即可。同樣的使用狀態模式通過接口使用state變量來表示相應的狀態,不會產生混淆和矛盾,相比於使用多個變量來分區間表示狀態來說是非常清晰簡練的。State模式便於增加新的狀態(也需要修改其他狀態的狀態遷移代碼),不便於增加新的“依賴於狀態的處理”,比如doAlarm等,因為一旦增加了,實現了State接口的所有狀態都要增加該部分代碼。
同時我們也看到了實例的多面性,比如SafeFrame實例實現了ActionListener接口和Context接口,那么就可以將new SafeFrame()對象傳入fun1(ActionListener a)和fun2(Context context)這兩個方法之中,之后這兩個方法對該對象的使用是不同的,權限也不一樣,因此多接口就會產生多面性。狀態模式其實是用了分而治之的思想,將不同的狀態分開來討論,抽取共同性,從而使問題變得簡單。