Java设计模式之十二 ---- 备忘录模式和状态模式

前言

上一篇中我们学习了行为型模式的策略模式(Strategy Pattern)和模板模式(Template Pattern)。本篇则来学习下行为型模式的两个模式,备忘录模式(Memento Pattern)和状态模式(Memento Pattern)。

备忘录模式

简介

备忘录模式(Memento Pattern)用于保存一个对象的某个状态,以便在适当的时候恢复对象,该模式属于行为型模式
其主要目的是在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。

备忘录模式,其主要的的思想就是备份。例如,txt、word等文档的保存,游戏的存档,操作系统的备份,也包括我们经常用的快捷键Ctrl+Z等等。除了以上的这些应用,我们在编程中接触最多的估计就是数据库的事物了,它提供了一种恢复机制,可在出现异常的时候进行还原。

备忘录模式主要由这三个角色组成,备忘录角色(Memento)、发起人角色(Originator)和负责人(Caretaker)角色。

  • 备忘录(Memento):主要的功能是包含要被恢复的对象的状态。
  • 发起人(Originator):在创建的时候,会在备忘录对象中存储状态。
  • 负责人(Caretaker):主要是负责从备忘录对象中恢复对象的状态。

示例图如下:
Java设计模式之十二 ---- 备忘录模式和状态模式

我们这里依旧用一个示例来进行说明吧。
我们在玩游戏有的时候,会经常用到一个游戏功能,那就是存档。主要是为了保存游戏进度,防止信息丢失,并且也可以通过进行读档恢复保存的信息。比如xuwujing在玩一个游戏的时候,创建好角色之后进行打怪练级,然后挑战BOSS,不过担心挑战失败,于是便在挑战前进行存档,存档成功之后,再来进行挑战BOSS。
那么我们可以根据这个场景来使用备忘录模式来进行开发。

首先定义一个Memento,也就是游戏存档的信息,主要存储游戏人物等级和生命值。


class SaveMsg{
   private  int level;
   private int life;
       
   public SaveMsg( int level, int life) {
       super();
       this.level = level;
       this.life = life;
   }
   public int getLevel() {
       return level;
   }
   public void setLevel(int level) {
       this.level = level;
   }
   public int getLife() {
       return life;
   }
   public void setLife(int life) {
       this.life = life;
   }
}

然后再定义一个Originator,这里就是玩家了,除了等级和生命值信息外,玩家还可以进行存档、读档,以及进行一些活动,打怪练级和挑战BOSS。

那么代码如下:

class Player {
   //等级
   private  int level;
   //生命值
   private int life;

   public Player( int level, int life) {
       super();
       this.level = level;
       this.life = life;
   }
   //保存信息
   public SaveMsg saveStateToMemento() {
       return new SaveMsg(level,life);
   }
   
   //恢复信息
   public void getStateFromMemento(SaveMsg sm) {
       this.level = sm.getLevel();
       this.life = sm.getLife();
   }
   
   //获取当前状态
   public void getStatus() {
       System.out.println("玩家xuwujing当前信息:");
       System.out.println("人物等级:"+level+",人物生命:"+life);
   }
   
   //练级
   public void leveling() {
       this.level = this.level+1;
       this.life = this.life+10;
       System.out.println("恭喜玩家xuwujing升级!等级提升了1,生命提升了10!");
   }
   
   //挑战BOSS
   public boolean challengeBOSS() {
       //设置条件
       return this.level>2&&this.life>100;
   }
}

最后在定义一个Caretaker,作为游戏存档页,用于保存存档信息。
代码如下:

class GameSavePage{
    private SaveMsg sm;

    public SaveMsg getSm() {
        return sm;
    }
    public void setSm(SaveMsg sm) {
        this.sm = sm;
    }
    
}

编写好之后,那么我们来进行测试。

相应的测试代码如下:

    public static void main(String[] args) {
        int level = 1;
        int life = 100;
        //创建一个玩家
        Player player =new Player(level, life);
        System.out.println("玩家xuwujing进入游戏!");
        //状态
        player.getStatus();
        //进行练级
        player.leveling();
        GameSavePage savePage =new GameSavePage();
        //状态
        player.getStatus();
        System.out.println("玩家xuwujing正在存档...");
        //第一次存档
        savePage.setSm(player.saveStateToMemento());
        System.out.println("玩家xuwujing存档成功!");
        System.out.println("玩家xuwujing挑战新手村的BOSS!");
         boolean flag=player.challengeBOSS();
        if(flag) {
            System.out.println("玩家xuwujing挑战BOSS成功!");
            return;
        }
        System.out.println("玩家xuwujing挑战BOSS失败!游戏结束!开始读取存档...");
        savePage.getSm();
        System.out.println("玩家xuwujing读取存档成功!");
        //进行练级
        player.leveling();
        //状态
        player.getStatus();
        System.out.println("玩家xuwujing挑战新手村的BOSS!");
        flag=player.challengeBOSS();
        if(flag) {
            System.out.println("玩家xuwujing挑战BOSS成功!");
            return;
        }
}

