案例
APP抽奖活动
- 假如没参加一次这个活动要扣除用户50积分,中奖概率是10%
- 奖品数量固定,抽完就不能再抽奖
- 活动有4个状态:可以抽奖,不能抽奖,发放奖品和奖品领完
- 有3个活动:扣除50积分,抽奖,发放奖品
- 4个状态转换图如下
传统解决方案
通过if/else判断当前抽奖的状态,从而实现不同的逻辑,伪代码如下
if(state == 不能抽奖){
扣除50积分();
state = 可以抽奖;
}else if(state == 可以抽奖){
抽奖();
if(抽中了){
state = 不能抽奖;
}else{
state = 发放奖品;
}
}else if(state == 发放奖品){
发放奖品();
奖品数量减一;
if(当前所剩奖品数量不为0){
state = 不能抽奖;
}else{
state = 奖品领完;
}
}else if(state == 奖品领完){
打印提示信息;
}else{
打印出错信息;
}
传统方案的问题
难以应对变化,在添加一种状态时,我们需要手动添加if/else,在添加一种功能时,要对所有的状态进行判断,这样把每个状态对应的活动代码全部挤在一个类里,会使得代码变得越来越臃肿,难以维护,并且一旦没有处理某个状态。便会产生及其严重的BUG。
用状态模式解决
- 画出状态图,上面已经画过了
- 分析有多少个活动,将这些活动封装在一个状态接口里,每个具体的状态可以在各自的状态下实现这些活动
- 分析有多少个具体状态,实现状态接口,并聚合上下文客户端
- 创建一个上下文客户端,组合或聚合状态接口作为当前状态的引用;实现状态接口,使其也拥有状态接口的活动;组合所有的具体状态
/**
*
* @ClassName StatePattern
* @Description 状态模式
* @author fuling
* @date 2020年11月3日 下午3:09:44
*/
public class StatePattern {
public static void main(String[] args) {
Context context = new Context(1);
for(int i = 0; i < 20; i++) {
System.out.println("=========================");
context.deductPoints();
context.luckyDraw();
}
}
}
class Context implements State{
//组合所有的状态类
static State CANNOT_LUCKY_DRAW_STATE;//不能抽奖状态
static State CAN_LUCKY_DRAW_STATE;//可以抽奖状态
static State GIVE_OUT_PRIZE_STATE;//发放奖品状态
static State PRIZE_EMPTY_STATE;//奖品领完状态
//当前拥有的奖品数量
int count;
//当前状态
State currentState;
Context(int count){
this.count = count;
CANNOT_LUCKY_DRAW_STATE = new CannotLuckyDrawState(this);
CAN_LUCKY_DRAW_STATE = new CanLuckyDrawState(this);
GIVE_OUT_PRIZE_STATE = new GiveOutPrizeState(this);
PRIZE_EMPTY_STATE = new PrizeEmptyState(this);
//如果当前奖品数量大于0,则初始化当前状态为不能抽奖状态,否则为奖品领完状态
if(count > 0)currentState = CANNOT_LUCKY_DRAW_STATE;
else currentState = PRIZE_EMPTY_STATE;
}
@Override
public void deductPoints() {
this.currentState.deductPoints();
}
@Override
public boolean luckyDraw() {
if(this.currentState.luckyDraw()) {
giveOutPrize();
return true;
}
return false;
}
@Override
public void giveOutPrize() {
this.currentState.giveOutPrize();
}
}
interface State{
RuntimeException EXCEPTION = new UnsupportedOperationException();
//定义三个活动
void deductPoints();//扣除积分
boolean luckyDraw();//抽奖
void giveOutPrize();//领取奖品
}
/**
*
* @ClassName CannotLuckyDrawState
* @Description 不能抽奖状态
* @author fuling
* @date 2020年11月3日 下午3:57:58
*/
class CannotLuckyDrawState implements State{
Context context;
CannotLuckyDrawState(Context context){
this.context = context;
}
@Override
public void deductPoints() {
System.out.println("成功扣除50积分!");
//当前状态变为可以抽奖状态
this.context.currentState = Context.CAN_LUCKY_DRAW_STATE;
}
@Override
public boolean luckyDraw() {
throw EXCEPTION;
}
@Override
public void giveOutPrize() {
throw EXCEPTION;
}
}
/**
*
* @ClassName CanLuckyDrawState
* @Description 可以抽奖状态
* @author fuling
* @date 2020年11月3日 下午3:58:12
*/
class CanLuckyDrawState implements State{
Context context;
CanLuckyDrawState(Context context){
this.context = context;
}
@Override
public void deductPoints() {
throw EXCEPTION;
}
@Override
public boolean luckyDraw() {
int random = new Random().nextInt(10);
if(random != 9) {
System.out.println("很遗憾,没有中奖!");
this.context.currentState = Context.CANNOT_LUCKY_DRAW_STATE;
return false;
}else {
System.out.println("中奖了!");
this.context.currentState = Context.GIVE_OUT_PRIZE_STATE;
return true;
}
}
@Override
public void giveOutPrize() {
throw EXCEPTION;
}
}
/**
*
* @ClassName GiveOutPrizeState
* @Description 领取奖品状态
* @author fuling
* @date 2020年11月3日 下午3:58:24
*/
class GiveOutPrizeState implements State{
Context context;
GiveOutPrizeState(Context context){
this.context = context;
}
@Override
public void deductPoints() {
throw EXCEPTION;
}
@Override
public boolean luckyDraw() {
throw EXCEPTION;
}
@Override
public void giveOutPrize() {
//礼物数量减一
this.context.count--;
System.out.println("成功领取到奖品!");
//如果领取完奖品后还有奖品,则将当前状态设置为不可抽奖状态,否则设置为奖品领完状态
if(this.context.count > 0) {
this.context.currentState = Context.CANNOT_LUCKY_DRAW_STATE;
}else {
this.context.currentState = Context.PRIZE_EMPTY_STATE;
}
}
}
/**
*
* @ClassName PrizeEmptyState
* @Description 奖品领完状态
* @author fuling
* @date 2020年11月3日 下午3:58:40
*/
class PrizeEmptyState implements State{
Context context;
PrizeEmptyState(Context context){
this.context = context;;
}
@Override
public void deductPoints() {
System.out.println("奖品已经领完了");
}
@Override
public boolean luckyDraw() {
System.out.println("奖品已经领完了");
return false;
}
@Override
public void giveOutPrize() {
System.out.println("奖品已经领完了");
}
}
用状态模式的方案遵守了开闭原则,如果此时我们需要添加一种新状态,则只需要实现状态接口即可,不需要修改客户端代码。
状态模式小结
- 主要用来解决对象在多种状态转换时,需要对外输出不同的行为的问题。状态和行为是一一对应的,状态之间可以相互转换
- 当一个对象的状态改变时,允许其改变行为,这个对象看起来像是改变了其类
- 该模式主要有两个角色:Context类为环境角色,用于维护state的实例;State为抽象状态角色(状态接口),定义一个接口封装特定的行为活动
- 状态模式将每个状态的行为封装到对应的一个类中,代码有很强的可读性
- 将容易产生问题的if/else语句删除了,方便维护。如果把每个状态的行为都放到一个类中,每次调用方法时都要判断当前是什么状态
,不但会产生很多if/else语句,而且容易出错 - 符合开闭原则,容易增删状态
- 如果状态很多的话,会产生很多状态类,加大维护难度
- 当一个事件或者对象有很多种状态,状态之间会相互转换,对不同的状态要求有不同的行为的时候,可以考虑使用状态模式。