学习设计模式《十八》——备忘录模式

发布于:2025-07-18 ⋅ 阅读:(48) ⋅ 点赞:(0)

一、基础概念

        备忘录模式的本质是【保存和恢复内部状态】。

备忘录模式的思考
序号 备忘录模式的思考 说明
1 保存是手段,恢复才是目的 标准的备忘录模式保存数据的手段是通过内存缓存;广义的备忘录模式实现的时候,可以采用离线存储的方式,把这些数据保存到文件或者数据库等地方
2 备忘录模式备忘些什么内容呢? 备忘的就是原发器对象的内部状态,这些内部状态是不对外的,只有原发器对象才能够进行操作
3 备忘录模式为什么要保存数据呢? 目的是为了在有需要的时候,恢复原发器对象的内部状态,所以恢复才是备忘录模式的目的

1-对于备忘录模式最主要的一个特点【封装状态的备忘录对象】不应该被除了原发器对象之外的对象访问,至于如何存储都是小事。

2-备忘​​​​​​​录模式要解决的主要问题是【在不破坏对象封装性的前提下,来保存和恢复对象的内部状态】这是一个很主要的判断依据

        备忘录模式的定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态了

认识备忘录模式
序号 认识备忘录模式 说明
1 备忘录模式的功能 在不破坏封装性的前提下,捕获一个对象的内部状态,这里有两点要注意:
《1》一个是不破坏封装性(即:对象不能暴露它不应该暴露的细节);
《2》另一个是捕获的对象的内部状态(且通常还是运行期间某个时候对象的内部状态)
为什么要捕获这个对象内部状态呢?捕获这个内部状态有什么用?
《1》是为了在以后的某个时候,将该对象的状态恢复到备忘录所保存的状态,这才是备忘录真正的目的;
捕获的状态存放在哪里呢?
        在备忘录模式中,捕获的内部状态存储在备忘录对象中;而备忘录对象通常会被存储在原发器对象之外(即:被保存状态对象的外部)通常是放在管理者对象那里。
2 备忘录对象 就是用来记录原发起需要保存的状态对象(简单点的实现就是封装数据的对象)
 备忘录对象和普通的封装数据对象是有区别的:
《1》备忘录对象一般只让原发器对象来操作(为了保证这一点,通常备忘录对象作为原发器对象的内部类来实现,且实现为私有,这样就断绝了外部来访问这个备忘录对象的途径);备忘录对象需要保存在原发器对象之外,为了与外部交互,通常备忘录对象都会实现一个窄接口来标识对象类型。
《2》普通封装的数据对象是谁都可以使用;
3 原发器对象 就是需要被保存状态的对象,也有可能需要恢复状态的对象(原发器对象一般会包含备忘录对象的实现)
        《1》通常原发器对象应该提供捕获某个时刻对象内部状态的方法,在这个方法中,原发器对象会创建备忘录对象,把需要保存的状态数据设置到备忘录对象中,然后把备忘录对象提供给管理者对象来保存
        《2》当然,原发器对象也应该提供这样的方法(按照外部要求来恢复内部状态到某个备忘录对象记录的状态)。
4 管理者对象 主要是负责保存备忘录对象:
《1》并不一定要特别的做出一个管理者对象来(广义地说,调用原发器获得备忘录对象后,备忘录对象放在哪里,哪个对象就可以算是管理者对象);
《2》管理者对象并不是只能管理一个备忘录对象,一个管理者对象可以管理很多的备忘录对象。
《3》狭义的管理者对象是只管理同一类的备忘录对象,但广义的管理者对象是可以管理不同类型的备忘录对象的。
《4》管理者对象需要实现的基本功能主要是:存入备忘录对象、保存备忘录对象和获取备忘录对象(从功能上看,就是一个缓存功能的实现,或者是一个简单的对象实例池的实现)。
《5》管理者虽然能存取备忘录对象,但是不能访问备忘录对象内部的数据
5 窄接口和宽接口 在备忘录模式中,为了控制备忘录对象的访问,出现了窄接口和宽接口的概念:
《1》窄接口:管理者只能看到备忘录的窄接口,窄接口的实现中通常没有任何的方法,只是一个类型标识。窄接口使得管理者只能将备忘录传递给其他对象
《2》宽接口:原发器能够看到一个宽接口,允许它访问所需的所有数据,来返回到先前的状态(理想情况是:只允许生成备忘录的原发器来访问该备忘录的内部状态,通常实现成为原发器内的一个私有内部类)
即:我们的备忘录模式一示例中:定义了【IFlowAMockMemo】接口,里面没有定义任何方法,然后让备忘录来实现这个接口,从而标识备忘录就是这么一个IFlowAMockMemo的类型,这个接口就是窄接口;备忘录对象是实现在原发器内的一个私有内部类,只有原发器对象可以访问它,原发器可以访问到备忘录对象中所有的内部状态,这就是宽接口
这是备忘录模式的标准实现方式(即:窄接口没有任何方法,把备忘录对象实现成为原发器对象的私有内部类) 
               
