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>

 

posted @ 2014-04-12 12:41  LukeLin  阅读(928)  评论(0编辑  收藏  举报