本文来自李明子csdn博客(http://blog.csdn.net/free1985),商业转载请联系博主获得授权,非商业转载请注明出处!
备忘录(Memento)模式用于记录对象某一时刻的内部状态以便需要时还原对象的内部状态。
从目的可知,备忘录主要用于对象状态的还原。对于对象经过一系列操作后的还原我们通常可以采取如下三种策略:
备忘录各参与者的交互如图2-2所示。
图2-2 备忘录交互图
结合图2-1,下面介绍各类在备忘录设计模式中扮演的角色。
3.1 Memento
Memento是备忘录,用于保存对象某一时刻的内部状态。这些被保存的内部状态应该被隐藏,仅对创建它的原发器Originator可见。
3.2 Originator
Originator是原发器,负责将对象内部状态保存为备忘录以及根据备忘录还原对象的内部状态。
3.3 Caretaker
Caretaker是管理者,负责触发保存和还原对象事件以及对备忘录的保存。
下面我们用一个业务场景实例来进一步讲解备忘录的使用。
4.1 场景介绍
某平面绘制工具可以在3*3的画布上用255种颜色的画笔绘制图形。画布不支持图层,后绘制的图形会覆盖先绘制的图形。该工具提供撤销操作,可以撤销最后一步操作。
以下各节将介绍该场景各类的具体实现及其在备忘录设计模式中所对应的参与者角色。
4.2 Memento
Memento是备忘录类,维护某一时刻画布的像素矩阵。对应于备忘录模式的参与者,Memento即是备忘录Memento。下面的代码给出了Memento的声明。
package demo.designpattern.memento.canvas;
import java.util.Arrays;
import java.util.Date;
/**
* 画布备忘录
* Created by LiMingzi on 2017/10/12.
*/
public class Memento {
/**
* 画布像素矩阵
*/
private byte[][] pixels = new byte[3][3];
/**
* 记录时间
*/
private Date recordDate;
/**
* 设置像素矩阵
* @param pixels 像素矩阵
*/
void setPixels(byte[][] pixels) {
for (int i = 0; i < pixels.length; i++) {
this.pixels[i] = Arrays.copyOf(pixels[i],pixels[i].length);
}
this.recordDate =new Date();
}
/**
* 获取像素矩阵
* @return 像素矩阵
*/
byte[][] getPixels() {
return pixels;
}
/**
* 获取记录时间
* @return
*/
public Date getRecordDate() {
return recordDate;
}
}
上述代码中,14行,维护了画布某时刻的像素矩阵pixels。25行,设置像素矩阵方法setPixels使用深拷贝保存像素矩阵。该方法与36行的获取像素矩阵方法getPixels使用了默认的可见性。这意味着,像素矩阵这一内部状态只有与备忘录Memento在同一包内的画布类Canvas可以访问。与此相对的,另一成员变量记录时间recordDate(19行)的get方法(44行)被声明为public,可以被所有类访问。Memento的这种接口开放形式通常被叫做“宽接口”和“窄接口”。在C++中,可以用友元实现。
4.3 Canvas
Canvas是画布类,实现了向指定坐标绘制像素点的功能。对应于备忘录模式的参与者,Canvas是原发器Originator。下面的代码给出了Canvas的声明。
package demo.designpattern.memento.canvas;
import java.util.Arrays;
import java.util.Date;
/**
* 画布
* Created by LiMingzi on 2017/10/12.
*/
public class Canvas {
/**
* 画布像素矩阵
*/
private byte[][] pixels = new byte[3][3];
/**
* 填充颜色
* @param x 横坐标
* @param y 纵坐标
* @param color 颜色(1-255)
*/
public void fill(int x,int y,int color){
pixels[x][y] = (byte)color;
}
/**
* 创建备忘录
* @return 备忘录
*/
public Memento createMemento(){
// 备忘录
Memento memento = new Memento();
memento.setPixels(pixels);
return memento;
}
/**
* 还原画布
* @param memento 备忘录
*/
public void revertCanvas(Memento memento){
// 备忘录中的像素矩阵
byte[][] pixels = memento.getPixels();
for (int i = 0; i < pixels.length; i++) {
this.pixels[i] = Arrays.copyOf(pixels[i],pixels[i].length);
}
}
/**
* 输出像素矩阵
*/
public void outputPixels(){
for (byte[] pixelRow : pixels) {
// 每行的输出信息
String rowString = "";
for (byte pixel : pixelRow) {
rowString+=String.format("%1$-4s",decodeColor(pixel));
}
System.out.println(rowString);
}
}
/**
* 解析颜色值
* @param colorByte 用byte保存的颜色值
* @return 解码后的int表示的颜色值
*/
private int decodeColor(byte colorByte){
return colorByte<0?256+colorByte:colorByte;
}
}
上述代码中,14行声明了像素矩阵pixels,它是本示例中备忘录维护的内部状态。22行,填充颜色方法fill向指定坐标填充像素点。30行,createMemento方法用于创建备忘录对象,它调用了Memento设置内部状态的方法setPixels方法。41行,还原画布方法revertCanvas用指定的备忘录还原画布,它调用了Memento获取内部状态的方法getPixels方法。52行输出像素矩阵方法替代了实际的绘制过程,输出像素矩阵中保存的内容。68行,decodeColor方法将用byte类型保存的颜色值解码为int类型。它给我们的提示是,在实际应用中,备忘录可能带来大量的内存损耗,应尽可能减少对存储的占用。
4.4 Drawer
Drawer是绘制器类,用于实现绘制功能。对应于备忘录模式的参与者,Drawer是管理者Caretaker。下面的代码给出了Drawer的声明。
package demo.designpattern.memento;
import demo.designpattern.memento.canvas.Canvas;
import demo.designpattern.memento.canvas.Memento;
import java.util.ArrayList;
import java.util.List;
/**
* 绘制器
* Created by LiMingzi on 2017/10/12.
*/
public class Drawer {
/**
* 画布
*/
private Canvas canvas = new Canvas();
/**
* 备忘录集合
*/
private List mementos = new ArrayList();
/**
* 绘制线段
*
* @param fromX 起始点横坐标
* @param fromY 起始点纵坐标
* @param toX 终结点横坐标
* @param toY 终结点纵坐标
* @param color 颜色(1-255)
*/
public void drawLine(int fromX, int fromY, int toX, int toY, int color) {
mementos.add(canvas.createMemento());
System.out.println("绘制线段(" + fromX + "," + fromY + "),(" + toX + "," + toY + ")," + color + ":");
if (fromX == toX) {
// 最小纵坐标
int minY;
// 最大纵坐标
int maxY;
if (fromY < toY) {
minY = fromY;
maxY = toY;
} else {
minY = toY;
maxY = fromY;
}
for (int i = minY; i < maxY + 1; i++) {
canvas.fill(fromX, i, color);
}
} else if (fromY == toY) {
// 最小横坐标
int minX;
// 最大横坐标
int maxX;
if (fromX < toX) {
minX = fromX;
maxX = toX;
} else {
minX = toX;
maxX = fromX;
}
for (int i = minX; i < maxX + 1; i++) {
canvas.fill(i, fromY, color);
}
}
canvas.outputPixels();
}
/**
* 撤销下一步
*/
public void withdraw() {
if(mementos.size()<1){
return;
}
// 上一步备忘录
Memento memento = mementos.get(mementos.size() - 1);
System.out.println("撤销最后一步操作(恢复"+memento.getRecordDate()+"时的画布):");
canvas.revertCanvas(memento);
canvas.outputPixels();
mementos.remove(memento);
}
}
上述代码中,21行,mementos维护了画布备忘录集合以便在撤销操作时使用。32行drawLine方法实现线段绘制功能。该方法首先调用canvas的createMemento方法创建画布的备忘录,并将其加入到备忘录链表中;然后根据起止点及颜色计算需要着色的像素点一一着色;最后调用canvas的outputPixels方法绘制画布。71行,撤销下一步方法withdraw用于撤销最后一步操作。首先从操作链表中取出最后一个备忘录;然后调用canvas的revertCanvas方法恢复该备忘录中记录的画布状态;接着调用canvas的outputPixels方法绘制画布;最后将该备忘录从备忘录链表中移除。值得注意的是,因为Memento对Drawer开放的是窄接口,因此,Drawer可以像78行那样调用Memento的getRecordDate方法,却无法调用宽接口getPixels和setPixels方法。
4.5 测试代码
为了测试本文中的代码,我们可以编写如下测试代码。测试代码中我们先后绘制两条不同颜色的线段,之后依次撤销这两个绘制操作。为了区分备忘录的创建时间,两次绘制过程间人为加入1000ms延迟。
/**
* 备忘录测试
*/
public static void mementoTest(){
// 绘制器
Drawer drawer = new Drawer();
drawer.drawLine(0,0,2,0,1);
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
drawer.drawLine(0,0,0,2,255);
drawer.withdraw();
drawer.withdraw();
}
编译运行后,得到如下测试结果:
绘制线段(0,0),(2,0),1:
1 0 0
1 0 0
1 0 0
绘制线段(0,0),(0,2),255:
255 255 255
1 0 0
1 0 0
撤销最后一步操作(恢复Thu Oct 12 15:21:27 CST 2017时的画布):
1 0 0
1 0 0
1 0 0
撤销最后一步操作(恢复Thu Oct 12 15:21:26 CST 2017时的画布):
0 0 0
0 0 0
0 0 0