看完《设计模式之禅》
这本书,让我对前辈的智慧更加佩服,23种设计模式与设计模式之间的配合,足以让开发人员针对特定的场景下写出近乎完美的逻辑,真正的做到
的拥抱变化。
23种设计模式中,并不都是晦涩难懂,有的简单,有的还需要理解,其中让我印象最深的是状态模式
。当初理解这个模式还是废了一番周折,所以
这篇文章的目的还是梳理一下思路并记录一下,如果能帮到其他人就更好了。
让我们来看看状态模式的定义:当一个对象内在状态改变时允许其改变行为,这个对象看起来像改变了其类。
乍一看定义,并不是很容易让人理解,什么又是改变内在状态,有又改变了行为,又改变了类的(我当初看的也是很懵懂),接下来通过一个例子来理解它。
城市的纵向发展离不开电梯,就以电梯为例。
电梯的状态有停止、运行、开门和关门等状态。而且每个状态还都要有特定的行为,比如在开门的状态下,电梯只能关门,而不能运行;在关门状态下,电梯可以运行、开门等。用一张表来表示这个关系:
状态\动作 | 开门 | 关门 | 运行 | 停止 |
---|---|---|---|---|
开门状态 | X | O | X | X |
关门状态 | O | X | O | O |
运行状态 | X | X | X | O |
停止状态 | O | X | O | X |
这种情况下,一般做法是先根据依赖倒置原则定义出接口,电梯一共有四种状态,所以定义了四个常量表示这几种状态,实现类中电梯的每一次动作发生都要对状态进行判断,看是否可以执行动作。类图如下(借用书中类图):
电梯接口:
```
public interface ILift{
//电梯的4个状态
//开门状态
public final static int OPENING_STATE = 1;
//关门状态
public final static int CLOSING_STATE = 2;
//运行状态
public final static int RUNNING_STATE = 3;
//停止状态
public final static int STOPPING_STATE = 4;
//设置电梯的状态
public void setState(int state);
//电梯的动作
public void open();
public void close();
public void run();
public void stop();
}
```
实现类负责状态的实现与状态过渡:
private int state;
@Override
public void setState(int state) {
this.state = state;
}
//执行关门动作
@Override
public void close() {
switch (this.state) {
case OPENING_STATE:
System.out.println("电梯关门了。。。");//只有开门状态可以关闭电梯门,可以对应电梯状态表来看
this.setState(CLOSING_STATE);//关门之后电梯就是关闭状态了
break;
case CLOSING_STATE:
//do nothing //已经是关门状态,不能关门
break;
case RUNNING_STATE:
//do nothing //运行时电梯门是关着的,不能关门
break;
case STOPPING_STATE:
//do nothing //停止时电梯也是关着的,不能关门
break;
}
}
//执行开门动作
@Override
public void open() {
switch (this.state) {
case OPENING_STATE://门已经开了,不能再开门了
//do nothing
break;
case CLOSING_STATE://关门状态,门打开:
System.out.println("电梯门打开了。。。");
this.setState(OPENING_STATE);
break;
case RUNNING_STATE:
//do nothing 运行时电梯不能开门
break;
case STOPPING_STATE:
System.out.println("电梯门开了。。。");//电梯停了,可以开门了
this.setState(OPENING_STATE);
break;
}
}
//执行运行动作
@Override
public void run() {
switch (this.state) {
case OPENING_STATE://电梯不能开着门就走
//do nothing
break;
case CLOSING_STATE://门关了,可以运行了
System.out.println("电梯开始运行了。。。");
this.setState(RUNNING_STATE);//现在是运行状态
break;
case RUNNING_STATE:
//do nothing 已经是运行状态了
break;
case STOPPING_STATE:
System.out.println("电梯开始运行了。。。");
this.setState(RUNNING_STATE);
break;
}
}
//执行停止动作
@Override
public void stop() {
switch (this.state) {
case OPENING_STATE: //开门的电梯已经是是停止的了(正常情况下)
//do nothing
break;
case CLOSING_STATE://关门时才可以停止
System.out.println("电梯停止了。。。");
this.setState(STOPPING_STATE);
break;
case RUNNING_STATE://运行时当然可以停止了
System.out.println("电梯停止了。。。");
this.setState(STOPPING_STATE);
break;
case STOPPING_STATE:
//do nothing
break;
}
}
}
具体的解释在注释中都有,当然上面这段程序也可以再优化一下,将重复执行语句封装成一个方法(不考虑状态切换规则的状态方法)
场景类:
public static void main(String[] agres) {
Lift lift = new Lift();
lift.setState(ILift.STOPPING_STATE);//电梯是停止的
lift.open();//开门
lift.close();//关门
lift.run();//运行
lift.stop();//停止
}
}
运行一下看结果:
实现逻辑非常完美,但还有些问题:
- Lift中每个动作执行都要进行switch判断,造成代码过长,代码阅读困难。
- 扩展性非常差,如果电梯新增一个状态,接口要改,实现方法也要大量修改,与开闭原则违背。
要解决问题首先要搞清问题,以上需求主要分为两个:状态和动作。
状态是如何产生的,以及这个状态怎么过渡到其他状态(执行动作)
使用状态模式,首先以状态为参考模型:
将电梯的每个状态都封装了起来,再通过场景类Context来切换。
定义一个LiftState抽象类,声明了一个受保护的类型Context变量,这个是串联各个状态的封装类。封装的目的很明显,就是电梯对象内部状态的变化不被调用类知 晓,也就是迪米特法则了(我的类内部情节你知道得越少越好):
//定义一个环境角色,也就是封装状态的变化引起的功能变化
protected Context context;
public void setContext(Context _context) {
this.context = _context;
}
//电梯开门动作
public abstract void open();
//电梯关门动作
public abstract void close();
//电梯运行动作
public abstract void run();
//电梯停止动作
public abstract void stop();
}
电梯开门的实现类(其他具体环境实现类似):
//开启当然可以关闭了,我就想测试一下电梯门开关功能
@Override
public void open() {
System.out.println("电梯门开启...");
}
@Override
public void close() {//虽然可以关门,但这个动作不归我执行
//状态修改
super.context.setLiftState(Context.closeingState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().close();
}
//电梯门不能开着就跑,这里什么也不做
@Override
public void run() {
//do nothing
}
//开门状态已经是停止的了
@Override
public void stop() {
//do nothing
}
}
Openning状态是由open()方法产生的,因此,在这个 方法中有一个具体的业务逻辑,在Openning状态下,电梯能过渡到 其他什么状态呢?按照现在的定义的是只能过渡到Closing状态,因此我们在Close()中定义了 状态变更,同时把Close这个动作也委托了给CloseState类下的Close方法执行,这个可能不好 理解,我们再看看Context类可能好理解一点:
//定义出所有的电梯状态
public final static OpenningState openningState = new OpenningState();//开门状态,这时候电梯只能关闭
public final static ClosingState closeingState = new ClosingState();//关闭状态,这时候电梯可以运行、停止和开门
public final static RunningState runningState = new RunningState();//运行状态,这时候电梯只能停止
public final static StoppingState stoppingState = new StoppingState();//停止状态,这时候电梯可以开门、运行
//定义一个当前电梯状态
private Listate liftState;
public Listate getLiftState() {
return this.liftState;
}
public void setLiftState(Listate liftState) {
//当前环境改变
this.liftState = liftState;
//把当前的环境通知到各个实现类中
this.liftState.setContext(this);
}
public void open() {
this.liftState.open();
}
public void close() {
this.liftState.close();
}
public void run() {
this.liftState.run();
}
public void stop() {
this.liftState.stop();
}
}
这就是状态模式,书中讲的很清楚。
总的来说,状态模式由三个角色组成:抽象状态角色
、具体状态角色
和环境角色
。具体状态角色只负责处理本状态必须完成的任务和决定是否可以过渡到其他状态两个职责,Context负责状态的过渡,这样将过渡与状态分离,提高了程序可扩展性和代码的可阅读性。
####状态模式优缺点:
- 优点:
● 结构清晰,避免了程序的复杂,提高了系统可维护性。
● 遵循了设计原则。很要的体现了开闭原则和单一职责原则,每个状态都是一个子类,与单一职责原则高度符合,扩展状态只需增加子类,正是开闭原则的体现。封装性与迪米特法则也非常吻合。 - 缺点:
只有一个缺点,如果一个事务有很多个状态,就会造成子类太多的问题。需要在项目使用时来衡量是否使用状态模式。
####使用场景:
● 行为随状态改变而改变的的场景,例如权限设计,人员对象权限不同执行相同的行为其结果也会不同。
● 条件、分支判断语句替代者,通过扩展子类实现条件判断处理。
*注意:状态模式适用于当某个对象在它的状态发生改变时,它的行为也随着发生比较大的变化,也就是说在行为受状态约束的情况下可以使用状态模式,而且使用时对象的状态最好不 要超过5个。
最后贴个状态模式通用类图:
组成状态模式三元素的关系非常清晰,具体状态由ConcreteState实现,具体过渡由Context实现,State负责对所有状态的封装,也是依赖倒置原则的体现。