learn.lianglianglee.com/专栏/白话设计模式 28 讲(完)/16 备忘模式:好记性不如烂笔头.md.html
2022-05-11 19:04:14 +08:00

842 lines
25 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>16 备忘模式:好记性不如烂笔头.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/白话设计模式 28 讲(完)/00 生活中的设计模式:启程之前,请不要错过我.md.html">00 生活中的设计模式:启程之前,请不要错过我.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/01 监听模式:坑爹的热水器.md.html">01 监听模式:坑爹的热水器.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/02 适配模式:身高不够鞋来凑.md.html">02 适配模式:身高不够鞋来凑.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/03 状态模式:人与水的三态.md.html">03 状态模式:人与水的三态.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/04 单例模式:你是我生命的唯一.md.html">04 单例模式:你是我生命的唯一.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/05 职责模式:我的假条去哪了.md.html">05 职责模式:我的假条去哪了.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/06 中介模式:找房子问中介.md.html">06 中介模式:找房子问中介.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/07 代理模式:帮我拿一下快递.md.html">07 代理模式:帮我拿一下快递.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/08 装饰模式:你想怎么穿就怎么穿.md.html">08 装饰模式:你想怎么穿就怎么穿.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/09 工厂模式:你要拿铁还是摩卡.md.html">09 工厂模式:你要拿铁还是摩卡.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/10 迭代模式:下一个就是你了.md.html">10 迭代模式:下一个就是你了.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/11 组合模式:自己组装电脑.md.html">11 组合模式:自己组装电脑.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/12 构建模式:想要车还是庄园.md.html">12 构建模式:想要车还是庄园.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/13 克隆模式:给你一个分身术.md.html">13 克隆模式:给你一个分身术.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/14 策略模式:怎么来不重要,人到就行.md.html">14 策略模式:怎么来不重要,人到就行.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/15 命令模式:大闸蟹,走起!.md.html">15 命令模式:大闸蟹,走起!.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/白话设计模式 28 讲(完)/16 备忘模式:好记性不如烂笔头.md.html">16 备忘模式:好记性不如烂笔头.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/17 享元模式:颜料很贵必须充分利用.md.html">17 享元模式:颜料很贵必须充分利用.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/18 外观模式:学妹别慌,学长帮你.md.html">18 外观模式:学妹别慌,学长帮你.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/19 访问模式:一千个读者一千个哈姆雷特.md.html">19 访问模式:一千个读者一千个哈姆雷特.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/20 生活中的设计模式:与经典设计模式的不解渊源.md.html">20 生活中的设计模式:与经典设计模式的不解渊源.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/21 生活中的设计模式:那些未完待续的设计模式.md.html">21 生活中的设计模式:那些未完待续的设计模式.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/22 深入解读过滤器模式:制作一杯鲜纯细腻的豆浆.md.html">22 深入解读过滤器模式:制作一杯鲜纯细腻的豆浆.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/23 深入解读对象池技术:共享让生活更便捷.md.html">23 深入解读对象池技术:共享让生活更便捷.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/24 深入解读回调机制:把你技能亮出来.md.html">24 深入解读回调机制:把你技能亮出来.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/25 谈谈我对设计模式的理解.md.html">25 谈谈我对设计模式的理解.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/26 谈谈我对设计原则的思考.md.html">26 谈谈我对设计原则的思考.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/27 谈谈我对项目重构的看法.md.html">27 谈谈我对项目重构的看法.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>16 备忘模式:好记性不如烂笔头</h1>
<p>【故事剧情】</p>
<blockquote>
<p>经过两三年的工作Tony 学到的东西越来越多,业务也越来越熟,终于到了他该带领一个小组进行独立开发的时候了。作为小组负责人后的 Tony工作自然就多了要负责技术的选型、核心代码的开发还要深度参与需求的讨论和评审期间还会被各种会议、面试打扰。</p>
<p>工作压力变大之后Tony 就经常忙的忘了这事、忘了那事为了解决这个问题不至于落下重要的工作Tony 想了一个办法:每天 9 点到公司,花 10 分钟想一下今天有哪些工作项有哪些线上问题必须要解决的有哪些任务需要完成的然后把这些列一个今日待工作项To Do List最后就是看一下新闻刷一下朋友圈等到 9:30 大家来齐后开始每日的晨会,接下来就是一整天的忙碌……</p>
<p>因此在每天工作开始(头脑最清醒的一段时间)之前,把今天需要完成的主要事项记录下来,列一个 To Do List是非常有必要的。这样当你忘记了要做什么事情时只要看一下 To Do List 就能想起所有今天要完成的工作项,就不会因忘记某项工作而影响项目的进度,<strong>好记性不如烂笔头</strong>嘛!</p>
</blockquote>
<p><img src="assets/fb30bb20-7da9-11e8-8748-9f97e9dc7c3b.jpg" alt="img" /></p>
<h3>用程序来模拟生活</h3>
<p>Tony 为了能够随时回想起要做的工作项,把工作项都列到 To Do List 中做为备忘,这样就可以在因为忙碌而忘记时,通过查看 To Do List 来找回记忆。下面我们用程序来模拟一下这个示例。</p>
<p>源码示例:</p>
<pre><code class="language-python">class Engineer:
&quot;工程师&quot;
def __init__(self, name):
self.__name = name
self.__workItems = []
def addWorkItem(self, item):
self.__workItems.append(item)
def forget(self):
self.__workItems.clear()
print(self.__name + &quot;工作太忙了,都忘记要做什么了!&quot;)
def writeTodoList(self):
todoList = TodoList()
for item in self.__workItems:
todoList.writeWorkItem(item)
return todoList
def retrospect(self, todoList):
self.__workItems = todoList.getWorkItems()
print(self.__name + &quot;想起要做什么了!&quot;)
def showWorkItem(self):
if(len(self.__workItems)):
print(self.__name + &quot;的工作项:&quot;)
for idx in range(0, len(self.__workItems)):
print(str(idx + 1) + &quot;. &quot; + self.__workItems[idx] + &quot;;&quot;)
else:
print(self.__name + &quot;暂无工作项!&quot;)
class TodoList:
&quot;工作项&quot;
def __init__(self):
self.__workItems = []
def writeWorkItem(self, item):
self.__workItems.append(item)
def getWorkItems(self):
return self.__workItems
class TodoListCaretaker:
&quot;TodoList管理类&quot;
def __init__(self):
self.__todoList = None
def setTodoList(self, todoList):
self.__todoList = todoList
def getTodoList(self):
return self.__todoList
</code></pre>
<p>测试代码:</p>
<pre><code class="language-python">def testEngineer():
tony = Engineer(&quot;Tony&quot;)
tony.addWorkItem(&quot;解决线上部分用户因昵称太长而无法显示全的问题&quot;)
tony.addWorkItem(&quot;完成PDF的解析&quot;)
tony.addWorkItem(&quot;在阅读器中显示PDF第一页的内容&quot;)
tony.showWorkItem()
caretaker = TodoListCaretaker()
caretaker.setTodoList(tony.writeTodoList())
print()
tony.forget()
tony.showWorkItem()
print()
tony.retrospect(caretaker.getTodoList())
tony.showWorkItem()
</code></pre>
<p>输出结果:</p>
<pre><code>Tony的工作项
1. 解决线上部分用户因昵称太长而无法显示全的问题;
2. 完成PDF的解析;
3. 在阅读器中显示PDF第一页的内容;
Tony工作太忙了都忘记要做什么了
Tony暂无工作项
Tony想起要做什么了
Tony的工作项
1. 解决线上部分用户因昵称太长而无法显示全的问题;
2. 完成PDF的解析;
3. 在阅读器中显示PDF第一页的内容;
</code></pre>
<h3>从剧情中思考备忘模式</h3>
<p>在上面的示例中Tony 将自己的工作项写在 TodoList 中作为备忘,这样,在自己忘记工作内容时,可以通过 TodoList 来快速恢复记忆。像 TodoList 一样,将一个对象的状态或内容记录起来,在状态发生改变或出现异常时,可以恢复对象之前的状态或内容,这在程序中叫做<strong>备忘录模式</strong>,也可简称备忘模式。</p>
<h4>备忘录模式</h4>
<blockquote>
<p>capture the object's internal state without exposing its internal structure, so that the object can be returned to this state later.</p>
<p>在不破坏内部结构的前提下捕获一个对象的内部状态,这样便可在以后将该对象恢复到原先保存的状态。</p>
</blockquote>
<p>备忘录模式的最大功能就是做备份,可以保存对象的一个状态作为备份,这样便可让对象在将来的某一时刻恢复到之前保存的状态。如同游戏中“死”了的英雄可以满血复活一样,再比如很多电器(如电视、冰箱)都有恢复出厂设置的功能,人生没有彩排,但程序却可以让你无数次回放!</p>
<h3>备忘录模式的模型抽象</h3>
<h4>类图</h4>
<p>备忘录模式的类图表示如下:</p>
<p><img src="assets/1e114240-7daa-11e8-8a07-2345656531ad.jpg" alt="enter image description here" /></p>
<p>这是最原始和简单版本的备忘录模式的类图在这个类图中Originator 是要进行备份的对象的发起类,如示例中的 EngineerMemento 是备份的状态,如示例中的 TodoListCaretaker 是备份的管理类,如示例中的 TodoListCaretaker。Originator 依赖 Memento但不直接与 Memento 进行交互,而是与 Memento 的管理类 Caretaker 进行交互。因为对于上层应用来说不用关心具体是怎么备份的以及备份了什么内容,而只需要创建一个备份点,并能从备份点中还原即可。</p>
<p>简单版本的备忘录模式只能备忘一个属性而且只能备忘一次。因此在实际项目中很少看到这个版本,因为大部分实际应用场景都比这复杂。在实际项目中,通常会对原始的备忘录模式进行改造,也就是备忘录模式的升级版本。我们看一下比较通用的一个升级版的类图:</p>
<p><img src="assets/2b1c08d0-7daa-11e8-8748-9f97e9dc7c3b.jpg" alt="enter image description here" /></p>
<h4>代码框架</h4>
<p>因为升级版的备忘录模式比较通用,我们可以抽象出升级版备忘录模式的代码框架模型。</p>
<pre><code class="language-python">from copy import deepcopy
class Memento:
&quot;备忘录&quot;
def setAttribute(self, dict):
&quot;深度拷贝字典dict中的所有属性&quot;
self.__dict__ = deepcopy(dict)
def getAttribute(self):
&quot;获取属性字典&quot;
return self.__dict__
class Caretaker:
&quot;备忘录管理类&quot;
def __init__(self):
self._mementos = {}
def addMemento(self, name, memento):
self._mementos[name] = memento
def getMemento(self, name):
return self._mementos[name]
class Originator:
&quot;备份发起人&quot;
def createMemento(self):
memento = Memento()
memento.setAttribute(self.__dict__)
return memento
def restoreFromMemento(self, memento):
self.__dict__.update(memento.getAttribute())
</code></pre>
<h4>模型说明</h4>
<p>1设计要点</p>
<p>备忘录模式中主要有三个角色,在设计备忘录模式时要找到并区分这些角色:</p>
<ol>
<li><strong>发起人Originator</strong> 需要进行备份的对象。</li>
<li><strong>备忘录Memento</strong> 备份的状态,即一个备份的存档。</li>
<li><strong>备忘录管理者Caretaker</strong> 备份存档的管理者,由它负责与发起人的交互。</li>
</ol>
<p>2优缺点</p>
<p>备忘录模式的优点:</p>
<ul>
<li>给用户提供了一种可以恢复状态的机制,使得用户能够比较方便地回到某个历史的状态。</li>
<li>实现了信息的封装,用户不需要关心状态的保存细节。</li>
</ul>
<p>备忘录模式的缺点:</p>
<ul>
<li>如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存,此时可以限制保存的最大次数。</li>
</ul>
<h3>实战应用</h3>
<p>相信读者一定用过 DOS 命令行或 Linux 终端的命令,通过向上键或向下键可以快速地向前向后翻阅历史指令,选择其中的指令可以再次执行,这极大地方便了我们对命令的操作,这里就用到了对历史命令备忘的思想。下面我们可以模拟一下 Linux 终端的处理程序。</p>
<p><strong>TerminalMonitor.py</strong></p>
<pre><code class="language-python">#!/usr/bin/python
# Authoer: Spencer.Luo
# Date: 5/20/2018
# 引入升级版备忘录模式关键类
from pattern.Memento import Originator, Caretaker, Memento
class TerminalCmd(Originator):
&quot;终端命令&quot;
def __init__(self, text):
self.__cmdName = &quot;&quot;
self.__cmdArgs = []
self.parseCmd(text)
def parseCmd(self, text):
&quot;从字符串中解析命令&quot;
subStrs = self.getArgumentsFromString(text, &quot; &quot;)
# 获取第一个字段作为命令名称
if(len(subStrs) &gt; 0):
self.__cmdName = subStrs[0]
# 获取第一个字段之后的所有字符作为命令的参数
if (len(subStrs) &gt; 1):
self.__cmdArgs = subStrs[1:]
def getArgumentsFromString(self, str, splitFlag):
&quot;通过splitFlag进行分割获得参数数组.&quot;
if (splitFlag == &quot;&quot;):
print(&quot;splitFlag is empty!&quot;)
return &quot;&quot;
data = str.split(splitFlag)
result = []
for item in data:
item.strip()
if (item != &quot;&quot;):
result.append(item)
return result;
def showCmd(self):
print(self.__cmdName, self.__cmdArgs)
class TerminalCaretaker(Caretaker):
&quot;终端的备忘录管理类&quot;
def showHistoryCmds(self):
&quot;显示历史命令&quot;
for key, obj in self._mementos.items():
name = &quot;&quot;
value = []
if(obj._TerminalCmd__cmdName):
name = obj._TerminalCmd__cmdName
if(obj._TerminalCmd__cmdArgs):
value = obj._TerminalCmd__cmdArgs
print(&quot;&quot; + str(key) + &quot;条命令: &quot; + str(name) + &quot; &quot; + str(value))
def testTerminal():
cmdIdx = 0
caretaker = TerminalCaretaker()
curCmd = TerminalCmd(&quot;&quot;)
while (True):
strCmd = input(&quot;请输入指令:&quot;);
strCmd = strCmd.lower()
if (strCmd.startswith(&quot;q&quot;)):
exit(0)
elif(strCmd.startswith(&quot;h&quot;)):
caretaker.showHistoryCmds()
# 通过&quot;!&quot;符号表示获取历史的某个指令
elif(strCmd.startswith(&quot;!&quot;)):
idx = int(strCmd[1:])
curCmd.restoreFromMemento(caretaker.getMemento(idx))
curCmd.showCmd()
else:
curCmd = TerminalCmd(strCmd)
curCmd.showCmd()
caretaker.addMemento(cmdIdx, curCmd.createMemento())
cmdIdx +=1
testTerminal()
</code></pre>
<p><strong>输出结果:</strong></p>
<p><img src="assets/4328cda0-7daa-11e8-be78-bb5c0f92d7f1.jpg" alt="enter image description here" /></p>
<h3>应用场景</h3>
<ul>
<li>需要保存/恢复对象的状态或数据,如游戏的存档、虚拟机的快照。</li>
<li>需要实现撤销、恢复功能的场景,如 Word 中的 Ctrl+Z、Ctrl+Y 功能DOS 命令行或 Linux 终端的命令记忆功能。</li>
<li>提供一个可回滚的操作,如数据库的事务管理。</li>
</ul>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/白话设计模式 28 讲(完)/15 命令模式:大闸蟹,走起!.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/白话设计模式 28 讲(完)/17 享元模式:颜料很贵必须充分利用.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997b9e7caf3cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>