设计模式:状态(State)模式
设计模式:状态(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)这两个方法之中,之后这两个方法对该对象的使用是不同的,权限也不一样,因此多接口就会产生多面性。状态模式其实是用了分而治之的思想,将不同的状态分开来讨论,抽取共同性,从而使问题变得简单。