一、设计模式概念
设计模式给人的第一感觉,大概就是抽象,还有遥不可及。其实随着开发经验的逐渐积累,当你偶尔翻一下关于设计模式之类的书籍,你就会发现,里面的某些模式自己曾经实现过,只是尚未上升到理论的阶段。
设计模式,是针对某一类问题的最佳解决方案。可以这样定义设计模式:“设计模式是从许多优秀的软件系统中总结出来的可复用的设计方案”。在这里,不打算讨论全部设计模式的定义和使用。只是希望抛砖引玉,通过一个图形界面,让读者对于设计模式之一的“备忘录模式”有一个直观的感受。
二、问题提出
在说明备忘录模式之前,先抛出一个问题。玩过单机游戏的读者,应该经常使用到游戏的保存功能。单机等游戏能够随时将当前的进度保存起来,以便在以后的某个时刻读取进度,这样的游戏可以满足玩家长时间的体验。那么,如何使用设计模式来优雅地解决这样的问题呢?希望看完全文后的读者能够对解决这个问题有个初步的感知。
一直以来,设计模式由于本身的抽象性和复杂性,容易让初学者望而却步。因此本方所用的例子,将抛开纯理论分析,以一个画图板的undo操作来说明问题。
先给出备忘录模式的定义——
三、备忘录模式
“备忘录模式,是在不破坏封闭性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可将对象恢复到原先保存的状态”。该定义来自GOF四人帮对该模式的高度概括。
备忘录模式包括三种角色。
原发者——需要在某个时刻保存其状态的对象。原发者负责创建备忘录来保存自己的状态。当原发者需要恢复自身状态到某个时刻,它通过获得相应备忘录中的数据来还原。
备忘录——负责存储原发者状态的对象。
负责人——负责管理保存备忘录的对象,包括保存和获取备忘录对象。如果需要将备忘录对象持久化,负责人可以使用对象流将其写入文件。
四、案例现场
下面使用备忘录模式来设计一个GUI程序,主要功能如下:
程序通过一个画图板显示,当用户在画板上点击并且拖动后释放,就会在起点和终点之间形成一条直线。
程序提供清空操作,当用户点击鼠标中键的时候,画板全部内容被清空。
程序提供undo(撤销)操作,当用户点击鼠标右键的时候,最近一次操作被还原。
五、代码演示
原发者对象设计
本例中,原发者(画板)是JPanel的一个子类实例。该类主要包括创建备忘录状态及实现undo功能。DrawPanel类代码如下:
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JPanel;
public class DrawPanel extends JPanel{
private Color backColor = Color.white; //背景色
public DrawPanel() {
this.setBackground(backColor);
}
public Memento createMemento(Point<Integer> fromPos,Point<Integer> toPos){
Memento mem = new Memento(fromPos,toPos);
return mem;
}
/**
* 撤销上一步操作
*/
public void undo(Memento mem){
if(mem == null) return ;
Point<Integer> from = mem.getFrom();
Point<Integer> to = mem.getTo();
Graphics g = this.getGraphics();
g.setColor(backColor);
System.err.println("restore pos :" + mem);
//用画板背景色擦除
g.drawLine(from.getX(), from.getY(), to.getX(), to.getY());
}
public Color getBackgroundColor(){
return this.backColor;
}
}
备忘录对象设计
备忘录对象用于记录对象状态。在本例中,备忘录记录每一步操作的起点及终点位置。Memento类代码如下:
import java.text.MessageFormat;
public class Memento {
private Point<Integer> from;
private Point<Integer> to;
public Memento(Point<Integer> from, Point<Integer> to) {
super();
this.from = from;
this.to = to;
}
public Point<Integer> getFrom() {
return from;
}
public void setFrom(Point<Integer> from) {
this.from = from;
}
public Point<Integer> getTo() {
return to;
}
public void setTo(Point<Integer> to) {
this.to = to;
}
/**
* 格式化输出,便于追踪问题
*/
public String toString(){
String desc = "from ({0},{1}) to ({2},{3})";
return MessageFormat.format(desc, from.getX(),from.getY(),to.getX(),to.getY());
}
}
Memento类里面引用的状态对象Point表示一个位置点,Point类代码如下:
public class Point<T> {
private T x;
private T y;
public Point(T x, T y) {
super();
this.x = x;
this.y = y;
}
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
public T getY() {
return y;
}
public void setY(T y) {
this.y = y;
}
}
负责人对象设计
在本例中,负责人(Carataker)类使用一个栈的数据结构来保存用户每一步操作的顺序。当用户需要undo操作,从堆栈里弹出最近一次的备忘录给原发者,原发者再将当前状态进行还原。当堆栈为空时,用户不能进行undo操作。Carataker类代码如下:
import java.util.Stack;
public class Caretaker {
Stack<Memento> stack ;
public Caretaker(){
stack = new Stack<Memento>();
}
public Memento getMemento(){
if(stack.isEmpty()) return null;
Memento memento = stack.pop();
return memento;
}
public void saveMemento(Memento memento){
System.err.println("save pos :"+memento);
stack.push(memento);
}
}
应用程序入口(GUI界面展示),Application类代码如下:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
/**
* 简单画图板程序
*/
public class Application extends Frame implements MouseListener ,MouseMotionListener{
private DrawPanel drawPanel = new DrawPanel();//原发者
private Caretaker caretaker = new Caretaker();
private int mouseX = 0;
// 上一次 X 坐标位置
private int lastMouseX = 0;
// 鼠标 Y 坐标的位置
private int mouseY = 0;
// 上一次 Y 坐标位置
private int lastMouseY = 0;
// 画笔颜色
private Color penColor = Color.black;
public Application() {
// 设置标题栏文字
super("画图板");
drawPanel.addMouseListener(this);
drawPanel.addMouseMotionListener(this);
// 将画图板添加到窗体中
this.add(drawPanel);
// 添加窗口监听,点击关闭按钮时退出程序
this.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
// 设置窗口的大小
this.setSize(new Dimension(400, 430));
// 设置窗口位置,处于屏幕正中央
this.setLocationRelativeTo(null);
// 显示窗口
this.setVisible(true);
}
/**
* 重写 paint 绘图方法
*/
public void paint() {
Graphics g = this.drawPanel.getGraphics();
g.setColor(penColor);
g.drawLine(lastMouseX, lastMouseY, mouseX, mouseY);
}
public void mouseClicked(MouseEvent mouseEvent) {
int btnType = mouseEvent.getButton();
if(btnType == MouseEvent.BUTTON2){
//点击中键,清空图片
Graphics g = drawPanel.getGraphics();
g .setColor(drawPanel.getBackgroundColor());
g.clearRect(0,0,drawPanel.getWidth(),drawPanel.getHeight());
}else if(btnType == MouseEvent.BUTTON3){
//点击右键,撤销上一步操作
Memento mem = caretaker.getMemento();
//通过使用面板背景色画线来模拟直线擦除效果
drawPanel.undo(mem);
}
}
/**
* 鼠标按下
*/
public void mousePressed(MouseEvent mouseEvent) {
this.lastMouseX = this.mouseX = mouseEvent.getX();
this.lastMouseY = this.mouseY = mouseEvent.getY();
}
public void mouseReleased(MouseEvent mouseEvent) {
int btnType = mouseEvent.getButton();
if(btnType != MouseEvent.BUTTON1) return; //只捕捉鼠标左键
this.lastMouseX = this.mouseX;
this.lastMouseY = this.mouseY;
this.mouseX = mouseEvent.getX();
this.mouseY = mouseEvent.getY();
Point<Integer> from = new Point<Integer>(this.lastMouseX,this.lastMouseY);
Point<Integer> to = new Point<Integer>(this.mouseX,this.mouseY);
caretaker.saveMemento(drawPanel.createMemento(from, to));
paint() ;
}
public void mouseDragged(MouseEvent mouseEvent) {}
public void mouseMoved(MouseEvent mouseEvent) {}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
public static void main(String[] args) {
new Application();
}
}
六、运行截图
程序运行界面如下
依次点击鼠标右键,效果如下