javascript设计模式--备忘录模式(Memento)
1 <!doctype html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Memento</title> 6 </head> 7 <body> 8 9 <script> 10 /** 11 * 备忘录模式 12 * 13 * 定义: 14 * 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。 15 * 16 * 本质: 17 * 保存和恢复内部状态 18 * 19 * 一个备忘录是一个对象,它存储另一个对象在某个瞬间的内部状态,后者被称为备忘录的原发器。 20 * 21 * 备忘录模式引入一个存储状态的备忘录对象,为了让外部无法访问这个对象的值,一般把这个对象实现成为需要保存数据的对象的内部类,通常还是私有的。这样,除了这个需要保存数据的对象,外部无法访问到这个备忘录对象的数据,这就保证了对象的封装性不被破坏。 22 * 但是这个备忘录对象需要存储在外部,为了避免让外部访问到这个对象内部的数据,备忘录模式引入了一个备忘录对象的窄接口,这个接口一般是空的,什么方法都没有,这样外部存储的地方,只是知道存储了一些备忘录接口的对象,但是由于接口是空的,它们无法通过接口去访问备忘录对象内部的数据。 23 * 24 * 功能 25 * 备忘录模式的功能,首先是在不破坏封装性的前提下,捕获一个对象的内部状态,这里要注意两点,一个是不破坏封装性,也就是对象不能暴露它不应该暴露的细节;另外一个是捕获的是对象的内部状态,而且通常还是运行期间某个时刻对象的内部状态。 26 * 27 * 为什么要捕获这个对象的内部状态?捕获这个内部状态有什么用? 28 * 是为了在以后某个时候,将该对象的状态恢复到备忘录所保存的状态,这才是备忘录真正的目的。前面保存的状态就是为了后面恢复,虽然不是一定要恢复,但是目的是为了恢复。 29 * 30 * 捕获的状态存放在哪里? 31 * 在备忘录对象中。而备忘录对象通常会被存储在原发器对象之外,也就是内保存状态的对象的外部,通常是存放在管理者对象那里。 32 * 33 * 备忘录对象 34 * 备忘录对象通常就是记录原发器要保存的状态的对象,简单点的实现,也就是封装数据的对象。 35 * 但是备忘录对象和普通的封装数据的对象还是有区别的。主要是备忘录对象一般只让原发器对象来操作,而不是像普通的封装数据的对象那样,谁都可以使用。为了保证这点,通常会把备忘录对象作为原发器对象的内部类来实现,而且是线程私有的,这就断绝了外部来访问这个备忘录对象的途径。 36 * 备忘录对象需要保存在原发器对象之外,为了与外部交互,通常备忘录对象都会实现一个窄接口,来标识对象的类型。 37 * 38 * 原发器对象 39 * 原发器对象就是需要被保存状态的对象,也是有可能需要恢复状态的对象。原发器一般会包含备忘录对象的实现。 40 * 通常原发器对象应该提供捕获某个时刻对象内部状态的方法,在这个方法中,原发器兑现会创建备忘录对象,把需要保存的状态数据设置到备忘录对象中,然后把备忘录对象提供给管理者对象来保存。 41 * 当然,原发器对象也应该提供这样的方法,按照外部要求来恢复内部状态到某个备忘录对象记录的状态。 42 * 43 * 管理者对象 44 * 管理者对象主要负责保存备忘录对象。 45 * 1.调用原发器获得备忘录对象后,备忘录对象放在那里,哪个对象就可以算是管理者对象。 46 * 2.管理者对象并不是只能管理一个备忘录对象。 47 * 3.狭义的管理者对象制管理同一类的备忘录对象,广义的是可以管理不同类型的备忘录对象。 48 * 4.管理者对象基本功能主要是缓存的实现,或者是一个简单的对象实例池。 49 * 5.管理者虽然能存取备忘录对象,但是不能访问备忘录对象内部的数据。 50 * 51 * 窄接口和宽接口 52 * 窄接口:管理者只能看到备忘录的窄接口,窄接口的实现中通常没有任何的方法,只是一个类型标识。窄接口使得管理者只能将备忘录传递给其他对象。 53 * 宽接口:原发器能够看到一个宽接口,允许它访问所需的所有数据,来返回到先前的状态。 54 * 55 * 使用备忘录的潜在代价 56 * 标准的备忘录模式的实现机制是依靠缓存来实现的,因此,当需要备忘的数据量较大时,或者是存储的备忘录对象数据量不大但是数量很多的时候,或者是用户很频繁地创建备忘录对象的时候,这些都会导致非常大的开销。 57 * 58 * 增量存储 59 * 如果需要频繁地创建备忘录对象,而且创建和应用备忘录对象来恢复状态的顺序是可控的,那么可以让备忘录进行增量存储,也就是备忘录可以仅仅存储原发器内部相对于上一次存储状态后的增量改变。 60 * 61 * 备忘录模式调用顺序 62 * 创建备忘录对象的阶段: 63 * 1.创建原发器对象,调用原发器的业务处理方法。 64 * 2.调用createMemento方法 65 * 3.创建备忘录对象。 66 * 4.调用saveMemento方法吧备忘录对象存放在管理者对象那里。 67 * 68 * 使用备忘录对象来恢复原发器对象状态的阶段: 69 * 1.调用retriveMemento方法来获取要恢复的备忘录对象。 70 * 2.调用setMemento方法,传入备忘录对象,来让原发器恢复状态。 71 * 3.调用方法来获取状态数据。 72 * 73 * 74 * 离线存储 75 * 从备忘录模式的功能和实现上,是可以把备忘录的数据实现成为离线存储。 76 */ 77 78 (function () { 79 // 示例代码 80 81 // 原发器对象 82 function Originator() { 83 // 表示原发器的状态 84 var state = ''; 85 // 创建保存原发器对象的状态的备忘录对象 86 this.createMemento = function () { 87 return new MementoImpl(state); 88 }; 89 // 重新设置原发器对象的状态,让其回到备忘录对象记录的状态 90 this.setMemento = function (memento) { 91 this.state = memento.getState(); 92 }; 93 // 真正的备忘录对象,实现备忘录窄接口 94 // 实现程私有的内部类,不让外部访问 95 function MementoImpl(state) { 96 // 表示需要保存的状态 97 state = state || ''; 98 // 只提供getter 99 this.getState = function () { 100 return state; 101 }; 102 } 103 } 104 105 // 负责保存备忘录的对象 106 function Caretaker() { 107 var memento = null; 108 // 保存备忘录对象 109 this.saveMemento = function (mementoObj) { 110 memento = mementoObj; 111 }; 112 // 获取被保存的备忘录对象 113 this.retriveMemento = function () { 114 return memento; 115 }; 116 } 117 118 }()); 119 120 (function () { 121 // 示例1 122 123 // 模拟运行流程A 124 function FlowAMock(flowName) { 125 // 流程名称,不需要外部存储的状态数据 126 var flowName = flowName || ''; 127 // 代指某个中间结果,需要外部存储的状态数据 128 var tempResult = 0; 129 // 代指某个中间结果,需要外部存储的状态数据 130 var tempState = ''; 131 // 运行流程的第一个阶段 132 this.runPhaseOne = function () { 133 // 在这个阶段,可能产生了中间结果,示意一下 134 tempResult = 3; 135 tempState = 'PhaseOne'; 136 }; 137 // 按照方案一来运行流程后半部分 138 this.schema1 = function () { 139 tempState += ', Schema1'; 140 console.log(tempState + ': now run ' + tempResult); 141 tempResult += 11; 142 }; 143 // 按照方案二来运行流程后半部分 144 this.schema2 = function () { 145 tempState += ', Schema2'; 146 console.log(tempState + ': now run ' + tempResult); 147 tempResult += 22; 148 }; 149 // 创建保存原发器对象状态的备忘录对象 150 this.createMemento = function () { 151 return new MementoImpl(tempResult, tempState); 152 }; 153 // 重新设置原发器对象的状态,让其回到备忘录对象记录的状态 154 this.setMemento = function (memento) { 155 tempResult = memento.getTempResult(); 156 tempState = memento.getTempState(); 157 }; 158 // 真正的备忘录对象,实现备忘录窄接口 159 function MementoImpl(tempResult, tempState) { 160 tempResult = tempResult || 0; 161 tempState = tempState || ''; 162 this.getTempResult = function () { 163 return tempResult; 164 }; 165 this.getTempState = function () { 166 return tempState; 167 }; 168 } 169 } 170 171 // 负责保存模拟运行流程A的对象的备忘录对象 172 function FlowAMementoCareTaker() { 173 var memento = null; 174 // 保存备忘录对象 175 this.saveMemento = function (mementoObj) { 176 memento = mementoObj; 177 }; 178 // 获取被保存的备忘录对象 179 this.retriveMemento = function () { 180 return memento; 181 }; 182 } 183 184 // 创建模拟运行流程的对象 185 var mock = new FlowAMock('TestFlow'); 186 // 运行流程第一个阶段 187 mock.runPhaseOne(); 188 // 创建一个管理者 189 var careTaker = new FlowAMementoCareTaker(); 190 // 创建此时对象的备忘录对象,并保存到管理者对象那里,后面要用 191 careTaker.saveMemento(mock.createMemento()); 192 193 // 按照方案一来运行流程的后半部分 194 mock.schema1(); 195 196 // 从管理者获取备忘录对象,然后设置回去 197 // 让模拟运行流程的对象自己恢复自己的内部状态 198 mock.setMemento(careTaker.retriveMemento()); 199 200 // 按照方案二来运行流程的后半部分 201 mock.schema2(); 202 203 }()); 204 205 (function () { 206 // 再次实现可撤销操作 207 208 function AbstractCommand() { 209 this.operation = null; 210 } 211 212 AbstractCommand.prototype = { 213 execute: function () { 214 }, 215 undo: function (memento) { 216 this.operation.setMemento(memento); 217 }, 218 redo: function (memento) { 219 this.operation.setMemento(memento); 220 }, 221 createMemento: function () { 222 return this.operation.createMemento(); 223 } 224 }; 225 226 function AddCommand(opeNum) { 227 AbstractCommand.call(this); 228 this.opeNum = opeNum; 229 } 230 231 AddCommand.prototype = { 232 __proto__: AbstractCommand.prototype, 233 234 execute: function () { 235 this.operation.add(this.opeNum); 236 } 237 }; 238 239 function SubstractCommand(opeNum) { 240 AbstractCommand.call(this); 241 this.opeNum = opeNum; 242 } 243 244 SubstractCommand.prototype = { 245 __proto__: AbstractCommand.prototype, 246 247 execute: function () { 248 this.operation.substract(this.opeNum); 249 } 250 }; 251 252 function Operation() { 253 var result = 0; 254 this.getResult = function () { 255 return result; 256 }; 257 this.add = function (num) { 258 result += num; 259 }; 260 this.substract = function (num) { 261 result -= num; 262 }; 263 this.createMemento = function () { 264 return new MementoImpl(result); 265 }; 266 this.setMemento = function (memento) { 267 result = memento.getResult(); 268 }; 269 270 function MementoImpl(result) { 271 result = result || 0; 272 this.getResult = function () { 273 return result; 274 }; 275 } 276 } 277 278 function Calculator() { 279 this.undoCmds = []; 280 this.redoCmds = []; 281 this.undoMementos = []; 282 this.redoMementos = []; 283 this.addCmd = null; 284 this.substractCmd = null; 285 } 286 287 Calculator.prototype = { 288 addPressed: function () { 289 var m1 = this.addCmd.createMemento(); 290 291 this.addCmd.execute(); 292 this.undoCmds.push(this.addCmd); 293 294 var m2 = this.addCmd.createMemento(); 295 this.undoMementos.push([m1, m2]); 296 }, 297 substractPressed: function () { 298 var m1 = this.substractCmd.createMemento(); 299 300 this.substractCmd.execute(); 301 this.undoCmds.push(this.substractCmd); 302 303 var m2 = this.substractCmd.createMemento(); 304 this.undoMementos.push([m1, m2]); 305 }, 306 // 撤销 307 undoPressed: function () { 308 if (this.undoCmds.length) { 309 var cmd = this.undoCmds.pop(); 310 var ms = this.undoMementos.pop(); 311 312 cmd.undo(ms[0]); 313 this.redoCmds.push(cmd); 314 this.redoMementos.push(ms); 315 } else { 316 console.log('很抱歉,没有可撤销命令'); 317 } 318 }, 319 // 恢复 320 redoPressed: function () { 321 if (this.redoCmds.length) { 322 var cmd = this.redoCmds.pop(); 323 var ms = this.redoMementos.pop(); 324 325 cmd.redo(ms[1]); 326 327 this.undoCmds.push(cmd); 328 this.undoMementos.push(ms); 329 } else { 330 console.log('很抱歉,没有可撤销命令'); 331 } 332 } 333 }; 334 335 var operation = new Operation(); 336 var addCmd = new AddCommand(5); 337 var substractCmd = new SubstractCommand(3); 338 339 addCmd.operation = operation; 340 substractCmd.operation = operation; 341 342 var calculator = new Calculator(); 343 calculator.addCmd = addCmd; 344 calculator.substractCmd = substractCmd; 345 346 calculator.addPressed(); 347 console.log('一次加法运算后的结果为:' + operation.getResult()); 348 349 calculator.substractPressed(); 350 console.log('一次减法运算后的结果为:' + operation.getResult()); 351 352 calculator.undoPressed(); 353 console.log('撤销一次后的结果为:' + operation.getResult()); 354 355 calculator.undoPressed(); 356 console.log('再撤销一次后的结果为:' + operation.getResult()); 357 358 calculator.redoPressed(); 359 console.log('恢复操作一次后的结果为:' + operation.getResult()); 360 361 calculator.redoPressed(); 362 console.log('在恢复操作一次后的结果为:' + operation.getResult()); 363 }()); 364 365 /** 366 * 备忘录模式的优点 367 * 1.更好的封装性。 368 * 备忘录模式通过使用备忘录对象,来封装原发器对象的内部状态,虽然这个对象是保存在原发器对象的外部,但是由于备忘录对象的窄接口并不提供任何方法。这样有效地保证了对原发器对象内部状态的封装,不把原发器对象的内部实现细节暴露给外面。 369 * 2.简化了原发器。 370 * 备忘录对象被保存到了原发器对象之外,让客户来管理他们请求的状态,从而让原发器对象得到简化。 371 * 3.窄接口和宽接口。 372 * 373 * 缺点 374 * 1.可能会导致高开销。 375 * 376 * 377 * 何时选用 378 * 1.如果必须保存一个对象在某一个时刻的全部或者部分状态,方便在以后需要的时候,可以把该对象恢复到先前的状态,可以使用备忘录模式。使用备忘录对象来封装和保存需要保存的内部状态,然后把备忘录对象保存到管理者对象中,在需要的时候,再从管理者对象中获取备忘录对象,来恢复对象的状态。 379 * 2.如果需要保存一个对象的内部状态,但是如果用接口来让其他对象直接得到这些需要保存的状态,将会暴露对象的实现细节并破坏对象的封装性,这时可以使用备忘录模式。把备忘录对象实现成为原发器对象的内部类,而且还是私有的,从而保证只有原发器对象才能访问该备忘录对象。这样既保存了需要保存的状态,又不会暴露原发器对象的内部实现细节。 380 * 381 * 382 * 相关模式 383 * 备忘录模式和命令模式 384 * 可以组合使用。 385 * 命令模式中,在实现命令的撤销和重做的时候,可以使用备忘录模式,在命令操作的时候记录下操作前后的状态,然后在命令撤销和重做的时候,直接使用相应的备忘录对象来恢复状态就可以了。 386 * 在这种撤销的执行顺序和重做的执行顺序可控的情况下,备忘录对象还可以采用增量式记录的方式,有效减少缓存的数据量。 387 * 388 * 备忘录模式和原型模式 389 * 可以组合使用。 390 * 在原发器对象创建备忘录对象的时候,如果原发器对象中全部或者大部分的状态都需要保存,一个简洁的方式就是直接克隆一个原发器对象。也就是说,这个时候备忘录对象里面存放的是一个原发器对象的实例。 391 */ 392 393 (function () { 394 // http://www.dofactory.com/javascript-memento-pattern.aspx 395 var Person = function (name, street, city, state) { 396 this.name = name; 397 this.street = street; 398 this.city = city; 399 this.state = state; 400 }; 401 402 Person.prototype = { 403 hydrate: function () { 404 var memento = JSON.stringify(this); 405 return memento; 406 }, 407 dehydrate: function (memento) { 408 var m = JSON.parse(memento); 409 410 this.name = m.name; 411 this.street = m.street; 412 this.city = m.city; 413 this.state = m.state; 414 } 415 }; 416 417 var CareTaker = function () { 418 var mementos = {}; 419 420 this.add = function (key, memento) { 421 mementos[key] = memento; 422 }; 423 this.get = function (key) { 424 return mementos[key]; 425 }; 426 }; 427 428 new function run() { 429 430 var mike = new Person("Mike Foley", "1112 Main", "Dallas", "TX"); 431 var john = new Person("John Wang", "48th Street", "San Jose", "CA"); 432 433 var caretaker = new CareTaker(); 434 435 // save state 436 437 caretaker.add(1, mike.hydrate()); 438 caretaker.add(2, john.hydrate()); 439 440 // mess up their names 441 442 mike.name = "King Kong"; 443 john.name = "Superman"; 444 445 // restore original state 446 447 mike.dehydrate(caretaker.get(1)); 448 john.dehydrate(caretaker.get(2)); 449 450 console.log(mike.name); 451 console.log(john.name); 452 }; 453 }()); 454 </script> 455 </body> 456 </html>