能否在窄接口中提供备忘录对象对外的方法?变相提供一个宽点的接口呢?
        通常情况是不会这么做的(因为这样一来,所有能拿到这个接口的对象就可以通过这个接口来访问备忘录内部的数据或功能,这违反了备忘录模式的初衷【备忘录模式要求“在不破坏封装性”的前提下】如果这么做,那就等于是暴露了内部细节。因此,备忘录模式在实现的时候,对外多是采用窄接口,而且通常不会定义任何方法)。       
6

使用备忘录模式

潜在的代价

标准的备忘录模式的实现机制是依靠缓存来实现的,因此,当需要备忘录的数据量较大时,或者是存储的备忘录对象数据量不大但是数量很多的时候,或者是用户很频繁地创建备忘录对象时,这些都会导致非常大的内存开销。因此在使用备忘录模式的时候,一定要好好思考应用的环境,如果使用的代价太高,就不要选择备忘录模式,可以采用其他替代方案。
7 增量存储 如果需要频繁地创建备忘录对象,而且创建和应用备忘录对象来恢复状态的顺序是可控的,那么可以让备忘录进行增量存储,也就是备忘录可以仅仅存储原发器内部相对于上一次存储状态后的增量改变。
(如:在命令模式实现可撤销命令的实现中,就可以使用备忘录来保存每个命令对象的状态,然后在撤销命令的时候,使用备忘录来恢复这些状态。由于命令的历史列表是按照命令操作的顺序来存放的,也是按照这个历史列表来进行取消和重做的,因此顺序是可控的,
那么这种情况,还可以让备忘录对象只存储一个命令所产生的增量改变而不是它所影响的每一个对象的完整状态)。
备忘录模式的优缺点
序号 备忘录模式的优点 备忘录模式的缺点
1

更好的封装性

        通过使用备忘录对象,来封装原发器对象的内部状态,虽然这个对象是保存在原发器对象的外部,但是由于备忘录对象的窄接口并不提供任何方法,这样有效保证了对原发器对象内部状态的封装,不把原发器对象的内部实现细节暴露给外部。

可能会导致高开销

        备忘录模式基本的功能就是备忘录对象的存储和恢复,它的基本实现方式就是缓存备忘录对象。这样一来,如果需要缓存的数量很大,或者是特别频繁地创建备忘录对象,开销是很大的。

2

简化了原发器

        备忘录对象被保存到原发器对象之外,让客户来管理他们的请求状态,从而让原发器对象得到简化。

3

窄接口和宽接口

        引入窄接口和宽接口,使得不同的地方,对备忘录对象的访问是不一样的。窄接口保证了只有原发器才可以访问备忘录对象的状态。

        何时选用备忘录模式?

        《1》如果必须保存一个对象在某一个时刻的全部或者部分状态,方便在以后需要的时候,可以把该对象恢复到先前的状态,可以使用备忘录模式【使用备忘录对象来封装和保存需要保存的内部状态,然后把备忘录对象保存到管理者对象中,在需要的时候,再从管理者对象中获取备忘录对象,来恢复对象的状态】。
        《2》如果需要保存一个对象的内部状态,但是如果用接口来让其他对象直接得到这些需要保存的状态,将会暴露对象的实现细节并破坏对象的封装性,这时可以使用备忘录模式【把备忘录对象实现细节成为原发器对象的内部类,而且还是私有,从而保证了只有原发器对象才能访问该备忘录对象;这样即保存了需要保存的状态,又不会暴露内部实现细节】。

