一.定义
每个人都会犯错,都希望有"后悔药"能弥补自己过失,在计算机应用中,客户同样会常常犯错,希望回退到某个历史状态,如:Eclipse中使用Ctrl+Z 回退键,该需求可使用备忘录模式实现,该模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的某个历史状态;
备忘录模式(Memento Pattern)
1) 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以后就可通过该对象恢复到原先保存的状态;
2) 又称快照模式,属于行为模式,是用来记录一个对象的某种状态或某些数据,当要回退时,可从备忘录对象中获取原数据进行恢复;
3) 简单说:一个对象中一般都封装了很多属性,这些属性的值会随程序的运行而变化,当我们需要保存某一个时刻该对象的某些值时,
我们就再创建一个对象,将当前对象的一些属性保存到新的对象中,当我们需要恢复的时候再从新的对象中取出属性值即可;
二. 特点
1. 优点
1) 给用户提供了一种可以恢复状态的机制,当用户需要时能比较方便地将数据恢复到某个历史状态;
2) 实现了内部状态的封装,除了创建它的发起者之外,其他对象都不能访问这些状态信息;
3) 简化了发起者类,使其无需管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理;
2. 缺点
1) 资源消耗大,如果要保存的内部状态过多或特别繁琐,将会占用较大的内存资源;
2) 如果需要保存的属性发生了变化,那么我们还需要修改自己的代码。
三. 应用场景
-
需要保存与恢复数据的场景,如玩游戏的中间结构的存档功能;
-
需要提供一个可回滚操作的场景,如Word、Eclipse等软件在编辑时 按 Ctrl+Z 组合键,还有数据库中的事务操作;
-
当一个对象需要记录其历史属性,并且需要记录的属性是所有属性的一部分时,可以使用备忘录模式记录属性;
-
为了节约内存,备忘录模式可以和原型模式配合使用 配合深拷贝;
四. 模式的结构
1.备忘录模式结构图
2.备忘录模式角色及职责
-
Originator(发起者) : 记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,是需要保存状态的对象;
-
Memento(备忘录) : 负责保存好记录,即Originator 内部状态,在需要的时候提供这些内部状态给发起者;
-
Caretaker(管理者) : 对备忘录对象进行管理,提供保存与获取备忘录的功能,但不能对备忘录的内容进行访问与修改;
-
如果希望保存多个originator对象的不同时间的状态,可以使用HashMap<String, List<Memento>> 进行管理;
五. 模式的实现
0. 需求-游戏角色状态恢复问题
游戏角色有攻击力和防御力,在大战Boss前保存自身攻防状态,当大战Boss后攻防下降,这时从备忘录对象恢复到大战前的状态;
1.传统方案解决
类图 及 问题如下:
1) 一个对象就对应一个保存对象状态的对象,这样当我们游戏的对象很多时,不利于管理,内存开销也很大;
2) 传统方式是简单地做备份,new出另一个对象出来,再把需要备份的数据放到这个新对象中备份, 这样就暴露了对象内部的细节;
2.使用备忘录模式
1>实例结构图
2>流程说明
使用了备忘录模式之后,对方在提供给我们Orginator类的基础上还给我们提供了Memento、Caretaker类。我们客户端只需在需要备份的地方调用createMementor()函数,需要还原的地方调用recoverFromMemento(memento:Memento)。如果需要备份的属性发生了变化,那也只是第三方类库Orginator、Mementor进行了修改,对于我们客户端来说,代码不需要任何修改。
3>相关代码实现
/**
* 备忘录模式客户端
* @author NorthStar
* @date 2021/12/18 20:39
*/
object MementoClient {
@JvmStatic
fun main(args: Array<String>) {
val gameRole = GameRole()
val caretaker = Caretaker()
gameRole.vit=100
gameRole.def=100
println("和boss大战前的状态")
gameRole.display()
caretaker.setRoleMemento(gameRole.createMemento()) //保存状态
println("和boss开始大战~")
gameRole.vit=30
gameRole.def=30
println("和boss大战后的状态")
gameRole.display()
println("大战后 使用备忘录恢复到大战前~")
gameRole.recoverGameRoleFromMemento(caretaker.getRoleMemento())
println("恢复后的状态~")
gameRole.display()
}
}
/**
* 备忘录角色
* vit 攻击力
* def 防御力
*/
class Memento(var vit:Int=0,var def:Int=0)/**
// 游戏角色 -Originator
class GameRole {
var vit:Int=0
var def:Int=0
//创建 Memento 根据当前状态得到 Memento 对象
fun createMemento():Memento{
return Memento(vit,def)
}
//通过备忘录对象,恢复gameRole状态
fun recoverGameRoleFromMemento(memento:Memento){
vit=memento.vit
def=memento.def
}
//显示当前游戏角色的状态
fun display(){
println("游戏角色当前的攻击力: $vit 防御力: $def")
}
}
/**
* 守护者对象, 保存游戏角色状态
*/
class Caretaker {
//若对GameRole 只保存一次状态
lateinit var memento: Memento
//若对GameRole 需要保存多次
var mementoList: ArrayList<Memento> = ArrayList()
//若对多个游戏角色保存多个状态
var roleMementoMap: HashMap<String, ArrayList<Memento>> = HashMap()
fun add(memento: Memento) {
mementoList.add(memento)
}
//获取第index个 Originator 的备忘录对象(即保存状态)
fun getMemento(index: Int): Memento {
return mementoList[index]
}
fun setRoleMemento(memento: Memento) {
this.memento=memento
}
fun getRoleMemento(): Memento {
return memento
}
}
程序运行结果
和boss大战前的状态
游戏角色当前的攻击力: 100 防御力: 100
和boss开始大战~
和boss大战后的状态
游戏角色当前的攻击力: 30 防御力: 30
大战后 使用备忘录恢复到大战前~
恢复后的状态~
游戏角色当前的攻击力: 100 防御力: 100
六.备忘录模式与克隆的区别
有人说,备忘录模式就是用来保存对象中一些属性,那完全可以克隆这个对象,何必采用构造复杂的备忘录模式呢?原因有以下几点:
-
进行一次克隆会将对象的全部属性都复制到一个新的对象中去,而当我们仅需备份对象中一部分属性时使用备忘录模式最高效;
-
使用备忘录模式,当需要备份的属性发生变化后,只需修改Orginator(发起类)和Mementor(备忘录类),无需修改客户端代码;
-
使用备忘录模式,备份数据虽交给了其他对象保存,但对象的备份和还原操作都是通过对象本身函数实现的,体现了封装的思想;