输出结果:

        玩家xuwujing进入游戏!
        玩家xuwujing当前信息:
        人物等级:1,人物生命:100
        恭喜玩家xuwujing升级!等级提升了1,生命提升了10!
        玩家xuwujing当前信息:
        人物等级:2,人物生命:110
        玩家xuwujing正在存档...
        玩家xuwujing存档成功!
        玩家xuwujing挑战新手村的BOSS!
        玩家xuwujing挑战BOSS失败!游戏结束!开始读取存档...
        玩家xuwujing读取存档成功!
        恭喜玩家xuwujing升级!等级提升了1,生命提升了10!
        玩家xuwujing当前信息:
        人物等级:3,人物生命:120
        玩家xuwujing挑战新手村的BOSS!
        玩家xuwujing挑战BOSS成功!

备忘录模式优点

给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态;
实现了信息的封装,使得用户不需要关心状态的保存细节;

备忘录模式缺点

非常的消耗资源;
客户端必须知道所有的策略类才能进行调用;

使用场景:

需要保存/恢复数据的相关状态场景;

状态模式

简介

状态模式(State Pattern)属于行为型模式,其状态的对象和一个行为随着状态对象改变而改变
其主要目的解决的是当控制一个对象状态转换的条件表达式过于复杂是的情况。把状态的判断逻辑转移到表示不同状态一系列类中,可以把复杂的判断简单化。

状态模式,其主要的的思想就是提供一种状态,提供给客户端进行调用。状态可谓无处不在,无论是电脑、手机等电子产品的开机和关机的状态,还是经常用到的网络在线和离线状态,即使是在我们编程中Tcp也有创建、监听、关闭状态。

状态模式主要由环境角色(Context)、 抽象状态(State)和具体状态(Concrete State)组成。

  • 环境角色(Context): 它定义了客户程序需要的接口并维护一个具体状态角色的实例,将与状态相关的操作委托给当前的具体状态对象来处理。

  • 抽象状态角色(State): 定义一个接口以封装使用上下文环境的的一个特定状态相关的行为。
  • 具体状态角色(Concrete State):实现抽象状态定义的接口。

示例图如下:
Java设计模式之十二 ---- 备忘录模式和状态模式

这里为了方便理解,我们依旧使用一个简单的示例来加以说明。
我们在使用耳机听音乐的时候,一般会有两个状态,播放和暂停,按一下是从暂停到播放,再按一下就是从播放到暂停。那么我们可以根据这个场景来使用状态模式进行开发!

首先依旧定义一个抽象状态角色,用于表示音乐的状态,并指定该类的行为,也就是方法,这个抽象类的代码如下:

interface MusicState{
   void press();
}

定义好该抽象类之后,我们再来定义具体状态角色类。这里定义两个状态,一个是播放状态,一个是暂停状态,代码如下:

class PlayState implements MusicState{

   @Override
   public void press() {
       System.out.println("播放音乐!");
   }
}

class PauseState implements MusicState{

   @Override
   public void press() {
       System.out.println("暂停音乐!");
   }
}

然后在来定义一个环境角色,用于对客户端提供一个接口,并对状态进行处理。代码如下:

class Headset{
   private MusicState state;
   private int i;
   public Headset(MusicState state){
       this.state=state;
   }
   public void press() {
       if((i&1)==0) {
           this.state=new PlayState();
       }else {
           this.state=new PauseState();
       }
       this.state.press();
       i++;
   }
   public MusicState getState() {
       return state;
   }
   public void setState(MusicState state) {
       this.state = state;
   }
   
}

最后再来进行测试,测试代码如下:


public static void main(String[] args) {
       Headset hs = new Headset(new PlayState());
       //第一次播放音乐
       hs.press();
       //第二次暂停音乐
       hs.press();
       //第三次播放音乐
       hs.press();

}

输出结果:

    播放音乐!
    暂停音乐!
    播放音乐!

状态模式优点:

扩展性好,将和状态有关的行为放到一起,增加新的的状态,只需要改变对象状态即可改变对象的行为即可;
复用性好,让多个环境对象共享一个状态对象,从而减少系统中对象的个数;

状态模式缺点:

使用状态模式会增加系统类和对象的个数,并且该模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱;
状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。

使用场景:

行为随状态改变而改变的场景;
条件、分支语句的代替者。

注意事项 :

在行为受状态约束的时候使用状态模式,而且状态不超过5个。

** 和策略模式比较:**
在学习状态模式的时候,很容易和策略模式搞混,因为它们实在是太像了,很难区分,在查阅一番资料之后,整理了如下的相同点和区别点。

相同点:

  1. 它们很容易添加新的状态或策略,而且不需要修改使用它们的Context对象。
  2. 它们都符合OCP原则,在状态模式和策略模式中,Context对象对修改是关闭的,添加新的状态或策略,都不需要修改Context。
  3. 它们都会初始化。
  4. 它们都依赖子类去实现相关行为。

区别点

    1. 状态模式的行为是平行性的,不可相互替换的;
    2. 而策略模式的行为是平等性的,是可以相互替换的。
    3. 最重要的一个不同之处是,策略模式的改变由客户端完成;
    4. 而状态模式的改变,由环境角色或状态自己.