设计模式——2_5 备忘录(Memento)

春风若有怜花意,可否许我再少年?

——佚名

定义

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复至先前保存的状态

简单来说,备忘录就是找一个第三方的对象存储某个对象在某个时刻的状态(快照),以供业务层在随时可以把这个状态恢复到快照时的状态

比如说:

​ 我有个 Apple(苹果) 类的对象 a,这个对象里面有个属性叫 color(颜色),他默认是红色的

​ 然后我给你个按钮 x,点一下就会把 a.color 变成 绿色/黄色;再给你另一个按钮 y,点一下就会把 a.color 还原回上次你点x之前的颜色

​ 这时候你在点x的时候,我找一个 ColorRecorder(颜色记录员) 对象把 a 现在的状态存起来,以便在你点 y 的时候可以直接还原,这时候 ColorRecorder 其实就是一个备忘录




图纸

在这里插入图片描述




一个例子:带有限制的扑克牌元素拖动

道友,玩过蜘蛛纸牌吗?就是那个预装在Windows XP上的纸牌整理游戏。玩家可以通过鼠标拖拽屏幕上的扑克牌,把他们按照顺序排列完成游戏,就像这样:

在这里插入图片描述

这个鼠标拖动的动作,就是我们这次的例子,准备好了吗?我们开始了:



扑克牌和桌面

于是乎,我们为这个游戏设计了这样的类结构:

在这里插入图片描述

Point & Poker
/**
 * 坐标点
 */
public class Point {

    /**
     * 相对于左上角的x坐标
     */
    private final double x;
    /**
     * 相对于左上角的y坐标
     */
    private final double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return Double.compare(point.x, x) == 0 &&
                Double.compare(point.y, y) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}

/**
 * 扑克牌信息
 */
public class Poker {

    public static final String[] SUITS = new String[]{
            "spades", "hearts", "clubs", "diamonds"
    };

    /**
     * 点数
     */
    private int number;

    /**
     * 花色
     */
    private String suit;

    public Poker(int number, String suit) {
        setNumber(number);
        setSuit(suit);
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        if (number > 0 && number <= 13) {
            this.number = number;
        } else {
            throw new RuntimeException(String.format("%s 不符合扑克牌点数要求", number));
        }
    }

    public String getSuit() {
        return suit;
    }

    public void setSuit(String suit) {
        for (String s : SUITS) {
            if (s.equals(suit)) {
                this.suit = suit;
                return;
            }
        }

        throw new RuntimeException(String.format("%s 不符合扑克牌花色要求", suit));
    }
}
Card & Deck
/**
 * 扑克牌UI元素
 */
public class Card {

    public static Card createCard(Poker message,boolean isFront){
        Card card = new Card();

        card.setMessage(message);//写入扑克牌信息
        card.setFront(isFront);//写入正反面信息

        //初始化UI事件
        //鼠标按住事件:当鼠标按住当前card的时候,card的position会随着鼠标的移动而移动

        return card;
    }

    /**
     * 当前这张卡牌元素的渲染位置
     */
    private Point position;

    /**
     * 扑克信息
     */
    private Poker message;

    /**
     * 是正面朝上的吗?
     * true:是的,当前扑克牌正面朝上
     */
    private boolean isFront = false;

    //width height等N多个信息略

    /**
     * 判断参数牌是否在这张扑克牌的UI元素内
     *
     * @param card 要被判断的牌
     * @return true 是的,这张牌在这个UI元素内
     */
    public boolean isInside(Card card) {
        //return 参数牌是否在UI元素内;
        return true;
    }

    /**
     * 绘制当前这张牌
     */
    public void draw() {
        //绘制单张卡牌的方法
    }

    //setter & getter 略
}

/**
 * 牌堆,在桌面上N张牌叠在一起则称之为牌堆
 */
public class Deck {

    /**
     * 用于表示牌堆里面的牌
     */
    private final List<Card> cardList = new ArrayList<>();
    /**
     * 当前这个牌堆的渲染位置
     */
    private Point position;

    public Deck(Point position) {
        this.position = position;
    }

    public boolean add(Card card) {
        cardList.add(card);
        draw();//重新绘制画面
        return true;
    }

    /**
     * 绘制
     */
    public void draw() {
        /*
         * 遍历 cardList,把牌堆里面的card对齐,然后逐一调用card里面的draw方法
         */
    }

    /**
     * 判断参数牌是否在这张扑克牌的UI元素内
     *
     * @param card 要被判断的牌
     * @return true 是的,这个牌在这个UI元素内
     */
    public boolean isInside(Card card) {
        //如果参数点在最有一张牌里,那就是落在这个牌堆里面
        return cardList.get(cardList.size() - 1).isInside(card);
    }
    
