zoukankan      html  css  js  c++  java
  • 「补课」进行时:设计模式(17)——备忘录模式

    1. 前文汇总

    「补课」进行时:设计模式系列

    2. 从版本控制开始

    相信每个程序猿,每天工作都会使用版本控制工具,不管是微软提供的 vss 还是 tfs ,又或者是开源的 svn 或者 git ,每天下班前,总归会使用版本控制工具提交一版代码。

    版本管理工具是让我们在代码出问题的时候,可以方便的获取到之前的版本进行版本回退,尤其是在项目发布投运的时候,当出现问题的时候直接获取上一个版本进行回滚操作。

    在这个操作中间,最重要的就是保存之前的状态,那么如何保存之前的状态?

    操作很简单,我们可以定义一个中间变量,保留这个原始状态。

    先定义一个版本管理 Git 类:

    public class Git {
        private String state;
        // 版本发生改变,现在是 version2
        public void changeState() {
            this.state = "version2";
        }
    
        public String getState() {
            return state;
        }
    
        public void setState(String state) {
            this.state = state;
        }
    }
    

    然后是一个场景 Client 类:

    public class Client {
        public static void main(String[] args) {
            Git git = new Git();
            // 初始化版本
            git.setState("version1");
            System.out.println("当前的版本信息:");
            System.out.println(git.getState());
            // 记录下当前的状态
            Git backup = new Git();
            backup.setState(git.getState());
            // 提交一个版本,版本进行改变
            git.changeState();
            System.out.println("提交一个版本后的版本信息:");
            System.out.println(git.getState());
            // 回退一个版本,版本信息回滚
            git.setState(backup.getState());
            System.out.println("回退一个版本后的版本信息:");
            System.out.println(git.getState());
        }
    }
    

    执行结果:

    当前的版本信息:
    version1
    提交一个版本后的版本信息:
    version2
    回退一个版本后的版本信息:
    version1
    

    程序运行正确,输出结果也是我们期望的,但是结果正确并不表示程序是合适的。

    在场景类 Client 类中,这个是高层模块,现在却在高层模块中做了中间临时变量 backup 的状态的保持,为什么一个状态的保存和恢复要让高层模块来负责呢?

    这个中间临时变量 backup 应该是 Git 类的职责,而不是让一个高层次的模块来进行定义。

    我们新建一个 Memento 类,用作负责状态的保存和备份。

    public class Memento {
        private String state;
        
        public Memento(String state) {
            this.state = state;
        }
    
        public String getState() {
            return state;
        }
    
        public void setState(String state) {
            this.state = state;
        }
    }
    

    新建一个 Memento ,用构造函数来传递状态 state ,修改上面的 Git 类,新增两个方法 createMemento()restoreMemento(),用来创建备忘录以及恢复一个备忘录。

    public class Git {
        private String state;
        // 版本发生改变,现在是 version2
        public void changeState() {
            this.state = "version2";
        }
    
        public String getState() {
            return state;
        }
    
        public void setState(String state) {
            this.state = state;
        }
        // 创建一个备忘录
        public Memento createMemento(String state) {
            return new Memento(state);
        }
        // 恢复一个备忘录
        public void restoreMemento(Memento memento) {
            this.setState(memento.getState());
        }
    }
    

    修改后的场景类:

    public class Client {
        public static void main(String[] args) {
            Git git = new Git();
            // 初始化版本
            git.setState("version1");
            System.out.println("当前的版本信息:");
            System.out.println(git.getState());
            // 记录下当前的状态
            Memento mem = git.createMemento(git.getState());
            // 提交一个版本,版本进行改变
            git.changeState();
            System.out.println("提交一个版本后的版本信息:");
            System.out.println(git.getState());
            // 项目发布失败,回滚状态
            git.restoreMemento(mem);
            System.out.println("回退一个版本后的版本信息:");
            System.out.println(git.getState());
        }
    }
    

    运行结果和之前的案例保持一致,那么这就结束了么,当然没有,虽然我们在 Client 中不再需要重复定义 Git 类了,但是这是对迪米特法则的一个亵渎,它告诉我们只和朋友类通信,那这个备忘录对象是我们必须要通信的朋友类吗?对高层模块来说,它最希望要做的就是创建一个备份点,然后在需要的时候再恢复到这个备份点就成了,它不用关心到底有没有备忘录这个类。

    那我们可以对这个备忘录的类再做一下包装,创建一个管理类,专门用作管理这个备忘录:

    public class Caretaker {
        private Memento memento;
    
        public Memento getMemento() {
            return memento;
        }
    
        public void setMemento(Memento memento) {
            this.memento = memento;
        }
    }
    

    非常简单纯粹的一个 JavaBean ,甭管它多简单,只要有用就成,我们来看场景类如何调用:

    public class Client {
        public static void main(String[] args) {
            Git git = new Git();
            // 创建一个备忘录管理者
            Caretaker caretaker = new Caretaker();
            // 初始化版本
            git.setState("version1");
            System.out.println("当前的版本信息:");
            System.out.println(git.getState());
            // 记录下当前的状态
            caretaker.setMemento(git.createMemento(git.getState()));
            // 提交一个版本,版本进行改变
            git.changeState();
            System.out.println("提交一个版本后的版本信息:");
            System.out.println(git.getState());
            // 项目发布失败,回滚状态
            git.restoreMemento(caretaker.getMemento());
            System.out.println("回退一个版本后的版本信息:");
            System.out.println(git.getState());
        }
    }
    

    现在这个备份者就类似于一个备份的仓库管理员,创建一个丢进去,需要的时候再拿出来。这就是备忘录模式。

    3. 备忘录模式

    3.1 定义

    备忘录模式(Memento Pattern)提供了一种弥补真实世界缺陷的方法,让“后悔药”在程序的世界中真实可行,其定义如下:

    Without violating encapsulation,capture and externalize an object's internalstate so that the object can be restored to this state later.(在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。)

    3.2 通用类图

    • Originator 发起人角色:记录当前时刻的内部状态,负责定义哪些属于备份范围的状态,负责创建和恢复备忘录数据。
    • Memento 备忘录角色:负责存储 Originator 发起人对象的内部状态,在需要的时候提供发起人需要的内部状态。
    • Caretaker 备忘录管理员角色:对备忘录进行管理、保存和提供备忘录。

    3.3 通用代码

    发起人:

    public class Originator {
        private String state;
    
        public String getState() {
            return state;
        }
    
        public void setState(String state) {
            this.state = state;
        }
        // 创建一个备忘录
        public Memento createMemento() {
            return new Memento(this.state);
        }
        // 恢复一个备忘录
        public void restoreMemento(Memento memento) {
            this.setState(memento.getState());
        }
    }
    

    备忘录:

    public class Memento {
        private String state;
        public Memento(String state) {
            this.state = state;
        }
    
        public String getState() {
            return state;
        }
    
        public void setState(String state) {
            this.state = state;
        }
    }
    

    备忘录管理员:

    public class Caretaker {
        // 备忘录对象
        private Memento memento;
    
        public Memento getMemento() {
            return memento;
        }
    
        public void setMemento(Memento memento) {
            this.memento = memento;
        }
    }
    

    场景类:

    public class Client {
        public static void main(String[] args) {
            // 定义发起人
            Originator originator = new Originator();
            // 定义备忘录管理员
            Caretaker caretaker = new Caretaker();
            // 创建一个备忘录
            caretaker.setMemento(originator.createMemento());
            // 恢复一个备忘录
            originator.restoreMemento(caretaker.getMemento());
        }
    }
    

    4. clone 方式的备忘录

    我们可以通过复制的方式产生一个对象的内部状态,这是一个很好的办法,发起人角色只要实现 Cloneable 就成,比较简单:

    public class Originator implements Cloneable {
        // 内部状态
        private String state;
    
        public String getState() {
            return state;
        }
    
        public void setState(String state) {
            this.state = state;
        }
    
        private Originator backup;
    
        // 创建一个备忘录
        public void createMemento() {
            this.backup = this.clone();
        }
        // 恢复一个备忘录
        public void restoreMemento() {
            this.setState(this.backup.getState());
        }
        // 克隆当前对象
        @Override
        protected Originator clone() {
            try {
                return (Originator) super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    

    备忘录管理员:

    public class Caretaker {
        // 发起人对象
        private Originator originator;
    
        public Originator getOriginator() {
            return originator;
        }
    
        public void setOriginator(Originator originator) {
            this.originator = originator;
        }
    }
    

    场景类:

    public class Client {
        public static void main(String[] args) {
            // 定义发起人
            Originator originator = new Originator();
            // 创建初始状态
            originator.setState("初始状态");
            System.out.println("初始状态:" + originator.getState());
            // 创建备份
            originator.createMemento();
            // 修改状态
            originator.setState("修改后的状态");
            System.out.println("修改后的状态:" + originator.getState());
            // 恢复状态
            originator.restoreMemento();
            System.out.println("恢复后的状态:" + originator.getState());
        }
    }
    

    运行结果是我们所希望的,程序精简了很多,而且高层模块的依赖也减少了,这正是我们期望的效果。

    但是我们来考虑一下原型模式深拷贝和浅拷贝的问题,在复杂的场景下它会让我们的程序逻辑异常混乱,出现错误也很难跟踪。因此 Clone 方式的备忘录模式适用于较简单的场景。

    5. 多备份的备忘录

    我们每天使用的 Windows 是可以拥有多个备份时间点的,系统出现问题,我们可以自由选择需要恢复的还原点。

    我们上面的备忘录模式尚且不具有这个功能,只能有一个备份,想要有多个备份也比较简单,我们在备份的时候做一个标记,简单一点可以使用一个字符串。

    我们只要把通用代码中的 Caretaker 管理员稍做修改就可以了:

    public class Caretaker {
        // 容纳备忘录的容器
        private Map<String, Memento> mementoMap = new HashMap<>();
    
        public Memento getMemento(String keys) {
            return mementoMap.get(keys);
        }
    
        public void setMemento(String key, Memento memento) {
            this.mementoMap.put(key, memento);
        }
    }
    

    对场景类做部分修改:

    public class Client {
        public static void main(String[] args) {
            // 定义发起人
            Originator originator = new Originator();
            // 定义备忘录管理员
            Caretaker caretaker = new Caretaker();
            // 创建两个备忘录
            caretaker.setMemento("001", originator.createMemento());
            caretaker.setMemento("002", originator.createMemento());
            // 恢复一个指定的备忘录
            originator.restoreMemento(caretaker.getMemento("002"));
        }
    }
    

    6. 更好的封装

    在系统管理上,一个备份的数据是完全、绝对不能修改的,它保证数据的洁净,避免数据污染而使备份失去意义。

    在我们的程序中也有着同样的问题,备份是不能被褚篡改的,那么也就是需要缩小备忘录的访问权限,保证只有发起人可读就可以了。

    这个很简单,直接使用内置类就可以了:

    public class Originator {
        private String state;
    
        public String getState() {
            return state;
        }
    
        public void setState(String state) {
            this.state = state;
        }
        // 创建一个备忘录
        public IMemento createMemento() {
            return new Memento(this.state);
        }
        // 恢复一个备忘录
        public void restoreMemento(IMemento memento) {
            this.setState(((Memento)memento).getState());
        }
    
        private class Memento implements IMemento {
            private String state;
            private Memento(String state) {
                this.state = state;
            }
    
            public String getState() {
                return state;
            }
    
            public void setState(String state) {
                this.state = state;
            }
        }
    }
    

    这里使用了一个 IMemento 接口,这个接口实际上是一个空接口:

    public interface IMemento {
    }
    

    这个空接口的作用是用作公共的访问权限。

    下面看一下备忘录管理者的变化:

    public class Caretaker {
        // 备忘录对象
        private IMemento memento;
    
        public IMemento getMemento() {
            return memento;
        }
    
        public void setMemento(IMemento memento) {
            this.memento = memento;
        }
    }
    

    上面这段示例全部通过接口访问,如果我们想访问它的属性貌似是无法访问到了。

    但是安全是相对的,没有绝对的安全,我们可以使用 refelect 反射修改 Memento 的数据。

    在这里我们使用了一个新的设计方法:双接口设计,我们的一个类可以实现多个接口,在系统设计时,如果考虑对象的安全问题,则可以提供两个接口,一个是业务的正常接口,实现必要的业务逻辑,叫做宽接口;另外一个接口是一个空接口,什么方法都没有,其目的是提供给子系统外的模块访问,比如容器对象,这个叫做窄接口,由于窄接口中没有提供任何操纵数据的方法,因此相对来说比较安全。

  • 相关阅读:
    工作量单位-人月、人日、人时 详解
    PHP数组的交集array_intersect(),array_intersect_assoc(),array_inter_key()函数详解
    常用的Mysql数据库操作语句大全
    Linux服务器,PHP的10大安全配置实践
    PHP如何获取二个日期的相差天数?
    常见HTTP状态码列表
    PHP中静态(static)调用非静态方法详解
    PHP引用(&)初探:函数的引用返回
    PHP的大括号(花括号{})使用详解
    详解JavaScript中的Url编码/解码,表单提交中网址编码
  • 原文地址:https://www.cnblogs.com/babycomeon/p/14096173.html
Copyright © 2011-2022 走看看