大话设计模式之七:16~20章(状态模式 、适配器模式、备忘录模式、组合模式、迭代器模式)

第十六章 :无尽加班何时休——状态模式
状态模式  

 
 
优点:
缺点:

所使用的项目是:工作状态

功能:

界面:


 
设计思路:
收获:

附:

1.概述

在软件开发过程中,应用程序可能会根据不同的情况作出不同的处理。最直接的解决方案是将这些所有可能发生的情况全都考虑到。然后使用if... ellse语句来做状态判断来进行不同情况的处理。但是对复杂状态的判断就显得“力不从心了”。随着增加新的状态或者修改一个状体(if else(或switch case)语句的增多或者修改)可能会引起很大的修改,而程序的可读性,扩展性也会变得很弱。维护也会很麻烦。那么我就考虑只修改自身状态的模式。

例子1:按钮来控制一个电梯的状态,一个电梯开们,关门,停,运行。每一种状态改变,都有可能要根据其他状态来更新处理。例如,开门状体,你不能在运行的时候开门,而是在电梯定下后才能开门。

例子2:我们给一部手机打电话,就可能出现这几种情况:用户开机,用户关机,用户欠费停机,用户消户等。 所以当我们拨打这个号码的时候:系统就要判断,该用户是否在开机且不忙状态,又或者是关机,欠费等状态。但不管是那种状态我们都应给出对应的处理操作。

2.问题

对象如何在每一种状态下表现出不同的行为

3.解决方案

状态模式:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。

在很多情况下, 一个对象的行为取决于一个或多个动态变化的属性,这样的属性叫做 状态,这样的对象叫做 有状态的 ( stateful ) 对象,这样的对象状态是从事先定义好的一系列值中取出的。当一个这样的对象与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化。

4.适用性

在下面的两种情况下均可使用State模式:
1) •  一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为。
2) •  代码中包含大量与对象状态有关的条件语句 :一个操作中含有庞大的多分支的条件( if else(或switch case)语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常 , 有多个操作包含这一相同的条件结构。 State模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。

5.结构


6.模式的组成

环境类(Context):  定义客户感兴趣的接口。维护一个ConcreteState子类的实例,这个实例定义当前状态。
抽象状态类(State):  定义一个接口以封装与Context的一个特定状态相关的行为。
具体状态类(ConcreteState):  每一子类实现一个与Context的一个状态相关的行为。

7.效果

State模式有下面一些效果:
状态模式的优点:
1 ) 它将与特定状态相关的行为局部化,并且将不同状态的行为分割开来: State模式将所有与一个特定的状态相关的行为都放入一个对象中。因为所有与状态相关的代码都存在于某一个State子类中, 所以通过定义新的子类可以很容易的增加新的状态和转换。另一个方法是使用数据值定义内部状态并且让 Context操作来显式地检查这些数据。但这样将会使整个Context的实现中遍布看起来很相似的条件if else语句或switch case语句。增加一个新的状态可能需要改变若干个操作, 这就使得维护变得复杂了。State模式避免了这个问题, 但可能会引入另一个问题, 因为该模式将不同状态的行为分布在多个State子类中。这就增加了子类的数目,相对于单个类的实现来说不够紧凑。但是如果有许多状态时这样的分布实际上更好一些, 否则需要使用巨大的条件语句。正如很长的过程一样,巨大的条件语句是不受欢迎的。它们形成一大整块并且使得代码不够清晰,这又使得它们难以修改和扩展。 State模式提供了一个更好的方法来组织与特定状态相关的代码。决定状态转移的逻辑不在单块的 i f或s w i t c h语句中, 而是分布在State子类之间。将每一个状态转换和动作封装到一个类中,就把着眼点从执行状态提高到整个对象的状态。这将使代码结构化并使其意图更加清晰。

2) 它使得状态转换显式化: 当一个对象仅以内部数据值来定义当前状态时 , 其状态仅表现为对一些变量的赋值,这不够明确。为不同的状态引入独立的对象使得转换变得更加明确。而且, State对象可保证Context不会发生内部状态不一致的情况,因为从 Context的角度看,状态转换是原子的—只需重新绑定一个变量(即Context的State对象变量),而无需为多个变量赋值

3) State对象可被共享 如果State对象没有实例变量—即它们表示的状态完全以它们的类型来编码—那么各Context对象可以共享一个State对象。当状态以这种方式被共享时, 它们必然是没有内部状态, 只有行为的轻量级对象。

