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 的数据。

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

  • 相关阅读:
    leetcode 268. Missing Number
    DBSCAN
    python二维数组初始化
    leetcode 661. Image Smoother
    leetcode 599. Minimum Index Sum of Two Lists
    Python中的sort() key含义
    leetcode 447. Number of Boomerangs
    leetcode 697. Degree of an Array
    滴滴快车奖励政策,高峰奖励,翻倍奖励,按成交率,指派单数分级(1月3日)
    北京Uber优步司机奖励政策(1月2日)
  • 原文地址:https://www.cnblogs.com/babycomeon/p/14096173.html
Copyright © 2011-2022 走看看