二、备忘录模式示例

        业务需求:现在有一个仿真应用(功能是:模拟运行针对某个问题的多个解决方案,记录运行过程中的各种数据,在模拟运行完成之后,方便对这个多个解决方案进行比较和评价,从而选定最优的解决方案)。这种仿真系统,在很多领域都有应用(如:工作流系统,对同一个问题制定多个流程,然后通过仿真运行,最后来确定最优的流程作为解决方案;在工业设计和制造领域,仿真系统的应用就更加广泛了)。

        由于都是解决同一个具体的问题,这多个解决方案并不是完全一样的,现在我们假定它们的前半部分运行是完全一样的,只是在后半部分采用了不同的解决方案,后半部分需要使用前半部分运行所产生的数据

 2.1、不使用模式的示例

要保证初始数据一致,实现思路是:

《1》模拟运行流程的第一个阶段,得到后阶段各个方案运行所需的数据,并把数据保存下来,方便之后的各个方案使用。

《2》每次在模拟运行某一个方案之前,用保存的数据去重新设置模拟运行流程的对象,这样在运行后面不同的方案时,对于这些方案来说,初始的数据就都是一样的了。

  2.1.1、编写模拟运行流程A

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MemoPattern.NoPattern
{
    /// <summary>
    /// 模拟运行流程A(这是这是一个示意,用于指代具体流程)
    /// </summary>
    internal class FlowAMock
    {
        //流程名称(不用额外存储的状态数据)
        public string FlowName { get; set; }=string.Empty;
        //示意:代指某个中间的结果(需要外部存储的状态数据)
        public int TempResult { get; set; } = 0;
        //示意:代指某个中间结果(需要外部存储的状态数据)
        public string TempState { get; set; } = string.Empty;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="flowName">流程名称</param>
        public FlowAMock(string flowName)
        {
            this.FlowName = flowName;
        }

        /// <summary>
        /// 示意:运行流程的第一个阶段
        /// </summary>
        public void RunPhaseOne()
        {
            //在这个阶段可能产生了中间结果,这里仅作示意
            TempResult = 3;
            TempState = "PhaseOne";
        }

        /// <summary>
        /// 示意:按照方案一运行流程后半部分
        /// </summary>
        public void SchemaOne()
        {
            //示意:需要使用第一阶段产生的数据
            this.TempState += "[SchemaOne]";
            Console.WriteLine($"方案一目前的状态是【{TempState}】结果是【{TempResult}】");
            TempResult += 11;
        }

        /// <summary>
        /// 示意:按照方案二运行流程后半部分
        /// </summary>
        public void SchemaTwo()
        {
            //示意:需要使用第一阶段产生的数据
            this.TempState += "[SchemaTwo]";
            Console.WriteLine($"方案二目前的状态是【{TempState}】结果是【{TempResult}】");
            TempResult += 22;
        }

    }//Class_end
}

  2.1.2、客户端测试

namespace MemoPattern
{
    internal class Program
    {
        static void Main(string[] args)
        {
            TestFlowAMock();

            Console.ReadLine();
        }

        /// <summary>
        /// 测试模拟运行流程
        /// </summary>
        private static void TestFlowAMock()
        {
            Console.WriteLine("------测试模拟运行流程------");
            NoPattern.FlowAMock flowAMock = new NoPattern.FlowAMock("TestFlow");

            //运行流程的第一阶段
            flowAMock.RunPhaseOne();
            //获取到运行流程第一阶段后产生的数据,提供给后续使用【可确保后续流程的方案一、方案二使用的基础数据是一样的】
            int tempResult = flowAMock.TempResult;
            string tempState=flowAMock.TempState;

            //按照方案一运行流程后半部分
            flowAMock.SchemaOne();
            //运行完成方案一后将数据重设回去
            flowAMock.TempResult = tempResult;
            flowAMock.TempState = tempState;

            //按照方案二运行流程后半部分
            flowAMock.SchemaTwo();
        }

    }//Class_end
}

  2.1.3、运行结果

 这个不使用模式的方式是满足了业务的需求【即:在运行第一个阶段后,方案一和方案二运行时的初始数据是一样的】。但是这个实现有两个问题:

        《1》数据都是零散存放在外部,若需要存放的外部数据很多,就会很杂乱(可定义一个对象来封装);

        《2》还有一个严重问题是:运行期间的数据都放到外部存储起来了,模拟流程的对象被迫将内部数据结构开放给外部,这暴露了对象的实现细节,且破坏了对象的封装性【正常来说这些数据只是模拟流程对象内部的数据,不应该对外开放】

 2.2、备忘录模式的示例1

        需要在运行期间捕获模拟流程运行对象的内部状态即:这个需求就是运行第一个阶段产生的内部数据,并且在该对象之外来保存这些状态,因为在后面它有不同的运行方案。但是这些不同的运行方案需要的初始数据是一样的,都是流程在第一阶段运行产生的数据】。