状态模式的缺点:
1) 状态模式的使用必然会增加系统类和对象的个数。
2) 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。

8.实现

我们用电梯的例子来说明:

简单地实现代码:

//
abstract class ILift {  
    //电梯的四个状态  
    const OPENING_STATE = 1;  //门敞状态  
    const CLOSING_STATE = 2;  //门闭状态  
    const RUNNING_STATE = 3;  //运行状态  
    const STOPPING_STATE = 4; //停止状态;  
  
      
    //设置电梯的状态  
    public abstract function setState($state);  
  
    //首先电梯门开启动作  
    public abstract function open();  
  
    //电梯门有开启,那当然也就有关闭了  
    public abstract function close();  
  
    //电梯要能上能下,跑起来  
    public abstract function run();  
  
    //电梯还要能停下来,停不下来那就扯淡了  
    public abstract function stop();  
}  
  
/** 
 * 电梯的实现类  
 */   
class Lift extends  ILift {  
    private $state;  
  
    public function setState($state) {  
        $this->state = $state;  
    }  
    //电梯门关闭  
    public function close() {  
        //电梯在什么状态下才能关闭  
        switch($this->state){  
            case ILift::OPENING_STATE:  //如果是则可以关门,同时修改电梯状态  
                $this->setState(ILift::CLOSING_STATE);  
            break;  
            case ILift::CLOSING_STATE:  //如果电梯就是关门状态,则什么都不做  
                //do nothing;  
                return ;  
            break;  
            case ILift::RUNNING_STATE: //如果是正在运行,门本来就是关闭的,也说明都不做  
                //do nothing;  
                return ;  
            break;  
            case ILift::STOPPING_STATE:  //如果是停止状态,本也是关闭的,什么也不做  
                //do nothing;  
                return ;  
            break;  
        }  
                echo 'Lift colse <br>';  
    }  
  
    //电梯门开启  
    public function open() {  
        //电梯在什么状态才能开启  
        switch($this->state){  
            case ILift::OPENING_STATE: //如果已经在门敞状态,则什么都不做  
                //do nothing;  
                return ;  
            break;  
            case ILift::CLOSING_STATE: //如是电梯时关闭状态,则可以开启  
                $this->setState(ILift::OPENING_STATE);  
            break;  
            case ILift::RUNNING_STATE: //正在运行状态,则不能开门,什么都不做  
            //do nothing;  
                return ;  
            break;  
            case ILift::STOPPING_STATE: //停止状态,淡然要开门了  
                $this->setState(ILift::OPENING_STATE);  
            break;  
        }  
        echo 'Lift open <br>';  
    }  
    ///电梯开始跑起来  
    public function run() {  
        switch($this->state){  
            case ILift::OPENING_STATE: //如果已经在门敞状态,则不你能运行,什么都不做  
                //do nothing;  
                return ;  
            break;  
            case ILift::CLOSING_STATE: //如是电梯时关闭状态,则可以运行  
                $this->setState(ILift::RUNNING_STATE);  
            break;  
            case ILift::RUNNING_STATE: //正在运行状态,则什么都不做  
                //do nothing;  
                return ;  
            break;  
            case ILift::STOPPING_STATE: //停止状态,可以运行  
                $this->setState(ILift::RUNNING_STATE);  
        }  
        echo 'Lift run <br>';  
    }  
  
    //电梯停止  
    public function stop() {  
        switch($this->state){  
            case ILift::OPENING_STATE: //如果已经在门敞状态,那肯定要先停下来的,什么都不做  
                //do nothing;  
                return ;  
            break;  
            case ILift::CLOSING_STATE: //如是电梯时关闭状态,则当然可以停止了  
                $this->setState(ILift::CLOSING_STATE);  
            break;  
            case ILift::RUNNING_STATE: //正在运行状态,有运行当然那也就有停止了  
                $this->setState(ILift::CLOSING_STATE);  
            break;  
            case ILift::STOPPING_STATE: //停止状态,什么都不做  
                //do nothing;  
                return ;  
            break;  
        }  
        echo 'Lift stop <br>';  
    }  
      
}  
$lift = new Lift();   
     