    /*
    *	根据参数牌的点数判断他是否可以插入到这个牌堆里面
    *
    */
    public boolean isNext(Card card){
    	if(cardList.isEmpty()){
    		return card.getMessage().getNumber() == 13;
		}else {
    		return card.getMessage().getNumber() == cardList.get(cardList.size() - 1).getMessage().getNumber() - 1;
		}
	}

}
Table
/**
 * 牌桌
 */
public class Table {

    /**
     * 单例对象
     * 一场游戏里只有一个牌桌
     */
    private static final Table current = new Table();
    /**
     * 牌桌上的牌堆列表
     */
    private final List<Deck> deckList = new ArrayList<>();

    /**
     * 返还单例牌桌对象
     */
    public static Table getCurrent() {
        return current;
    }

    //setter && getter 略
}

除开 Point(坐标点)Poker(扑克信息) 这两个基础类,我们的游戏主体分为三个部分 Table(牌桌)Card(扑克牌UI)Deck(牌堆),他们在游戏里呈现出来的效果是这样:

在这里插入图片描述

在上面的GIF中,我们看到 Card 是可以用鼠标拖拽的,所以在 Card 的静态工厂方法 createCard 里面,我们让所有的 Card 在创建出来的时候就有一个鼠标按住事件的监听器,以实现在鼠标拖拽的功能


这些都是必做的事情,可问题在于,当你松开 Card 元素的元素应该发生什么事情呢?

你会说那不就是让 Card 停在那里吗?那看看下面这两个动图吧:



带有限制的扑克牌移动

在这里插入图片描述

在这里插入图片描述

第一种情况,拖到一半就松手,他要回到之前的位置

第二种情况,把牌放到错误的位置(8后面不是A),他也要回到之前的位置


也就是说,当我们松开 Card 的时候,应该去向 Table 确认,自己有没有被放在正确的位置上,如果没有则撤销刚刚的操作,回到原位

要实现这样的效果,我们需要在拖拽 Card 之前,就保留一个当前 Card 的状态的快照

这个状态最好保留在 Card 的内部,你应该发现了,Card 是没有向 Table 或者 Deck 暴露过和 position 这个属性相关的内容的,我甚至可以不给 position 写 get方法。但如果我把这个状态快照放在外部,那我就至少要向保存这个快照的类公开 position 属性的存在

所以我们的实现是这样的:

在这里插入图片描述

Table
/**
 * 牌桌
 */
public class Table {

    /**
     * 单例对象
     * 一场游戏里只有一个牌桌
     */
    private static final Table current = new Table();
    /**
     * 牌桌上的牌堆列表
     */
    private final List<Deck> deckList = new ArrayList<>();

    /**
     * 返还单例牌桌对象
     */
    public static Table getCurrent() {
        return current;
    }

    public boolean verify(Card card){
        for(Deck deck:deckList){
            //遍历当前牌桌上的所有牌堆,逐个验证
            if(deck.isInside(card)){
                return deck.isNext(card);
            }
        }

        return false;//不符合要求
    }

    //setter && getter 略
}
Card
/**
 * 扑克牌UI元素
 */
public class Card {

   ……

    public static Card createCard(Poker message, boolean isFront) {
         Card card = new Card();

        //card.setMessage(message);//写入扑克牌信息
        //card.setFront(isFront);//写入正反面信息

        //初始化UI事件

        CardMemento cardMemento;//快照
        //鼠标按住事件:创建一个快照
        {
            cardMemento = card.createCardMemento();//在鼠标按住拖拽之前
        }

        //鼠标拖动事件:card的position会随着鼠标的移动而移动
        {
			……
        }

        //鼠标松开事件:到Table中验证
        {
            if (Table.getCurrent().verify(card)) {
                //可以放到那里,消除快照
                cardMemento = null;
                ……
            } else {
                //还原
                card.setMemento(cardMemento);
            }
        }

        return card;
    }

    ……

    /**
     * 创建一个当前状态的快照
     */
    public CardMemento createCardMemento() {
        CardMemento memento = new CardMemento();
        memento.lastPosition = position;
        return memento;
    }

    /**
     * 通过参数备忘录还原状态
     */
    public void setMemento(CardMemento cardMemento) {
        setPosition(cardMemento.lastPosition);//还原状态
        draw();
    }

    public class CardMemento {

        //快照状态
        private Point lastPosition;

        private CardMemento(){}
    }
}

我们在 Table 中添加了一个用来验证 Card 位置是否正确的方法 verify

​ 在 Card 中创建了一个内部类 CardMemento(Card状态备忘录),而且通过 createCardMemento 方法创建当前 Card 状态的快照和允许通过 setMemento 方法还原 Card 的状态

​ 最后,在 createCard 创建 Card 对象时定义了在鼠标松开 Card 的时候的事件监听:

  • 在按住之前创建备忘录快照
  • 在松开之后进行验证,如果验证不通过则用上一步创建的快照进行还原

至此,我们的需求已经完成



备忘录和回滚

