基于C++的《Head First设计模式》笔记——备忘录模式

目录

一.专栏介绍

二.备忘录模式概念

三.案例与代码展示

四.备忘录模式的优点

五.备忘录模式的缺点

六.备忘录模式的用途

七.命令模式中使用备忘录模式


一.专栏介绍

本专栏是我学习《head first》设计模式的笔记。这本书中是用Java语言为基础的,我将用C++语言重写一遍,并且详细讲述其中的设计模式,涉及是什么,为什么,怎么做,自己的心得等等。希望阅读者在读完我的这个专题后,也能在开发中灵活且正确的使用,或者在面对面试官时,能够自信地说自己熟悉常用设计模式。

本章将开始**备忘录模式(Memento Pattern)**的学习。

二.备忘录模式概念

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

有时有必要记录一个对象的内部状态。为了允许用户取消不确定的操作或从错误中恢复过来,需要实现检查点和取消机制 ,而要实现这些机制,你必须事先将状态信息保存在某处,这样才能将对象恢复到它们之前的状态。但是对象通常封装了其部分或所有的状态信息。使得其状态不能被其他对象访问(private ),也就不可能在该对象之外保存其状态。而暴露其内部状态又将违反封装的原则(不能乱加get函数),可能有损应用的可靠性和可扩展性。

三大角色:

  • Originator(原发器):自身状态,创建 / 恢复备忘录

  • Memento(备忘录):私有存储状态,仅原发器可访问

  • Caretaker(管理者):保管多个备忘录,负责历史管理

需要注意,Originator 原发器负责创建和恢复备忘录 ,分别对应用自己的状态构造一个备忘录对象读取备忘录对象的状态给自己

另外,还要保证Memento备忘录对象的成员只有原发器才能访问 ,怎么做到呢,备忘录类将原发器类声明为自己的友元类

最后,我们最好将Caretaker管理者类提供迭代器从而遍历自己保管的备忘录对象。

三.案例与代码展示

在图形操作软件(画图板,photoshop)或者2D游戏中一个对象的位置,也就是x坐标和y坐标是不可或缺的元素。本例的Originator(原发器) 就存储一个位置坐标,**Memento(备忘录)**当然也存储一个位置坐标。

代码如下:

Memento.h:

cpp 复制代码
#pragma once
#include <vector>
#include <iostream>
using namespace std;

// 位置坐标和输出流重载,输出流重载用于打印展示
struct Position
{
	int x;
	int y;
	friend ostream& operator<<(ostream& os, const Position& pos)
	{
		os << "(" << pos.x << ", " << pos.y << ")";
		return os;
	}
};

// 备忘录,位置坐标也是私有的
class Memento
{
private:
	Memento(const Position& pos)
		:_pos(pos)
	{}
	// 将Originator(原发器)声明为友元,从而让它可以构造自己并且访问私有成员(位置坐标)
	friend class Originator;

	// 输出流用于输出展示,这里嵌套了Position的输出流重载
	friend ostream& operator<<(ostream& os, const Memento* memento)
	{
		if (memento == nullptr) 
			throw std::invalid_argument("operator<< 异常:传入了空的Memento指针,无法输出存档内容");
		os << "备忘录存档:" << memento->_pos;	// 嵌套!自动用 Position 的输出
		return os;
	}
private:
	Position _pos;
};

// 原发器,可以说是与业务直接相关的类
class Originator
{
public:
	// 用自己的当前状态构造一个备忘录对象给客户端,也可以说给Caretaker(管理者)对象
	Memento* createMemento()
	{
		return new Memento(_pos);
	}
	// 读取Memento(备忘录)对象的状态给自己
	void setMemento(const Memento* memento)
	{
        if (memento == nullptr) 
			throw std::invalid_argument("setMemento 异常:传入了空的Memento指针,无法读取Memento状态");
		_pos = memento->_pos;
		cout << "恢复后位置是:(" << _pos.x << ", " << _pos.y << ")" << endl;
	}
	// 客户端改变状态(位置)使用
	void setPosition(const Position& pos)
	{
		_pos = pos;
		cout << "当前的位置是:(" << _pos.x << ", " << _pos.y << ")" << endl;
	}
private:
	Position _pos;
};

// 管理者,管理Memento(备忘录)对象
class CareTaker
{
public:
	// 添加一个Memento(备忘录)对象
	void addMemento(Memento* memento)
	{
		_mementoes.push_back(memento);
	}
	// 获取一个Memento(备忘录)对象给Originator(原发器)读取,从而撤销到这个备忘录对象的状态
	Memento* getMemento(int idx) 
	{
		if (idx >= 0 && idx < _mementoes.size())
			return _mementoes[idx];
		return nullptr;
	}