//电梯的初始条件应该是停止状态   
$lift->setState(ILift::STOPPING_STATE);   
//首先是电梯门开启,人进去   
$lift->open();   
     
//然后电梯门关闭   
$lift->close();   
     
//再然后,电梯跑起来,向上或者向下   
$lift->run();      
 //最后到达目的地,电梯挺下来   
$lift->stop();  
//

显然我们已经完成了我们的基本业务操作,但是,我们在程序中使用了大量的switch…case这样的判断(if…else也是一样),首先是程序的可阅读性很差,其次扩展非常不方便。一旦我们有新的状态加入的话,例如新加通电和断点状态。我们势必要在每个业务方法里边增加相应的case语句。也就是四个函数open,close,run,stop都需要修改相应case语句。

状态模式:把不同状态的操作分散到不同的状态对象里去完成。看看状态类的uml类图


代码实现:

//
/** 
 *  
 * 定义一个电梯的接口  
 */   
abstract class LiftState{  
  
    //定义一个环境角色,也就是封装状态的变换引起的功能变化  
    protected  $_context;  
  
    public function setContext(Context $context){  
        $this->_context = $context;  
    }  
  
    //首先电梯门开启动作  
    public abstract function open();  
  
    //电梯门有开启,那当然也就有关闭了  
    public abstract function close();  
  
    //电梯要能上能下,跑起来  
    public abstract function run();  
  
    //电梯还要能停下来,停不下来那就扯淡了  
    public abstract function stop();  
  
}  
  
  
/** 
 * 环境类:定义客户感兴趣的接口。维护一个ConcreteState子类的实例,这个实例定义当前状态。 
 */   
class Context {  
    //定义出所有的电梯状态  
    static  $openningState = null;  
    static  $closeingState = null;  
    static  $runningState  = null;  
    static  $stoppingState = null;  
  
    public function __construct() {  
        self::$openningState = new OpenningState();  
        self::$closeingState = new ClosingState();  
        self::$runningState =  new RunningState();  
        self::$stoppingState = new StoppingState();  
  
    }  
  
    //定一个当前电梯状态  
    private  $_liftState;  
  
    public function getLiftState() {  
        return $this->_liftState;  
    }  
  
    public function setLiftState($liftState) {  
        $this->_liftState = $liftState;  
        //把当前的环境通知到各个实现类中  
        $this->_liftState->setContext($this);  
    }  
  
  
    public function open(){  
        $this->_liftState->open();  
    }  
  
    public function close(){  
        $this->_liftState->close();  
    }  
  
    public function run(){  
        $this->_liftState->run();  
    }  
  
    public function stop(){  
        $this->_liftState->stop();  
    }  
}  
  
/** 
 * 在电梯门开启的状态下能做什么事情  
 */   
class OpenningState extends LiftState {  
  
    /** 
     * 开启当然可以关闭了,我就想测试一下电梯门开关功能 
     * 
     */  
    public function close() {  
        //状态修改  
        $this->_context->setLiftState(Context::$closeingState);  
        //动作委托为CloseState来执行  
        $this->_context->getLiftState()->close();  
    }  
  
    //打开电梯门  
    public function open() {  
        echo 'lift open...', '<br/>';  
    }  
    //门开着电梯就想跑,这电梯,吓死你!  
    public function run() {  
        //do nothing;  
    }  
  
    //开门还不停止?  
    public function stop() {  
        //do nothing;  
    }  
  
}  
  
/** 
 * 电梯门关闭以后,电梯可以做哪些事情  
 */   
class ClosingState extends LiftState {  
  
    //电梯门关闭,这是关闭状态要实现的动作  
    public function close() {  
        echo 'lift close...', '<br/>';  
  
    }  
    //电梯门关了再打开,逗你玩呢,那这个允许呀  
    public function open() {  
        $this->_context->setLiftState(Context::$openningState);  //置为门敞状态  
        $this->_context->getLiftState()->open();  
    }  
  
    //电梯门关了就跑,这是再正常不过了  
    public function run() {  
        $this->_context->setLiftState(Context::$runningState); //设置为运行状态;  
        $this->_context->getLiftState()->run();  
    }  
  
    //电梯门关着,我就不按楼层  
      
    public function stop() {  
        $this->_context->setLiftState(Context::$stoppingState);  //设置为停止状态;  
        $this->_context->getLiftState()->stop();  
    }  
  
}  
  