那么如何能够在不破坏对象封装性的前提下,来保存和恢复对象的状态?

        《1》备忘录模式引入了一个存储状态的备忘录对象,为了让外部无法访问这个对象的值,一般把这个对象实现为需要保存数据对象的内部类,且是私有的;这样除了需要保存数据的对象,外部无法访问到这个备忘录对象的数据,就保证了对象的封装性不被破坏。

        《2》备忘录对象还需要存储在外部,为了避免让外部访问到这个对象内部的数据,备忘录模式还引入了一个备忘录对象的窄接口,这个接口一般都是空的(什么方法也没有);这样外部存储的地方,只知道存储了一些备忘录接口的对象,但由于接口是空的,它们无法通过接口去访问备忘录对象内部的数据】。

如何使用备忘录模式?

《1》模拟流程运行的对象就相当于备忘录模式中的原发器;

《2》在模拟流程对象里面创建一个内部私有类作为备忘录对象来存储数据;

《3》为了和管理者对象交互,管理者需要知道保存对象的类型,就提供一个备忘录对象的窄接口来供管理者使用。

  2.2.1、定义窄接口提供给管理者使用

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MemoPattern.MemoDemoOne
{
    /// <summary>
    /// 备忘录的窄接口(没有任何方法定义)
    /// </summary>
    internal interface IFlowAMockMemo
    {

    }//Interface_end
}

  2.2.2、实现模拟运行流程A

        模拟运行流程A对象相当于原发器对象(该类里面原有的内部属性状态内容不用再暴露出去【即:不在对外提供公有的属性方法】;其次就是在该类中提供私有的备忘录对象,里面封装想要保存的内部状态,同时让这个备忘录对象实现窄接口;最后就是这个类中提供创建备忘录对象和根据备忘录对象恢复内部状态的方法)。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MemoPattern.MemoDemoOne
{
    /// <summary>
    /// 模拟运行流程A(这是这是一个示意,用于指代具体流程)【备忘录模式的原发器对象】
    /// </summary>
    internal class FlowAMock
    {
        //流程名称(不用额外存储的状态数据)
        private string flowName = string.Empty;
        //示意:代指某个中间的结果(需要外部存储的状态数据)
        private int tempResult = 0;
        //示意:代指某个中间结果(需要外部存储的状态数据)
        private string tempState = string.Empty;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="flowName">流程名称</param>
        public FlowAMock(string flowName)
        {
            this.flowName = flowName;
        }

        /// <summary>
        /// 示意:运行流程的第一个阶段
        /// </summary>
        public void RunPhaseOne()
        {
            //在这个阶段可能产生了中间结果,这里仅作示意
            tempResult = 3;
            tempState = "PhaseOne";
        }

        /// <summary>
        /// 示意:按照方案一运行流程后半部分
        /// </summary>
        public void SchemaOne()
        {
            //示意:需要使用第一阶段产生的数据
            this.tempState += "[SchemaOne]";
            Console.WriteLine($"方案一目前的状态是【{tempState}】结果是【{tempResult}】");
            tempResult += 11;
        }

        /// <summary>
        /// 示意:按照方案二运行流程后半部分
        /// </summary>
        public void SchemaTwo()
        {
            //示意:需要使用第一阶段产生的数据
            this.tempState += "[SchemaTwo]";
            Console.WriteLine($"方案二目前的状态是【{tempState}】结果是【{tempResult}】");
            tempResult += 22;
        }

        #region 备忘录内容

        /// <summary>
        /// 继承备忘录接口实现真正的备忘录功能(实现内部私有类,不让外部访问)
        /// </summary>
        private class MemoImpl : IFlowAMockMemo
        {
            //保存中间的某个结果(此处仅做示意)
            private int tempResult=0;
            //保存某个中间状态
            private string tempState = string.Empty;

            /// <summary>
            /// 结果内容外部只读
            /// </summary>
            public int TempResult { get { return tempResult; } }
            /// <summary>
            /// 状态内容外部只读
            /// </summary>
            public string TempState { get { return tempState; } }

            public MemoImpl(int tempResult,string tempState)
            {
                this.tempResult = tempResult;
                this.tempState = tempState;
            }

        }

        /// <summary>
        /// 创建保存原发器对象的备忘录对象
        /// </summary>
        /// <returns>返回创建好的备忘录对象</returns>
        public IFlowAMockMemo CreateMemo()
        {
            return new MemoImpl(this.tempResult,this.tempState);
        }

        /// <summary>
        /// 重新设置原发器对象状态,让其回到备忘录对象记录的状态
        /// </summary>
        /// <param name="flowAMockMemo"></param>
        public void SetMemo(IFlowAMockMemo flowAMockMemo)
        {
            MemoImpl memoImpl = (MemoImpl)flowAMockMemo;
            this.tempResult = memoImpl.TempResult;
            this.tempState = memoImpl.TempState;
        }

        #endregion


    }//Class_end
}

  2.2.3、实现备忘录对象的管理者

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MemoPattern.MemoDemoOne
{
    /// <summary>
    /// 负责保存模拟运行流程A对象的备忘录管理者对象
    /// </summary>
    internal class FlowAMemoManager
    {
        //记录保存的备忘录对象
        private IFlowAMockMemo flowAMockMemo = null;

        /// <summary>
        /// 保存备忘录对象
        /// </summary>
        /// <param name="flowAMockMemo"></param>
        public void SaveMemo(IFlowAMockMemo flowAMockMemo)
        {
            this.flowAMockMemo = flowAMockMemo;
        }

        /// <summary>
        /// 获取被保存的备忘录对象
        /// </summary>
        /// <returns></returns>
        public IFlowAMockMemo RetriveMemo()
        {
            return this.flowAMockMemo;
        }

    }//Class_end
}

  2.2.4、客户端测试

