1. 意圖
在不破壞封裝性的前提下,捕獲一個物件的內部狀態,并在物件之外保存這個狀態,這樣以后就可將該物件恢復到原先保存的狀態
2. 動機
假如你開發一款文字編輯器應用程式,除了簡單的文字編輯功能外,編輯器還要有設定文本格式和插入內嵌圖片的等功能,后來,決定添加一個讓用戶能撤銷施加在文本上的任何操作,
剛開始,打算用直接的方式實作該功能:程式在執行任何操作前會記錄所有物件的狀態,并將其保存,當需要撤銷某個操作時,程式將從歷史記錄中獲得最近的快照,然后使用它來恢復所有物件的狀態,
但是,如何生成這些快照呢?先想到的是遍歷物件的所有成員變數并將其數值復制保存,這需要物件本身沒有嚴格的訪問權限限制,但是,大多數物件會使用私有成員變數來存盤重要資料,這樣別人就無法輕易查看其中的內容,
假設所有物件都是public的,這種方式,仍存在其他的問題,未來,由于需求變化,可能會添加或洗掉一些成員變數,這需要對負責復制物件狀態的類進行修改,
另外,為了讓其他物件能保存或讀取快照,很可能需要將快照的成員變數設為公有,而這將暴漏被復制物件的狀態,其他類也會對快照類的每個小改動產生依賴,我們似乎走進了一個死胡同:要么暴漏類的所有內部細節而使其過去脆弱;要么限制對其狀態的訪問權限而無法生成快照,
我們剛剛遇到的問題,都是封裝破損造成的,一些物件試圖超出其職責范圍的作業,由于在執行某些行為時需要獲取資料,所以它們侵入了其他物件的私有空間,而不是讓這些物件來完成實際的作業
備忘錄模式將創建狀態快照(Snapshot)的作業委派給實際狀態的擁有者原發器(Originator)物件,這樣其他物件就不再需要從 “外部” 復制編輯器狀態了,編輯器類擁有其狀態的完全訪問權,因此可以自行生成快照,模式建議將物件狀態的副本存盤在一個名為備忘錄 (Memento) 的特殊物件中, 除了創建備忘錄的物件外, 任何物件都不能訪問備忘錄的內容, 其他物件必須使用受限介面與備忘錄進行互動, 它們可以獲取快照的元資料 (創建時間和操作名稱等), 但不能獲取快照中原始物件的狀態
3. 適用性
- 必須保存一個物件在某個時刻的(部分)狀態,這樣以后需要時他才能恢復到先前的狀態
- 如果一個介面讓其他物件直接得到這些狀態,將會暴露物件的實作細節并破壞物件的封裝性
4. 結構
5. 效果
1) 可以在不破壞物件封裝情況的前提下創建物件狀態快照
2) 簡化了原發器 在其他的保持封裝性的設計中,Originator負責保持客戶請求過的內部狀態版本,這就把所有存盤管理的責任交給了Originator,讓客戶管理請求的狀態可以簡化Originator,并且使得客戶作業結束時無需通知原發器
3) 使用備忘錄可能代價很高 如果原生器在生成備忘錄時必須拷貝并存盤大量的資訊,或者客戶非常頻繁地創建備忘錄和恢復原發器的狀態,可能導致很大的開銷,除非封裝和恢復Originator狀態的開銷不大,
-存盤增量式改變 如果備忘錄的創建及其回傳的順序是可預測的,備忘錄可以僅存盤原發器內部狀態的增量改變
例如,一個包含可撤銷命令的歷史串列可使用備忘錄, 以保證命令被取消時他們可以恢復到正確的狀態,歷史串列定義了一個特定的順序,按照這個順序命令可以被撤銷和重做,這意味著一個命令可以只存盤一個命令所產生的增量改變而不是它所影響的每一個物件的完整狀態,
6. 代碼實作
假設開發一個圖形編輯器的撤銷功能,其允許修改螢屏上形狀的顏色和位置,但任何修改都可被撤銷和重復,“撤銷” 功能基于備忘錄和命令模式的合作,編輯器記錄命令的執行歷史,在執行任何命令之前,都會生成備份并將其連接到一個命令物件,而在執行完成后,會將已執行的命令放入歷史記錄中,當用戶請求撤銷操作時,編輯器將從歷史記錄中獲取最近的命令,恢復在該命令內部保存的狀態備份,如果用戶再次請求撤銷操作,編輯器將恢復歷史記錄中的下一個命令,以此類推,被撤銷的命令都將保存在歷史記錄中,直至用戶對螢屏上的形狀進行了修改,這對恢復被撤銷的命令來說至關重要
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 =https://www.cnblogs.com/muxianbai/p/ 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); } }View Code
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); } // ... } }View Code
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); } } }View Code
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) ) ); } }
運行結果
7. 與其他模式的關系
- 可以同時使用命令模式和備忘錄模式來實作 “撤銷”,在這種情況下,命令用于對目標物件執行各種不同的操作,備忘錄用來保存一條命令執行前該物件的狀態
- 可以同時使用備忘錄和迭代器模式來獲取當前迭代器的狀態,并且在需要的時候進行回滾
- 有時候原型模式可以作為備忘錄的一個簡化版本,其條件是你需要在歷史記錄中存盤的物件的狀態比較簡單,不需要鏈接其他外部資源,或者鏈接可以方便地重建
8. 已知應用
使用示例:備忘錄的基本原則可通過序列化來實作,這在Java語言中很常見,盡管備忘錄不是生成物件狀態快照的唯一或最有效方法,但它能在保護原始物件的結構不暴露給其他物件的情況下保存物件狀態的備份
下面是核心 Java 程式庫中該模式的一些示例:
所有 java.io.Serializable 的實作都可以模擬備忘錄
所有 javax.faces.component.StateHolder 的實作
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/308449.html
標籤:設計模式
上一篇:命令模式(學習筆記)
下一篇:模板方法(學習筆記)