/** 
 * 电梯在运行状态下能做哪些动作  
 */   
class RunningState extends LiftState {  
  
    //电梯门关闭?这是肯定了  
    public function close() {  
        //do nothing  
    }  
  
    //运行的时候开电梯门?你疯了!电梯不会给你开的  
    public function open() {  
        //do nothing  
    }  
  
    //这是在运行状态下要实现的方法  
    public function run() {  
        echo 'lift run...', '<br/>';  
    }  
  
    //这个事绝对是合理的,光运行不停止还有谁敢做这个电梯?!估计只有上帝了  
    public function stop() {  
        $this->_context->setLiftState(Context::$stoppingState); //环境设置为停止状态;  
        $this->_context->getLiftState()->stop();  
    }  
  
}  
  
  
  
/** 
 * 在停止状态下能做什么事情  
 */   
class StoppingState extends LiftState {  
  
    //停止状态关门?电梯门本来就是关着的!  
    public function close() {  
        //do nothing;  
    }  
  
    //停止状态,开门,那是要的!  
    public function open() {  
        $this->_context->setLiftState(Context::$openningState);  
        $this->_context->getLiftState()->open();  
    }  
    //停止状态再跑起来,正常的很  
    public function run() {  
        $this->_context->setLiftState(Context::$runningState);  
        $this->_context->getLiftState()->run();  
    }  
    //停止状态是怎么发生的呢?当然是停止方法执行了  
    public function stop() {  
        echo 'lift stop...', '<br/>';  
    }  
  
}  
  
/** 
 * 模拟电梯的动作  
 */   
class Client {  
  
    public static function main() {  
        $context = new Context();  
        $context->setLiftState(new ClosingState());  
  
        $context->open();  
        $context->close();  
        $context->run();  
        $context->stop();  
    }  
}  
Client::main();  
来源: http://blog.csdn.net/hguisu/article/details/7557252
//


第十七章在NBA 我需要翻译——适配器模式
适配器模式  


 
 
优点:
缺点:

所使用的项目是:姚明翻译

功能:

界面:

设计思路:

 

收获:

第十八章:如果再回到从前——备忘录模式
备忘录模式  在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样就可以将该对象恢复到原先保存的状态

 
 
优点:
缺点:

所使用的项目是:游戏角色状态

功能:

界面:

设计思路:

 
收获:

附:

每个人都有过后悔的时候,但人生并无后悔药,有些错误一旦发生就无法再挽回,有些人一旦错过就不会再回来,有些话一旦说出口就不可能再收回,这就是人生。为了不后悔,凡事我们都需要三思而后行。说了这么多,大家可能已经晕了,不是在学设计模式吗?为什么弄出这么一堆人生感悟来,呵呵,别着急,本章将介绍一种让我们可以在软件中实现后悔机制的设计模式——备忘录模式,它是软件中的“后悔药”,是软件中的“月光宝盒”。话不多说,下面就让我们进入备忘录模式的学习。

 

21.1 可悔棋的中国象棋

       Sunny软件公司欲开发一款可以运行在Android平台的触摸式中国象棋软件,由于考虑到有些用户是“菜鸟”,经常不小心走错棋;还有些用户因为不习惯使用手指在手机屏幕上拖动棋子,常常出现操作失误,因此该中国象棋软件要提供“悔棋”功能,用户走错棋或操作失误后可恢复到前一个步骤。如图21-1所示:


21-1  Android版中国象棋软件界面示意图

      如何实现“悔棋”功能是Sunny软件公司开发人员需要面对的一个重要问题,“悔棋”就是让系统恢复到某个历史状态,在很多软件中通常称之为“撤销”。下面我们来简单分析一下撤销功能的实现原理:

      在实现撤销时,首先必须保存软件系统的历史状态,当用户需要取消错误操作并且返回到某个历史状态时,可以取出事先保存的历史状态来覆盖当前状态。如图21-2所示:


21-2撤销功能示意图

      备忘录模式正为解决此类撤销问题而诞生,它为我们的软件提供了“后悔药”,通过使用备忘录模式可以使系统恢复到某一特定的历史状态。

21.2 备忘录模式概述

      备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原,当前很多软件都提供了撤销(Undo)操作,其中就使用了备忘录模式。

      备忘录模式定义如下:

