设计模式给人的第一感觉,大概就是抽象,还有遥不可及。其实随着开发经验的逐渐积累,当你偶尔翻一下关于设计模式之类的书籍,你就会发现,里面的某些模式自己曾经实现过,只是尚未上升到理论的阶段。
设计模式,是针对某一类问题的最佳解决方案。可以这样定义设计模式:“设计模式是从许多优秀的软件系统中总结出来的可复用的设计方案”。在这里,不打算讨论全部设计模式的定义和使用。只是希望抛砖引玉,通过一个图形界面,让读者对于设计模式之一的“备忘录模式”有一个直观的感受。
在说明备忘录模式之前,先抛出一个问题。玩过单机游戏的读者,应该经常使用到游戏的保存功能。单机等游戏能够随时将当前的进度保存起来,以便在以后的某个时刻读取进度,这样的游戏可以满足玩家长时间的体验。那么,如何使用设计模式来优雅地解决这样的问题呢?希望看完全文后的读者能够对解决这个问题有个初步的感知。
一直以来,设计模式由于本身的抽象性和复杂性,容易让初学者望而却步。因此本方所用的例子,将抛开纯理论分析,以一个画图板的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); } }
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(); } }
依次点击鼠标右键,效果如下