namespace MemoPattern
{
    internal class Program
    {
        static void Main(string[] args)
        {
            TestMemoDemoOne();

            Console.ReadLine();
        }

        /// <summary>
        /// 测试备忘录示例一
        /// </summary>
        private static void TestMemoDemoOne()
        {
            Console.WriteLine("------测试备忘录示例一------");
            //创建模拟运行的流程对象
            MemoDemoOne.FlowAMock flowAMock = new MemoDemoOne.FlowAMock("TestFlow");

            //运行流程的第一个阶段
            flowAMock.RunPhaseOne();

            /*创建需运行对象的管理者维护该对象的数据某个时段的状态*/
            //创建一个流程的管理者
            MemoDemoOne.FlowAMemoManager flowAMemoManager = new MemoDemoOne.FlowAMemoManager();
            //创建【此模拟运行流程对象】的备忘录对象(并保存到管理者对象里面提供给后续使用)
            MemoDemoOne.IFlowAMockMemo flowAMockMemo = flowAMock.CreateMemo();
            flowAMemoManager.SaveMemo(flowAMockMemo);


            //按照方案一运行流程的后半部分
            flowAMock.SchemaOne();

            //从备忘录管理者那里获取备忘录对象,并设置回去(即:实现模拟运行流程对象自己恢复到内部状态)
            flowAMock.SetMemo(flowAMemoManager.RetriveMemo());

            //按照方案二运行流程的后半部分
            flowAMock.SchemaTwo();

        }

    }//Class_end
}

  2.2.5、运行结果

        由于备忘录对象是一个私有的内部类,外面只能通过备忘录对象的窄接口来获取备忘录对象,而这个接口没有任何方法,仅仅起到了一个标识对象类型的作用,从而保证了内部数据不会被外部获取或是操作,保证了原发器对象的封装性,也就不会再暴露原发器对象的内部结构了。

 2.3、备忘录模式的示例2

        业务需求:考虑一个计算器的功能(最简单的那种方式,只实现加减法运算,且现在需要对加减法实现可撤销和恢复操作)。