备忘录模式(Memento Pattern):在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。它是一种对象行为型模式,其别名为Token

      备忘录模式的核心是备忘录类以及用于管理备忘录的负责人类的设计,其结构如图21-3所示:


      在备忘录模式结构图中包含如下几个角色:

       Originator(原发器):它是一个普通类,可以创建一个备忘录,并存储它的当前内部状态,也可以使用备忘录来恢复其内部状态,一般将需要保存内部状态的类设计为原发器。

      ●Memento(备忘录)存储原发器的内部状态,根据原发器来决定保存哪些内部状态。备忘录的设计一般可以参考原发器的设计,根据实际需要确定备忘录类中的属性。需要注意的是,除了原发器本身与负责人类之外,备忘录对象不能直接供其他类使用,原发器的设计在不同的编程语言中实现机制会有所不同。

      Caretaker(负责人):负责人又称为管理者,它负责保存备忘录,但是不能对备忘录的内容进行操作或检查。在负责人类中可以存储一个或多个备忘录对象,它只负责存储对象,而不能修改对象,也无须知道对象的实现细节。

      理解备忘录模式并不难,但关键在于如何设计备忘录类和负责人类。由于在备忘录中存储的是原发器的中间状态,因此需要防止原发器以外的其他对象访问备忘录,特别是不允许其他对象来修改备忘录。

      下面我们通过简单的示例代码来说明如何使用Java语言实现备忘录模式:

      在使用备忘录模式时,首先应该存在一个原发器类Originator,在真实业务中,原发器类是一个具体的业务类,它包含一些用于存储成员数据的属性,典型代码如下所示:

[java]  view plain  copy
  1. package dp.memento;  
  2. public class Originator {  
  3.     private String state;  
  4.   
  5.     public Originator(){}  
  6.   
  7.   // 创建一个备忘录对象  
  8.     public Memento createMemento() {  
  9.     return new Memento(this);  
  10.     }  
  11.   
  12.   // 根据备忘录对象恢复原发器状态  
  13.     public void restoreMemento(Memento m) {  
  14.      state = m.state;  
  15.     }  
  16.   
  17.     public void setState(String state) {  
  18.         this.state=state;  
  19.     }  
  20.   
  21.     public String getState() {  
  22.         return this.state;  
  23.     }  
  24. }  

      对于备忘录类Memento而言,它通常提供了与原发器相对应的属性(可以是全部,也可以是部分)用于存储原发器的状态,典型的备忘录类设计代码如下:

[java]  view plain  copy
  1. package dp.memento;  
  2. //备忘录类,默认可见性,包内可见  
  3. class Memento {  
  4.     private String state;  
  5.   
  6.     public Memento(Originator o) {  
  7.     state = o.getState();  
  8.     }  
  9.   
  10.     public void setState(String state) {  
  11.         this.state=state;  
  12.     }  
  13.   
  14.     public String getState() {  
  15.         return this.state;  
  16.     }  
  17. }  

      在设计备忘录类时需要考虑其封装性除了Originator类,不允许其他类来调用备忘录类Memento的构造函数与相关方法如果不考虑封装性,允许其他类调用setState()等方法,将导致在备忘录中保存的历史状态发生改变,通过撤销操作所恢复的状态就不再是真实的历史状态,备忘录模式也就失去了本身的意义。

      在使用Java语言实现备忘录模式时,一般通过将Memento类与Originator类定义在同一个包(package)中来实现封装,在Java语言中可使用默认访问标识符来定义Memento类,即保证其包内可见。只有Originator类可以对Memento进行访问,而限制了其他类对Memento的访问。在 Memento中保存了Originatorstate值,如果Originator中的state值改变之后需撤销,可以通过调用它的restoreMemento()方法进行恢复。

      对于负责人类Caretaker,它用于保存备忘录对象,并提供getMemento()方法用于向客户端返回一个备忘录对象,原发器通过使用这个备忘录对象可以回到某个历史状态。典型的负责人类的实现代码如下:

[java]  view plain  copy
  1. package dp.memento;  
  2. public class Caretaker {  
  3.     private Memento memento;  
  4.   
  5.     public Memento getMemento() {  
  6.         return memento;  
  7.     }  
  8.   
  9.     public void setMemento(Memento memento) {  
  10.         this.memento=memento;  
  11.     }  
  12. }  

      Caretaker类中不应该直接调用Memento中的状态改变方法,它的作用仅仅用于存储备忘录对象。将原发器备份生成的备忘录对象存储在其中,当用户需要对原发器进行恢复时再将存储在其中的备忘录对象取出。

 

