在不破坏封装性的前提下,捕获一个对象的内部状态,并在对象以外保存这个状态。这样之后就可将该对象恢复到原先保存的状态java
假如你开发一款文字编辑器应用程序。除了简单的文字编辑功能外,编辑器还要有设置文本格式和插入内嵌图片的等功能。后来,决定添加一个让用户能撤销施加在文本上的任何操做。canvas
刚开始,打算用直接的方式实现该功能:程序在执行任何操做前会记录全部对象的状态,并将其保存。当须要撤销某个操做时,程序将从历史记录中得到最近的快照,而后使用它来恢复全部对象的状态。dom
可是,如何生成这些快照呢?先想到的是遍历对象的全部成员变量并将其数值复制保存。这须要对象自己没有严格的访问权限限制,可是,大多数对象会使用私有成员变量来存储重要数据,这样别人就没法轻易查看其中的内容。编辑器
假设全部对象都是public的,这种方式,仍存在其余的问题。将来,因为需求变化,可能会添加或删除一些成员变量。这须要对负责复制对象状态的类进行修改。ide
另外,为了让其余对象能保存或读取快照,极可能须要将快照的成员变量设为公有,而这将暴漏被复制对象的状态。其余类也会对快照类的每一个小改动产生依赖。咱们彷佛走进了一个死胡同:要么暴漏类的全部内部细节而使其过去脆弱;要么限制对其状态的访问权限而没法生成快照。this
咱们刚刚遇到的问题,都是封装破损形成的。一些对象试图超出其职责范围的工做。因为在执行某些行为时须要获取数据,因此它们侵入了其余对象的私有空间,而不是让这些对象来完成实际的工做spa
备忘录模式将建立状态快照(Snapshot)的工做委派给实际状态的拥有者原发器(Originator)对象。这样其余对象就再也不须要从 “外部” 复制编辑器状态了,编辑器类拥有其状态的彻底访问权,所以能够自行生成快照。模式建议将对象状态的副本存储在一个名为备忘录 (Memento) 的特殊对象中。 除了建立备忘录的对象外, 任何对象都不能访问备忘录的内容。 其余对象必须使用受限接口与备忘录进行交互, 它们能够获取快照的元数据 (建立时间和操做名称等), 但不能获取快照中原始对象的状态 设计
1) 能够在不破坏对象封装状况的前提下建立对象状态快照rest
2) 简化了原发器 在其余的保持封装性的设计中,Originator负责保持客户请求过的内部状态版本。这就把全部存储管理的责任交给了Originator。让客户管理请求的状态能够简化Originator,而且使得客户工做结束时无需通知原发器code
3) 使用备忘录可能代价很高 若是原生器在生成备忘录时必须拷贝并存储大量的信息,或者客户很是频繁地建立备忘录和恢复原发器的状态,可能致使很大的开销。除非封装和恢复Originator状态的开销不大。
-存储增量式改变 若是备忘录的建立及其返回的顺序是可预测的,备忘录能够仅存储原发器内部状态的增量改变
例如,一个包含可撤销命令的历史列表可以使用备忘录, 以保证命令被取消时他们能够恢复到正确的状态。历史列表定义了一个特定的顺序,按照这个顺序命令能够被撤销和重作。这意味着一个命令能够只存储一个命令所产生的增量改变而不是它所影响的每个对象的完整状态。
假设开发一个图形编辑器的撤销功能,其容许修改屏幕上形状的颜色和位置。但任何修改均可被撤销和重复。“撤销” 功能基于备忘录和命令模式的合做。编辑器记录命令的执行历史。在执行任何命令以前,都会生成备份并将其链接到一个命令对象。而在执行完成后,会将已执行的命令放入历史记录中。当用户请求撤销操做时,编辑器将从历史记录中获取最近的命令,恢复在该命令内部保存的状态备份。若是用户再次请求撤销操做,编辑器将恢复历史记录中的下一个命令,以此类推。被撤销的命令都将保存在历史记录中,直至用户对屏幕上的形状进行了修改。这对恢复被撤销的命令来讲相当重要
editor/Editor.java:编辑器代码
package memento.editor; import memento.history.History; import memento.history.Memento; import command.commands.Command; import composite.shapes.CompoundShape; import memento.shapes.Shape; import javax.swing.*; import java.io.*; import java.util.Base64; /** * @author GaoMing * @date 2021/7/25 - 20:47 */ public class Editor extends JComponent { private Canvas canvas; private CompoundShape allShapes = new CompoundShape(); private History history; public Editor() { canvas = new Canvas(this); history = new History(); } public void loadShapes(Shape... shapes) { allShapes.clear(); allShapes.add(shapes); canvas.refresh(); } public CompoundShape getShapes() { return allShapes; } public void execute(Command c) { history.push(c, new Memento(this)); c.execute(); } public void undo() { if (history.undo()) canvas.repaint(); } public void redo() { if (history.redo()) canvas.repaint(); } public String backup() { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(this.allShapes); oos.close(); return Base64.getEncoder().encodeToString(baos.toByteArray()); } catch (IOException e) { return ""; } } public void restore(String state) { try { byte[] data = Base64.getDecoder().decode(state); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data)); this.allShapes = (CompoundShape) ois.readObject(); ois.close(); } catch (ClassNotFoundException e) { System.out.print("ClassNotFoundException occurred."); } catch (IOException e) { System.out.print("IOException occurred."); } } }
editor/Canvas.java: 画布代码
package memento.editor; import memento.commands.ColorCommand; import memento.commands.MoveCommand; import memento.shapes.Shape; import javax.swing.*; import javax.swing.border.Border; import java.awt.*; import java.awt.event.*; import java.awt.image.BufferedImage; /** * @author GaoMing * @date 2021/7/25 - 20:47 */ public class Canvas extends java.awt.Canvas{ private Editor editor; private JFrame frame; private static final int PADDING = 10; Canvas(Editor editor) { this.editor = editor; createFrame(); attachKeyboardListeners(); attachMouseListeners(); refresh(); } private void createFrame() { frame = new JFrame(); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); JPanel contentPanel = new JPanel(); Border padding = BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING); contentPanel.setBorder(padding); contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); frame.setContentPane(contentPanel); contentPanel.add(new JLabel("Select and drag to move."), BorderLayout.PAGE_END); contentPanel.add(new JLabel("Right click to change color."), BorderLayout.PAGE_END); contentPanel.add(new JLabel("Undo: Ctrl+Z, Redo: Ctrl+R"), BorderLayout.PAGE_END); contentPanel.add(this); frame.setVisible(true); contentPanel.setBackground(Color.LIGHT_GRAY); } private void attachKeyboardListeners() { addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if ((e.getModifiers() & KeyEvent.CTRL_MASK) != 0) { switch (e.getKeyCode()) { case KeyEvent.VK_Z: editor.undo(); break; case KeyEvent.VK_R: editor.redo(); break; } } } }); } private void attachMouseListeners() { MouseAdapter colorizer = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (e.getButton() != MouseEvent.BUTTON3) { return; } Shape target = editor.getShapes().getChildAt(e.getX(), e.getY()); if (target != null) { editor.execute(new ColorCommand(editor, new Color((int) (Math.random() * 0x1000000)))); repaint(); } } }; addMouseListener(colorizer); MouseAdapter selector = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (e.getButton() != MouseEvent.BUTTON1) { return; } Shape target = editor.getShapes().getChildAt(e.getX(), e.getY()); boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) == ActionEvent.CTRL_MASK; if (target == null) { if (!ctrl) { editor.getShapes().unSelect(); } } else { if (ctrl) { if (target.isSelected()) { target.unSelect(); } else { target.select(); } } else { if (!target.isSelected()) { editor.getShapes().unSelect(); } target.select(); } } repaint(); } }; addMouseListener(selector); MouseAdapter dragger = new MouseAdapter() { MoveCommand moveCommand; @Override public void mouseDragged(MouseEvent e) { if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) != MouseEvent.BUTTON1_DOWN_MASK) { return; } if (moveCommand == null) { moveCommand = new MoveCommand(editor); moveCommand.start(e.getX(), e.getY()); } moveCommand.move(e.getX(), e.getY()); repaint(); } @Override public void mouseReleased(MouseEvent e) { if (e.getButton() != MouseEvent.BUTTON1 || moveCommand == null) { return; } moveCommand.stop(e.getX(), e.getY()); editor.execute(moveCommand); this.moveCommand = null; repaint(); } }; addMouseListener(dragger); addMouseMotionListener(dragger); } public int getWidth() { return editor.getShapes().getX() + editor.getShapes().getWidth() + PADDING; } public int getHeight() { return editor.getShapes().getY() + editor.getShapes().getHeight() + PADDING; } void refresh() { this.setSize(getWidth(), getHeight()); frame.pack(); } public void update(Graphics g) { paint(g); } public void paint(Graphics graphics) { BufferedImage buffer = new BufferedImage(this.getWidth(), this.getHeight(), BufferedImage.TYPE_INT_RGB); Graphics2D ig2 = buffer.createGraphics(); ig2.setBackground(Color.WHITE); ig2.clearRect(0, 0, this.getWidth(), this.getHeight()); editor.getShapes().paint(buffer.getGraphics()); graphics.drawImage(buffer, 0, 0, null); } }
history/History.java: 保存命令和备忘录的历史记录
package memento.history; import memento.commands.Command; import java.util.ArrayList; import java.util.List; /** * @author GaoMing * @date 2021/7/25 - 20:47 */ public class History { private List<Pair> history = new ArrayList<Pair>(); private int virtualSize = 0; private class Pair { Command command; Memento memento; Pair(Command c, Memento m) { command = c; memento = m; } private Command getCommand() { return command; } private Memento getMemento() { return memento; } } public void push(Command c, Memento m) { if (virtualSize != history.size() && virtualSize > 0) { history = history.subList(0, virtualSize - 1); } history.add(new Pair(c, m)); virtualSize = history.size(); } public boolean undo() { Pair pair = getUndo(); if (pair == null) { return false; } System.out.println("Undoing: " + pair.getCommand().getName()); pair.getMemento().restore(); return true; } public boolean redo() { Pair pair = getRedo(); if (pair == null) { return false; } System.out.println("Redoing: " + pair.getCommand().getName()); pair.getMemento().restore(); pair.getCommand().execute(); return true; } private Pair getUndo() { if (virtualSize == 0) { return null; } virtualSize = Math.max(0, virtualSize - 1); return history.get(virtualSize); } private Pair getRedo() { if (virtualSize == history.size()) { return null; } virtualSize = Math.min(history.size(), virtualSize + 1); return history.get(virtualSize - 1); } }
history/Memento.java:备忘录类
package memento.history; import memento.editor.Editor; /** * @author GaoMing * @date 2021/7/25 - 20:48 */ public class Memento { private String backup; private Editor editor; public Memento(Editor editor) { this.editor = editor; this.backup = editor.backup(); } public void restore() { editor.restore(backup); } }
commands/Command.java: 基础命令类
package memento.commands; /** * @author GaoMing * @date 2021/7/25 - 20:52 */ public interface Command { String getName(); void execute(); }
commands/ColorCommand.java: 修改已选形状的颜色
package memento.commands; import memento.editor.Editor; import memento.shapes.Shape; import java.awt.*; /** * @author GaoMing * @date 2021/7/25 - 20:52 */ public class ColorCommand implements Command{ private Editor editor; private Color color; public ColorCommand(Editor editor, Color color) { this.editor = editor; this.color = color; } @Override public String getName() { return "Colorize: " + color.toString(); } @Override public void execute() { for (Shape child : editor.getShapes().getSelected()) { child.setColor(color); } } }
commands/MoveCommand.java: 移动已选形状
package memento.commands; import memento.editor.Editor; import memento.shapes.Shape; /** * @author GaoMing * @date 2021/7/25 - 20:53 */ public class MoveCommand implements Command{ private Editor editor; private int startX, startY; private int endX, endY; public MoveCommand(Editor editor) { this.editor = editor; } @Override public String getName() { return "Move by X:" + (endX - startX) + " Y:" + (endY - startY); } public void start(int x, int y) { startX = x; startY = y; for (Shape child : editor.getShapes().getSelected()) { child.drag(); } } public void move(int x, int y) { for (Shape child : editor.getShapes().getSelected()) { child.moveTo(x - startX, y - startY); } } public void stop(int x, int y) { endX = x; endY = y; for (Shape child : editor.getShapes().getSelected()) { child.drop(); } } @Override public void execute() { for (Shape child : editor.getShapes().getSelected()) { child.moveBy(endX - startX, endY - startY); } } }
shapes/Shape.java
package memento.shapes; import java.awt.*; import java.io.Serializable; /** * @author GaoMing * @date 2021/7/25 - 20:55 */ public interface Shape extends Serializable { int getX(); int getY(); int getWidth(); int getHeight(); void drag(); void drop(); void moveTo(int x, int y); void moveBy(int x, int y); boolean isInsideBounds(int x, int y); Color getColor(); void setColor(Color color); void select(); void unSelect(); boolean isSelected(); void paint(Graphics graphics); }
shapes/BaseShape.java
package memento.shapes; import java.awt.*; /** * @author GaoMing * @date 2021/7/25 - 20:56 */ public abstract class BaseShape implements Shape{ int x, y; private int dx = 0, dy = 0; private Color color; private boolean selected = false; BaseShape(int x, int y, Color color) { this.x = x; this.y = y; this.color = color; } @Override public int getX() { return x; } @Override public int getY() { return y; } @Override public int getWidth() { return 0; } @Override public int getHeight() { return 0; } @Override public void drag() { dx = x; dy = y; } @Override public void moveTo(int x, int y) { this.x = dx + x; this.y = dy + y; } @Override public void moveBy(int x, int y) { this.x += x; this.y += y; } @Override public void drop() { this.x = dx; this.y = dy; } @Override public boolean isInsideBounds(int x, int y) { return x > getX() && x < (getX() + getWidth()) && y > getY() && y < (getY() + getHeight()); } @Override public Color getColor() { return color; } @Override public void setColor(Color color) { this.color = color; } @Override public void select() { selected = true; } @Override public void unSelect() { selected = false; } @Override public boolean isSelected() { return selected; } void enableSelectionStyle(Graphics graphics) { graphics.setColor(Color.LIGHT_GRAY); Graphics2D g2 = (Graphics2D) graphics; float dash1[] = {2.0f}; g2.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 2.0f, dash1, 0.0f)); } void disableSelectionStyle(Graphics graphics) { graphics.setColor(color); Graphics2D g2 = (Graphics2D) graphics; g2.setStroke(new BasicStroke()); } @Override public void paint(Graphics graphics) { if (isSelected()) { enableSelectionStyle(graphics); } else { disableSelectionStyle(graphics); } // ... } }
shapes/Circle.java
package memento.shapes; import java.awt.*; /** * @author GaoMing * @date 2021/7/25 - 20:57 */ public class Circle extends BaseShape{ private int radius; public Circle(int x, int y, int radius, Color color) { super(x, y, color); this.radius = radius; } @Override public int getWidth() { return radius * 2; } @Override public int getHeight() { return radius * 2; } @Override public void paint(Graphics graphics) { super.paint(graphics); graphics.drawOval(x, y, getWidth() - 1, getHeight() - 1); } }
shapes/Dot.java
package memento.shapes; import java.awt.*; /** * @author GaoMing * @date 2021/7/25 - 20:57 */ public class Dot extends BaseShape{ private final int DOT_SIZE = 3; public Dot(int x, int y, Color color) { super(x, y, color); } @Override public int getWidth() { return DOT_SIZE; } @Override public int getHeight() { return DOT_SIZE; } @Override public void paint(Graphics graphics) { super.paint(graphics); graphics.fillRect(x - 1, y - 1, getWidth(), getHeight()); } }
shapes/Rectangle.java
package memento.shapes; import java.awt.*; /** * @author GaoMing * @date 2021/7/25 - 20:58 */ public class Rectangle extends BaseShape{ private int width; private int height; public Rectangle(int x, int y, int width, int height, Color color) { super(x, y, color); this.width = width; this.height = height; } @Override public int getWidth() { return width; } @Override public int getHeight() { return height; } @Override public void paint(Graphics graphics) { super.paint(graphics); graphics.drawRect(x, y, getWidth() - 1, getHeight() - 1); } }
shapes/CompoundShape.java
package memento.shapes; import java.awt.*; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * @author GaoMing * @date 2021/7/25 - 20:59 */ public class CompoundShape extends BaseShape{ private List<Shape> children = new ArrayList<>(); public CompoundShape(Shape... components) { super(0, 0, Color.BLACK); add(components); } public void add(Shape component) { children.add(component); } public void add(Shape... components) { children.addAll(Arrays.asList(components)); } public void remove(Shape child) { children.remove(child); } public void remove(Shape... components) { children.removeAll(Arrays.asList(components)); } public void clear() { children.clear(); } @Override public int getX() { if (children.size() == 0) { return 0; } int x = children.get(0).getX(); for (Shape child : children) { if (child.getX() < x) { x = child.getX(); } } return x; } @Override public int getY() { if (children.size() == 0) { return 0; } int y = children.get(0).getY(); for (Shape child : children) { if (child.getY() < y) { y = child.getY(); } } return y; } @Override public int getWidth() { int maxWidth = 0; int x = getX(); for (Shape child : children) { int childsRelativeX = child.getX() - x; int childWidth = childsRelativeX + child.getWidth(); if (childWidth > maxWidth) { maxWidth = childWidth; } } return maxWidth; } @Override public int getHeight() { int maxHeight = 0; int y = getY(); for (Shape child : children) { int childsRelativeY = child.getY() - y; int childHeight = childsRelativeY + child.getHeight(); if (childHeight > maxHeight) { maxHeight = childHeight; } } return maxHeight; } @Override public void drag() { for (Shape child : children) { child.drag(); } } @Override public void drop() { for (Shape child : children) { child.drop(); } } @Override public void moveTo(int x, int y) { for (Shape child : children) { child.moveTo(x, y); } } @Override public void moveBy(int x, int y) { for (Shape child : children) { child.moveBy(x, y); } } @Override public boolean isInsideBounds(int x, int y) { for (Shape child : children) { if (child.isInsideBounds(x, y)) { return true; } } return false; } @Override public void setColor(Color color) { super.setColor(color); for (Shape child : children) { child.setColor(color); } } @Override public void unSelect() { super.unSelect(); for (Shape child : children) { child.unSelect(); } } public Shape getChildAt(int x, int y) { for (Shape child : children) { if (child.isInsideBounds(x, y)) { return child; } } return null; } public boolean selectChildAt(int x, int y) { Shape child = getChildAt(x,y); if (child != null) { child.select(); return true; } return false; } public List<Shape> getSelected() { List<Shape> selected = new ArrayList<>(); for (Shape child : children) { if (child.isSelected()) { selected.add(child); } } return selected; } @Override public void paint(Graphics graphics) { if (isSelected()) { enableSelectionStyle(graphics); graphics.drawRect(getX() - 1, getY() - 1, getWidth() + 1, getHeight() + 1); disableSelectionStyle(graphics); } for (Shape child : children) { child.paint(graphics); } } }
Demo.java: 初始化代码
package memento; import memento.editor.Editor; import memento.shapes.Circle; import memento.shapes.CompoundShape; import memento.shapes.Dot; import memento.shapes.Rectangle; import java.awt.*; /** * @author GaoMing * @date 2021/7/25 - 20:47 */ public class Demo { public static void main(String[] args) { Editor editor = new Editor(); editor.loadShapes( new Circle(10, 10, 10, Color.BLUE), new CompoundShape( new Circle(110, 110, 50, Color.RED), new Dot(160, 160, Color.RED) ), new CompoundShape( new Rectangle(250, 250, 100, 100, Color.GREEN), new Dot(240, 240, Color.GREEN), new Dot(240, 360, Color.GREEN), new Dot(360, 360, Color.GREEN), new Dot(360, 240, Color.GREEN) ) ); } }
运行结果
使用示例:备忘录的基本原则可经过序列化来实现,这在Java语言中很常见。尽管备忘录不是生成对象状态快照的惟一或最有效方法,但它能在保护原始对象的结构不暴露给其余对象的状况下保存对象状态的备份
下面是核心 Java 程序库中该模式的一些示例:
全部 java.io.Serializable 的实现均可以模拟备忘录 全部 javax.faces.component.StateHolder 的实现