在不破壞封裝性的前提下,捕獲一個對象的內部狀態,並在對象以外保存這個狀態。這樣之後就可將該對象恢復到原先保存的狀態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 的實現