思考

能否通过原型模式来创建备忘录对象?系统该如何设计?


21.3 完整解决方案

      为了实现撤销功能,Sunny公司开发人员决定使用备忘录模式来设计中国象棋软件,其基本结构如图21-4所示:


      在图21-4中,Chessman充当原发器,ChessmanMemento充当备忘录,MementoCaretaker充当负责人,在MementoCaretaker中定义了一个ChessmanMemento类型的对象,用于存储备忘录。完整代码如下所示:

[java]  view plain  copy
  1. //象棋棋子类:原发器  
  2. class Chessman {  
  3.     private String label;  
  4.     private int x;  
  5.     private int y;  
  6.   
  7.     public Chessman(String label,int x,int y) {  
  8.         this.label = label;  
  9.         this.x = x;  
  10.         this.y = y;  
  11.     }  
  12.   
  13.     public void setLabel(String label) {  
  14.         this.label = label;   
  15.     }  
  16.   
  17.     public void setX(int x) {  
  18.         this.x = x;   
  19.     }  
  20.   
  21.     public void setY(int y) {  
  22.         this.y = y;   
  23.     }  
  24.   
  25.     public String getLabel() {  
  26.         return (this.label);   
  27.     }  
  28.   
  29.     public int getX() {  
  30.         return (this.x);   
  31.     }  
  32.   
  33.     public int getY() {  
  34.         return (this.y);   
  35.     }  
  36.       
  37.     //保存状态  
  38.     public ChessmanMemento save() {  
  39.         return new ChessmanMemento(this.label,this.x,this.y);  
  40.     }  
  41.       
  42.     //恢复状态  
  43.     public void restore(ChessmanMemento memento) {  
  44.         this.label = memento.getLabel();  
  45.         this.x = memento.getX();  
  46.         this.y = memento.getY();  
  47.     }  
  48. }  
  49.   
  50. //象棋棋子备忘录类:备忘录  
  51. class ChessmanMemento {  
  52.     private String label;  
  53.     private int x;  
  54.     private int y;  
  55.   
  56.     public ChessmanMemento(String label,int x,int y) {  
  57.         this.label = label;  
  58.         this.x = x;  
  59.         this.y = y;  
  60.     }  
  61.   
  62.     public void setLabel(String label) {  
  63.         this.label = label;   
  64.     }  
  65.   
  66.     public void setX(int x) {  
  67.         this.x = x;   
  68.     }  
  69.   
  70.     public void setY(int y) {  
  71.         this.y = y;   
  72.     }  
  73.   
  74.     public String getLabel() {  
  75.         return (this.label);   
  76.     }  
  77.   
  78.     public int getX() {  
  79.         return (this.x);   
  80.     }  
  81.   
  82.     public int getY() {  
  83.         return (this.y);   
  84.     }     
  85. }  
  86.   
  87. //象棋棋子备忘录管理类:负责人  
  88. class MementoCaretaker {  
  89.     private ChessmanMemento memento;  
  90.   
  91.     public ChessmanMemento getMemento() {  
  92.         return memento;  
  93.     }  
  94.   
  95.     public void setMemento(ChessmanMemento memento) {  
  96.         this.memento = memento;  
  97.     }  
  98. }  

      编写如下客户端测试代码:

[java]  view plain  copy
  1. class Client {  
  2.     public static void main(String args[]) {  
  3.         MementoCaretaker mc = new MementoCaretaker();  
  4.         Chessman chess = new Chessman("车",1,1);  
  5.         display(chess);  
  6.         mc.setMemento(chess.save()); //保存状态       
  7.         chess.setY(4);  
  8.         display(chess);  
  9.         mc.setMemento(chess.save()); //保存状态  
  10.         display(chess);  
  11.         chess.setX(5);  
  12.         display(chess);  
  13.         System.out.println("******悔棋******");     
  14.         chess.restore(mc.getMemento()); //恢复状态  
  15.         display(chess);  
  16.     }  
  17.       
  18.     public static void display(Chessman chess) {  
  19.         System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");  
  20.     }  
  21. }  

      编译并运行程序,输出结果如下: 

棋子车当前位置为:第1行第1列。