在前面的命令模式中讲到了可撤销的操作;当时说过关于实现撤销恢复操作有两种思路:

《1》补偿式(或叫反操作式)【即:若被撤销的功能是加功能,那撤销的实现就变为减,以此类推】。

《2》存储恢复式【即:把操作前的状态记录下来,然后要撤销操作的时候直接恢复回去就可以了】。我们这里就采用存储恢复式的方法来实现。

  2.3.1、定义备忘录对象的窄接口

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MemoPattern.MemoDemoTwo
{
    /// <summary>
    /// 定义备忘录对象的窄接口【不定义任何方法】
    /// </summary>
    internal interface IMemo
    {
    }//Interface_end
}

  2.3.2、定义命令接口

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MemoPattern.MemoDemoTwo
{
    /// <summary>
    /// 定义命令接口
    /// </summary>
    internal interface ICommand
    {
        //执行命令
        void Execute();

        //撤销命令(恢复到备忘录对象记录的状态)
        void Undo(IMemo memo);

        //重做命令(恢复到备忘录对象记录的状态)
        void Redo(IMemo memo);

        //创建保存原触发器对象状态的备忘录对象
        IMemo CreateMemo();

    }//Interface_end
}

  2.3.3、定义操作运算的接口

这个操作运算的接口相当于计算器类的原发器对外提供的接口:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MemoPattern.MemoDemoTwo
{
    /// <summary>
    /// 操作运算的接口
    /// </summary>
    internal interface IOperation
    {
        //获取计算完成后的结果
        int GetResult();

        //执行加法
        void Add(int num);

        //执行减法
        void Substract(int num);

        //创建保存原发器对象状态的备忘录对象
        IMemo CreateMemo();

        //重新设置原发器对象状态,让其回到备忘录对象记录的状态
        void SetMemo(IMemo memo);


    }//Interface_end
}

  2.3.4、实现公共的命令对象

        由于现在执行撤销和恢复操作都是通过使用备忘录对象来恢复原发器的状态,因此就不用按照操作类型来区分,对于所有的命令实现,无论是加法还是减法它们的撤销和重做都是一样的。因此我,们这里实现一个所有命令的公共对象,实现公共功能,这个在实现具体的命令时就简单了。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MemoPattern.MemoDemoTwo
{
    /// <summary>
    /// 命令对象的公共对象,实现各个命令对象的公共方法
    /// </summary>
    abstract internal class AbstractCommand : ICommand
    {
        //持有真正的命令实现对象
        protected IOperation operation = null;

        public void SetOperation(IOperation operation)
        {
            this.operation=operation;
        }


        public IMemo CreateMemo()
        {
            return this.operation.CreateMemo();
        }

        public virtual void Execute()
        {
            //具体的功能实现,抽象类这里就不提供默认实现,让具体类自己实现
            throw new NotImplementedException();
        }

        public void Redo(IMemo memo)
        {
            this.operation.SetMemo(memo);
        }

        public void Undo(IMemo memo)
        {
            this.operation.SetMemo(memo);
        }
    }//Class_end
}

  2.3.5、实现具体的命令对象

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MemoPattern.MemoDemoTwo
{
    /// <summary>
    /// 具体的添加命令对象
    /// </summary>
    internal class AddCommand:AbstractCommand
    {
        private int operationNum = 0;

        public AddCommand(int operationNum)
        {
            this.operationNum = operationNum;
        }

        public override void Execute()
        {
            Console.WriteLine($"准备添加数字【{operationNum}】");
            this.operation.Add(operationNum);
        }

    }//Class_end
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MemoPattern.MemoDemoTwo
{
    /// <summary>
    /// 具体的减法命令对象
    /// </summary>
    internal class SubstractCommand:AbstractCommand
    {
        private int operationNum = 0;

        public SubstractCommand(int operationNum)
        {
            this.operationNum = operationNum;
        }

        public override void Execute()
        {
            Console.WriteLine($"准备减去数字【{operationNum}】");
            this.operation.Substract(operationNum);
        }

    }//Class_end
}

  2.3.6、实现运算类

        这个运算类实现的是真正的运算逻辑内容,在这里创建操作备忘录对象【也就是说该类就是原发器对象】。该类不在对外提供属性的访问方法,只允许内部操作,外部无法操作。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MemoPattern.MemoDemoTwo
{
    /// <summary>
    /// 具体的运算类,真正实现加减法运算
    /// </summary>
    internal class Operation : IOperation
    {
        //记录运算结果
        private int result = 0;

        public void Add(int num)
        {
            result += num;
        }

        public IMemo CreateMemo()
        {
            MemoImpl memoImpl = new MemoImpl(result);
            return memoImpl;
        }

        public int GetResult()
        {
            return result;
        }

        public void SetMemo(IMemo memo)
        {
            MemoImpl memoImpl = (MemoImpl)memo;
            this.result = memoImpl.GetResult();
        }

        public void Substract(int num)
        {
            result -= num;
        }

        #region 备忘录对象实现
        private class MemoImpl : IMemo
        {
            private int result = 0;

            public MemoImpl(int result)
            {
                this.result = result;
            }

            public int GetResult()
            {
                return result;
            }
        }

        #endregion

    }//Class_end
}

  2.3.7、实现计算器类

        这个计算器类相当于备忘录模式的管理者对象,在这个类里面实现撤销和重做操作,还有对应的加法、减法按钮功能实现。由于每个命令对象的撤销和重做状态是不一样的,撤销是回到命令操作前的状态;而重做是回到命令操作后的状态,因此对每一个命令,使用一个备忘录对象的数组来记录对应的状态。这些备忘录对象和命令对象是相对应的,因此也跟命令历史记录一样,设置相应的历史记录,它的顺序和命令完全对应起来。在操作命令历史记录的同事,对应操作相应的备忘录对象记录。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MemoPattern.MemoDemoTwo
{
    /// <summary>
    /// 计算器对象(计算器上的加法、减法、撤销、恢复按钮)【相当于备忘录对象的管理者】
    /// </summary>
    internal class Calculator
    {
        //命令操作的历史记录(在撤销时使用)
        private List<ICommand> undoCmdList=new List<ICommand>();
        //命令被撤销的历史记录(在恢复时使用)
        private List<ICommand> redoCmdList=new List<ICommand>();


        //命令操作对应的备忘录对象的历史记录,在撤销时使用
        //(数组有两个元素,第一个是命令执前的状态;第二个是命令执行后的状态)
        private List<IMemo[]>undoMemoList=new List<IMemo[]>();

        //被撤销命令对应的备忘录对象的历史记录,在恢复时使用
        //(数组有两个元素,第一个是命令执行前的状态,第二个是命令执行后的状态)
        private List<IMemo[]>redoMemoList=new List<IMemo[]>();

        //加法命令
        private ICommand addCmd = null;
        //减法命令
        private ICommand subCmd = null;

        /// <summary>
        /// 设置操作为加法
        /// </summary>
        /// <param name="command"></param>
        public void SetAddCmd(ICommand command)
        {
            this.addCmd = command;
        }

        /// <summary>
        /// 设置操作为减法
        /// </summary>
        /// <param name="command"></param>
        public void SetSubCmd(ICommand command)
        {
            this.subCmd = command;
        }

        /// <summary>
        /// 加法按钮逻辑
        /// </summary>
        public void AddPressed()
        {
            //获取对应的备忘录对象,并保存在相应的历史记录中
            IMemo memo1 = this.addCmd.CreateMemo();

            //执行命令
            this.addCmd.Execute();
            //将操作记录到历史记录中
            this.undoCmdList.Add(this.addCmd);

            //获取执行命令后的备忘录对象
            IMemo memo2 = this.addCmd.CreateMemo();
            //设置到撤销历史记录中
            this.undoMemoList.Add(new IMemo[] { memo1,memo2});

        }


        /// <summary>
        /// 减法按钮逻辑
        /// </summary>
        public void SubPressed()
        {
            //获取对应的备忘录对象,并保存到相应的历史记录中
            IMemo memo1=this.subCmd.CreateMemo();

            //执行命令
            this.subCmd.Execute();
            //把操作记录到历史记录中
            undoCmdList.Add(this.subCmd);

            //获取执行命令后的备忘录对象
            IMemo memo2=this.subCmd.CreateMemo();
            //设置到撤销的历史记录中
            this.undoMemoList.Add(new IMemo[] {memo1,memo2 });
        }

        /// <summary>
        /// 撤销按钮
        /// </summary>
        public void UndoPressed()
        {
            if (undoCmdList.Count > 0)
            {
                //取出最后一个命令来撤销
                ICommand cmd = undoCmdList[undoCmdList.Count - 1];
                //获取对应的备忘录对象
                IMemo[] memos = undoMemoList[undoCmdList.Count - 1];

                //撤销
                cmd.Undo(memos[0]);

                //如果还有恢复功能,那就把这个命令记录到恢复的历史记录中
                redoCmdList.Add(cmd);
                //把相应的备忘录对象也添加到恢复备忘录列表中
                redoMemoList.Add(memos);

                //将最后的命令删除
                undoCmdList.Remove(cmd);
                //将相应的备忘录对象也删除
                undoMemoList.Remove(memos);
            }
            else
            {
                Console.WriteLine("很抱歉,已经没有可撤销的命令了");
            }
        }

        /// <summary>
        /// 恢复按钮
        /// </summary>
        public void RedoPressed()
        {
            if (redoCmdList.Count > 0)
            {
                //取出最后一个命令来重做
                ICommand cmd = redoCmdList[redoCmdList.Count - 1];
                //获取相应的备忘录对象
                IMemo[] memos = redoMemoList[redoCmdList.Count - 1];

                //重做
                cmd.Redo(memos[1]);

                //将这个命令记录到可撤销的历史记录中
                undoCmdList.Add(cmd);
                //把相应的备忘录对象也添加到撤销列表中
                undoMemoList.Add(memos);

                //将最后一个命令删除
                redoCmdList.Remove(cmd);
                //将对应的备忘录对象也删除
                redoMemoList.Remove(memos);

            }
            else
            {
                Console.WriteLine("很抱歉,已经没有可恢复的命令了");
            }
        }

    }//Class_end
}

  2.3.8、客户端测试

namespace MemoPattern
{
    internal class Program
    {
        static void Main(string[] args)
        {
            TestMemoDemoTwo();

            Console.ReadLine();
        }

        /// <summary>
        /// 测试备忘录示例二
        /// </summary>
        private static void TestMemoDemoTwo()
        {
            Console.WriteLine("------测试备忘录示例二------");

            /*1-组装命令和接收者*/
            //创建操作对象【作为命令的接收者】
            MemoDemoTwo.IOperation operation = new MemoDemoTwo.Operation();

            //创建加法命令
            MemoDemoTwo.AddCommand addCmd = new MemoDemoTwo.AddCommand(5);
            //创建减法命令
            MemoDemoTwo.SubstractCommand subCmd=new MemoDemoTwo.SubstractCommand(3);

            //组装命令和接收者
            addCmd.SetOperation(operation);
            subCmd.SetOperation(operation);

            /*2-把命令设置到持有者【计算器中】*/
            MemoDemoTwo.Calculator calculator=new MemoDemoTwo.Calculator();
            calculator.SetAddCmd(addCmd);
            calculator.SetSubCmd(subCmd);

            /*3-模拟按下指定按钮*/
            calculator.AddPressed();
            Console.WriteLine($"执行【加法】一次运算后的结果是【{operation.GetResult()}】");
            calculator.SubPressed();
            Console.WriteLine($"执行【减法】一次运算后的结果是【{operation.GetResult()}】");
            
            /*4-测试撤销*/
            for ( int i = 1; i <=3 ; i++ )
            {
                calculator.UndoPressed();
                Console.WriteLine($"执行【撤销】【{i}】次后的结果是【{operation.GetResult()}】");
            }

            /*5-测试恢复*/
            for ( int i = 1;i <=3 ; i++ )
            {
                calculator.RedoPressed();
                Console.WriteLine($"执行【恢复】【{i}】次后的结果是【{operation.GetResult()}】");
            }
        }

    }//Class_end
}

  2.3.9、运行结果

三、项目源码工程

kafeiweimei/Learning_DesignPattern: 这是一个关于C#语言编写的基础设计模式项目工程,方便学习理解常见的26种设计模式https://github.com/kafeiweimei/Learning_DesignPattern


网站公告

今日签到

点亮在社区的每一天
去签到