	// 支持范围for的迭代器接口函数,让客户端方便展示"存档信息"
	using iterator = vector<Memento*>::iterator;
	iterator begin() { return _mementoes.begin(); }
	iterator end() { return _mementoes.end(); }
private:
	vector<Memento*> _mementoes;
};

main.cpp:

cpp 复制代码
#include "Memento.h"

int main()
{
	// 管理者对象
	CareTaker careTaker;
	// 原发器对象
	Originator originator;

	// 设置状态并且保存
	originator.setPosition({20, 30});
	careTaker.addMemento(originator.createMemento());

	// 设置状态并且保存
	originator.setPosition({100, 50});
	careTaker.addMemento(originator.createMemento());

	// 设置状态
	originator.setPosition({88, 99});

	cout << endl << "------存档信息展示------" << endl;

	size_t index = 0;
	for (const auto& e : careTaker)
	{
		cout << "存档" << index++ << ":" << e << endl;
	}

	cout << endl << "------开始回滚两次------" << endl;

	originator.setMemento(careTaker.getMemento(1));
	originator.setMemento(careTaker.getMemento(0));

	return 0;
}

运行结果:

比如在以一个游戏里,以下部分的输出就可以作为存档界面的展示信息:

四.备忘录模式的优点

  1. 保持被保存的状态(备忘录)处于关键对象(原发器)外面,有助于维护高内聚,低耦合
  2. 保持关键对象的数据封装,备忘录的私有状态只能被原发器读取
  3. 提供容易实现的恢复能力,撤销,读取存档等容易实现

五.备忘录模式的缺点

  • 使用备忘录的缺点是,保存和恢复状态可能相当耗时。因为原发器在构造备忘录对象和读取备忘录对象的过程中可能发生大量深拷贝。

六.备忘录模式的用途

  • 软件中撤销,重做的实现。
  • 游戏,photoshop中读取存档。

七.命令模式中使用备忘录模式

命令模式中要实现撤销,具体的命令类就不仅要有命令的执行函数(execute())函数,还要有撤销(undo())函数。这是我命令模式那篇博客中其中一个命令的执行和撤销函数:

cpp 复制代码
class LightOnCommand : public Command
{
public:
    LightOnCommand::LightOnCommand(Light* light) :
	    _light(light)
    {}
 
    void LightOnCommand::execute()
    {
	    _light->on();
    }
    void LightOnCommand::undo()
    {
	     _light->off()                 // 恢复
    }
private:
	Light* _light;						// 接收者
};

这里的撤销就简单的实现了一个反向操作,在Light对象比较复杂或者接口比较多的时候就不太方便实现undo()函数了。

加入备忘录模式的关键代码:

cpp 复制代码
class LightOnCommand : public Command
{
public:
    LightOnCommand::LightOnCommand(Light* light) :
	    _light(light)
    {}
 
    void LightOnCommand::execute()
    {
        _memento = _light->createMemento();     // 执行前保存
	    _light->on();
    }
    void LightOnCommand::undo()
    {
	     _light->restoreMemento(_memento);      // 恢复
    }
private:
	Light* _light;						// 接收者
    LightMemento _memento;              // 命令自带存档
};

也就是这个Light对象就变成了Originator(原发器) ,它提供了构造备忘录对象和读取备忘录对象的接口。命令对象不仅是命令,还有备忘录对象成员,用于撤销操作。当然也可以单独搞一个**Caretaker(管理者)**来管理这些备忘录对象,支持高内聚,低耦合,但有可能会导致类爆炸问题,这就需要我们在业务中权衡利弊。

还是那句话,很多时候良好的面向对象思想就能很好的解决问题。

相关推荐
tankeven2 小时前
HJ152 取数游戏
c++·算法
汉克老师2 小时前
GESPC++三级考试语法知识(五、字符数组 )
c++·字符数组·gesp三级·gesp3级·字母大小写转换
深邃-2 小时前
数据结构-队列
c语言·数据结构·c++·算法·html5
Rhystt2 小时前
代码随想录算法训练营第六十天|多余的边?从基础到进阶!
开发语言·c++·算法·图论
再玩一会儿看代码2 小时前
Java中 next() 和 nextLine() 有什么区别?一篇文章彻底搞懂
java·开发语言·经验分享·笔记·学习
羊小猪~~2 小时前
【QT】-- QMainWindow简介
开发语言·数据库·c++·后端·qt·前端框架·求职招聘
Heartache boy2 小时前
野火STM32_HAL库版课程笔记-TIM通道输出应用之PWM实现呼吸灯
笔记·stm32·单片机·嵌入式硬件
2301_810160952 小时前
C++中的策略模式进阶
开发语言·c++·算法
-Rane2 小时前
【C++】map和set
开发语言·c++