棋子车当前位置为:第1行第4列。

棋子车当前位置为:第1行第4列。

棋子车当前位置为:第5行第4列。

******悔棋******

棋子车当前位置为:第1行第4列。

21.4 实现多次撤销

      Sunny软件公司开发人员通过使用备忘录模式实现了中国象棋棋子的撤销操作,但是使用上述代码只能实现一次撤销,因为在负责人类中只定义一个备忘录对象来保存状态,后面保存的状态会将前一次保存的状态覆盖,但有时候用户需要撤销多步操作。如何实现多次撤销呢?本节将提供一种多次撤销的解决方案,那就是在负责人类中定义一个集合来存储多个备忘录,每个备忘录负责保存一个历史状态,在撤销时可以对备忘录集合进行逆向遍历,回到一个指定的历史状态,而且还可以对备忘录集合进行正向遍历,实现重做(Redo)操作,即取消撤销,让对象状态得到恢复。

      改进之后的中国象棋棋子撤销功能结构图如图21-5所示:


      在图21-5中,我们对负责人类MementoCaretaker进行了修改,在其中定义了一个ArrayList类型的集合对象来存储多个备忘录,其代码如下所示:

[java]  view plain  copy
  1. import java.util.*;  
  2.   
  3. class MementoCaretaker {  
  4.     //定义一个集合来存储多个备忘录  
  5.     private ArrayList mementolist = new ArrayList();  
  6.   
  7.     public ChessmanMemento getMemento(int i) {  
  8.         return (ChessmanMemento)mementolist.get(i);  
  9.     }  
  10.   
  11.     public void setMemento(ChessmanMemento memento) {  
  12.         mementolist.add(memento);  
  13.     }  
  14. }  

      编写如下客户端测试代码:

[java]  view plain  copy
  1. class Client {  
  2. private static int index = -1//定义一个索引来记录当前状态所在位置  
  3.     private static MementoCaretaker mc = new MementoCaretaker();  
  4.   
  5.     public static void main(String args[]) {  
  6.         Chessman chess = new Chessman("车",1,1);  
  7.         play(chess);          
  8.         chess.setY(4);  
  9.         play(chess);  
  10.         chess.setX(5);  
  11.         play(chess);      
  12.         undo(chess,index);  
  13.         undo(chess,index);    
  14.         redo(chess,index);  
  15.         redo(chess,index);  
  16.     }  
  17.       
  18.     //下棋  
  19.     public static void play(Chessman chess) {  
  20.         mc.setMemento(chess.save()); //保存备忘录  
  21.         index ++;   
  22.         System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");  
  23.     }  
  24.   
  25.     //悔棋  
  26.     public static void undo(Chessman chess,int i) {  
  27.         System.out.println("******悔棋******");  
  28.         index --;   
  29.         chess.restore(mc.getMemento(i-1)); //撤销到上一个备忘录  
  30.         System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");  
  31.     }  
  32.   
  33.     //撤销悔棋  
  34.     public static void redo(Chessman chess,int i) {  
  35.         System.out.println("******撤销悔棋******");   
  36.         index ++;   
  37.         chess.restore(mc.getMemento(i+1)); //恢复到下一个备忘录  
  38.         System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");  
  39.     }  
  40. }   

      编译并运行程序,输出结果如下:

棋子车当前位置为:第1行第1列。

棋子车当前位置为:第1行第4列。

棋子车当前位置为:第5行第4列。

******悔棋******

棋子车当前位置为:第1行第4列。

******悔棋******

棋子车当前位置为:第1行第1列。

******撤销悔棋******

棋子车当前位置为:第1行第4列。

******撤销悔棋******

棋子车当前位置为:第5行第4列。

 

 

扩展

本实例只能实现最简单的UndoRedo操作,并未考虑对象状态在操作过程中出现分支的情况。如果在撤销到某个历史状态之后,用户再修改对象状态,此后执行Undo操作时可能会发生对象状态错误,大家可以思考其产生原因。【注:可将对象状态的改变绘制成一张树状图进行分析。】

在实际开发中,可以使用链表或者堆栈来处理有分支的对象状态改变,大家可通过链表或者堆栈对上述实例进行改进。