备忘录很好的实现了我们的需求,但这并不是备忘录的全部功能。你可能已经发现了我给 CardMemento 的修饰符是 public,而不是 private。这是有讲究的,这意味着我们可以在别的类里面使用 CardMemento,就像这样:

在这里插入图片描述

Table
/**
 * 牌桌
 */
public class Table {
    
    ……

    //之前执行过操作的Card的备忘录
    private final List<Card.CardMemento> mementoList = new ArrayList<>();

    /**
     * 新增一个备忘录
     */
    public void addMemento(Card.CardMemento memento) {
        mementoList.add(memento);
    }

    /**
     * 撤销上一步
     */
    public void back() {
        if (!mementoList.isEmpty()) {
            Card.CardMemento memento = mementoList.get(mementoList.size() - 1);
            memento.getCard().setMemento(memento);
            mementoList.remove(memento);
        }
    }
}
Card
/**
 * 扑克牌UI元素
 */
public class Card {
    
    ……    

    public static Card createCard(Poker message, boolean isFront) {
        Card card = new Card();

        //card.setMessage(message);//写入扑克牌信息
        //card.setFront(isFront);//写入正反面信息

        //初始化UI事件

        CardMemento cardMemento;//快照
        //鼠标按住事件:创建一个快照
        {
            cardMemento = card.createCardMemento();//在鼠标按住拖拽之前
        }

        //鼠标拖动事件:card的position会随着鼠标的移动而移动
        {

        }

        //鼠标松开事件:到Table中验证
        {
            if (Table.getCurrent().verify(card)) {
                //可以放到那里,存储快照到Table中
                Table.getCurrent().addMemento(cardMemento);
                cardMemento = null;
            } else {
                //还原
                card.setMemento(cardMemento);
            }
        }

        return card;
    }

    public class CardMemento {

        //快照状态
        private Point lastPosition;

        private Card card;

        private CardMemento() {
            card = Card.this;
        }

        public Card getCard() {
            return card;
        }
    }
}

我们在 Table 里面新增了 mementoList ,用于存储本局游戏中进行过移动的 Card 的备忘录,并在 Card 的松开鼠标事件监听器中要求在动作验证成功之后把备忘录写进 mementoList 中。然后提供 back 方法,逆向访问 mementoList ,把那些移动过的 Card 一个一个还原掉

那你会说了,不对啊,既然 TableCardMemento 之间产生了关联,那不就意味着 CardMemento 里所存的状态信息也公布给 Table 了吗?

呐,厉害的就在这啦。CardMemento 里面除了 Card 这个 Table 本来就可以访问的类的属性以外,其他任何状态 Table 都是访问不到的。也就是说,Table 只能用她来还原 Card 的状态,除此之外什么都做不了


而这正是一个标准的备忘录实现




碎碎念

备忘录和封装

如果所有的类里面的属性都是公开的,那么备忘录这种设计模式是不会以现在这种形式出现的,在备忘录的定义里面说得很清楚,在不破坏封装性的前提下

所以备忘录其实是一种被动的保护机制。如果你的代码面临如下问题,那你就应该考虑备忘录了:

  1. 你必须备份一个对象在某个时刻的状态快照,以备还原
  2. 如果让外部对象获取到这个对象需要备份的状态,会暴露对象的实现细节并破坏这个对象的封装性



备忘录和命令

在命令模式中我们提到,因为每个 执行者 都专注于自己要执行的任务,这让依次撤销命令成为了可能

而当你真的需要撤销命令这种功能的时候,那么备忘录将会是很好的实现方式(各个命令所影响的组件上一个状态被保存,然后在撤销命令被调用的时候恢复状态)

也就是说,命令模式提供了这种撤销的可能性,而备忘录则为这种可能提供了具体的可执行方案



多个存档

备忘录就像我们在游戏中的存档一样,绝大多数游戏并不是一定要一命到底的。我们可以创建多个游戏备份,用来走不同的路线

备忘录也一样,你可以创建很多个备忘录快照来存储对象不同时间点的状态,这样你可以还原的选择就会更加多样

多说一句

这种时候你会存在多个备忘录,通常这些备忘录之间是存在某种关联的

比如说,我们现在有A、B、C和D四个快照。其中B状态一定是从A状态延伸而来的;D状态则是由C状态延伸而来的

那么这时候A和B,C和D之间通常会建立某种关联;最终这些快照会以树状图的形式关联在一起,这时候 组合模式 就是你可以的实现方式

而当你需要依次撤销或者展示这些快照的时候,你就需要在可能并不了解这个聚合对象的情况下获取里面的元素,这时候 迭代器 就是你可以考虑的选择

可是备忘录并不是越多越好的,虽然理论上来说我们希望备忘录这种东西越多越好,这样我们的容错率会更高,做起事情来可以更加肆无忌惮。但内存终究是有限的,我们不可能用有限的内存去记录无限的时间


所以要避免备忘录的滥用,在够用的前提下,存档还是越少越好




万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容

  • 35
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

乡亲们啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值