21.5 再谈备忘录的封装

      备忘录是一个很特殊的对象,只有原发器对它拥有控制的权力,负责人只负责管理,而其他类无法访问到备忘录,因此我们需要对备忘录进行封装。

      为了实现对备忘录对象的封装,需要对备忘录的调用进行控制,对于原发器而言,它可以调用备忘录的所有信息,允许原发器访问返回到先前状态所需的所有数据;对于负责人而言,只负责备忘录的保存并将备忘录传递给其他对象;对于其他对象而言,只需要从负责人处取出备忘录对象并将原发器对象的状态恢复,而无须关心备忘录的保存细节。理想的情况是只允许生成该备忘录的那个原发器访问备忘录的内部状态。

      在实际开发中,原发器与备忘录之间的关系是非常特殊的,它们要分享信息而不让其他类知道,实现的方法因编程语言的不同而有所差异,在C++中可以使用friend关键字,让原发器类和备忘录类成为友元类,互相之间可以访问对象的一些私有的属性;在Java语言中可以将原发器类和备忘录类放在一个包中,让它们之间满足默认的包内可见性,也可以将备忘录类作为原发器类的内部类,使得只有原发器才可以访问备忘录中的数据,其他对象都无法使用备忘录中的数据。

 

思考

如何使用内部类来实现备忘录模式?


21.6 备忘录模式总结

      备忘录模式在很多软件的使用过程中普遍存在,但是在应用软件开发中,它的使用频率并不太高,因为现在很多基于窗体和浏览器的应用软件并没有提供撤销操作。如果需要为软件提供撤销功能,备忘录模式无疑是一种很好的解决方案。在一些字处理软件、图像编辑软件、数据库管理系统等软件中备忘录模式都得到了很好的应用。

 

      1.主要优点

      备忘录模式的主要优点如下:

      (1)它提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原。

      (2)备忘录实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。备忘录保存了原发器的状态,采用列表、堆栈等集合来存储备忘录对象可以实现多次撤销操作。

 

      2.主要缺点

      备忘录模式的主要缺点如下:

      资源消耗过大,如果需要保存的原发器类的成员变量太多,就不可避免需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源。

 

      3.适用场景

      在以下情况下可以考虑使用备忘录模式:

      (1)保存一个对象在某一个时刻的全部状态或部分状态,这样以后需要时它能够恢复到先前的状态,实现撤销操作。

      (2)防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象。

 

练习

Sunny软件公司正在开发一款RPG网游,为了给玩家提供更多方便,在游戏过程中可以设置一个恢复点,用于保存当前的游戏场景,如果在后续游戏过程中玩家角色“不幸牺牲”,可以返回到先前保存的场景,从所设恢复点开始重新游戏。试使用备忘录模式设计该功能。

 

【作者:刘伟  http://blog.csdn.net/lovelion



第十九章:分公司=一部门——组合模式
组合模式  将对象组合成树形结构以表示“部分-整体”的层次结构。组合(Composite)模式使得用户对单个对象和组合对象的使用具有一致性。

 
 
优点:
缺点:

所使用的项目是:无

功能:

界面:

设计思路:

  上图,是一个公司的组织结构图,总部下面有多个子公司,同时总部也有各个部门,子公司下面有多个部门。如果对这样的公司开发一个OA系统,作为程序员的你,如何设计这个OA系统呢?先不说如何设计实现,接着往下看,看完了下面的内容,再回过头来想怎么设计这样的OA系统。

组合模式(Composite)将小对象组合成树形结构,使用户操作组合对象如同操作一个单个对象。组合模式定义了“部分-整体”的层次结构,基本对象可以被组合成更大的对象,而且这种操作是可重复的,不断重复下去就可以得到一个非常大的组合对象,但这些组合对象与基本对象拥有相同的接口,因而组合是透明的,用法完全一致。

我们这样来简单的理解组合模式,组合模式就是把一些现有的对象或者元素,经过组合后组成新的对象,新的对象提供内部方法,可以让我们很方便的完成这些元素或者内部对象的访问和操作。我们也可以把组合对象理解成一个容器,容器提供各种访问其内部对象或者元素的API,我们只需要使用这些方法就可以操作它了。


 

收获:

第二十章:想走?可以,先买票——迭代器模式
原则  

 
 
优点:
缺点:

所使用的项目是:乘车买票

功能:

界面:

设计思路:

收获:

猜你喜欢

转载自blog.csdn.net/qq_38880380/article/details/80380607