23种设计模式之备忘录模式-Memento

2021/04/13

备忘录模式

亦称 快照Snapshot

1、意图

行为模式:这类模式负责对象间的高效沟通和职责委派。

备忘录模式是一种行为设计模式, 允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。

问题

假如你正在开发一款文字编辑器应用程序。 除了简单的文字编辑功能外, 编辑器中还要有设置文本格式和插入内嵌图片等功能。

后来, 你决定让用户能撤销施加在文本上的任何操作。 这项功能在过去几年里变得十分普遍, 因此用户期待任何程序都有这项功能。 你选择采用直接的方式来实现该功能: 程序在执行任何操作前会记录所有的对象状态, 并将其保存下来。 当用户此后需要撤销某个操作时, 程序将从历史记录中获取最近的快照, 然后使用它来恢复所有对象的状态。

解决方案

一些对象试图超出其职责范围的工作。 由于在执行某些行为时需要获取数据, 所以它们侵入了其他对象的私有空间, 而不是让这些对象来完成实际的工作。

备忘录模式将创建状态快照 (Snapshot) 的工作委派给实际状态的拥有者原发器 (Originator) 对象。 这样其他对象就不再需要从 “外部” 复制编辑器状态了, 编辑器类拥有其状态的完全访问权, 因此可以自行生成快照。

模式建议将对象状态的副本存储在一个名为备忘录 (Memento) 的特殊对象中。 除了创建备忘录的对象外, 任何对象都不能访问备忘录的内容。 其他对象必须使用受限接口与备忘录进行交互, 它们可以获取快照的元数据 (创建时间和操作名称等), 但不能获取快照中原始对象的状态。

这种限制策略允许你将备忘录保存在通常被称为负责人 (Caretakers) 的对象中。 由于负责人仅通过受限接口与备忘录互动, 故其无法修改存储在备忘录内部的状态。 同时, 原发器拥有对备忘录所有成员的访问权限, 从而能随时恢复其以前的状态。

在文字编辑器的示例中, 我们可以创建一个独立的历史 (History) 类作为负责人。 编辑器每次执行操作前, 存储在负责人中的备忘录栈都会生长。 你甚至可以在应用的 UI 中渲染该栈, 为用户显示之前的操作历史。

当用户触发撤销操作时, 历史类将从栈中取回最近的备忘录, 并将其传递给编辑器以请求进行回滚。 由于编辑器拥有对备忘录的完全访问权限, 因此它可以使用从备忘录中获取的数值来替换自身的状态。

模式结构

1、基于嵌套类的实现

适合java\C#\C++等于支持嵌套类的语言

1、原发器 (Originator) 类可以生成自身状态的快照, 也可以在需要时通过快照恢复自身状态。

2、备忘录 (Memento) 是原发器状态快照的值对象 (value object)。 通常做法是将备忘录设为不可变的, 并通过构造函数一次性传递数据。

3、负责人 (Caretaker) 仅知道 “何时” 和 “为何” 捕捉原发器的状态, 以及何时恢复状态。 负责人通过保存备忘录栈来记录原发器的历史状态。 当原发器需要回溯历史状态时, 负责人将从栈中获取最顶部的备忘录, 并将其传递给原发器的恢复 (restoration) 方法。

4、在该实现方法中, 备忘录类将被嵌套在原发器中。 这样原发器就可访问备忘录的成员变量和方法, 即使这些方法被声明为私有。 另一方面, 负责人对于备忘录的成员变量和方法的访问权限非常有限: 它们只能在栈中保存备忘录, 而不能修改其状态。

2、基于中间接口的实现

1、在没有嵌套类的情况下, 你可以规定负责人仅可通过明确声明的中间接口与备忘录互动, 该接口仅声明与备忘录元数据相关的方法, 限制其对备忘录成员变量的直接访问权限。

2、另一方面, 原发器可以直接与备忘录对象进行交互, 访问备忘录类中声明的成员变量和方法。 这种方式的缺点在于你需要将备忘录的所有成员变量声明为公有。

3、封装更加严格的实现

如果你不想让其他类有任何机会通过备忘录来访问原发器的状态, 那么还有另一种可用的实现方式

1、这种实现方式允许存在多种不同类型的原发器和备忘录。 每种原发器都和其相应的备忘录类进行交互。 原发器和备忘录都不会将其状态暴露给其他类。

2、负责人此时被明确禁止修改存储在备忘录中的状态。 但负责人类将独立于原发器, 因为此时恢复方法被定义在了备忘录类中。

3、每个备忘录将与创建了自身的原发器连接。 原发器会将自己及状态传递给备忘录的构造函数。 由于这些类之间的紧密联系, 只要原发器定义了合适的设置器 (setter), 备忘录就能恢复其状态。

伪代码

本例结合使用了命令模式与备忘录模式, 可保存复杂文字编辑器的状态快照, 并能在需要时从快照中恢复之前的状态。

保存文字编辑器状态的快照

命令 (command) 对象将作为负责人, 它们会在执行与命令相关的操作前获取编辑器的备忘录。 当用户试图撤销最近的命令时, 编辑器可以使用保存在命令中的备忘录来将自身回滚到之前的状态。

备忘录类没有声明任何公有的成员变量、 获取器 (getter) 和设置器, 因此没有对象可以修改其内容。 备忘录与创建自己的编辑器相连接, 这使得备忘录能够通过编辑器对象的设置器传递数据, 恢复与其相连接的编辑器的状态。 由于备忘录与特定的编辑器对象相连接, 程序可以使用中心化的撤销栈实现对多个独立编辑器窗口的支持。

// 原发器中包含了一些可能会随时间变化的重要数据。它还定义了在备忘录中保存
// 自身状态的方法,以及从备忘录中恢复状态的方法。
class Editor is
    private field text, curX, curY, selectionWidth

    method setText(text) is
        this.text = text

    method setCursor(x, y) is
        this.curX = curX
        this.curY = curY

    method setSelectionWidth(width) is
        this.selectionWidth = width

    // 在备忘录中保存当前的状态。
    method createSnapshot():Snapshot is
        // 备忘录是不可变的对象;因此原发器会将自身状态作为参数传递给备忘
        // 录的构造函数。
        return new Snapshot(this, text, curX, curY, selectionWidth)
  
// 备忘录类保存有编辑器的过往状态。
class Snapshot is
    private field editor: Editor
    private field text, curX, curY, selectionWidth

    constructor Snapshot(editor, text, curX, curY, selectionWidth) is
        this.editor = editor
        this.text = text
        this.curX = curX
        this.curY = curY
        this.selectionWidth = selectionWidth

    // 在某一时刻,编辑器之前的状态可以使用备忘录对象来恢复。
    method restore() is
        editor.setText(text)
        editor.setCursor(curX, curY)
        editor.setSelectionWidth(selectionWidth)
 
// 命令对象可作为负责人。在这种情况下,命令会在修改原发器状态之前获取一个
// 备忘录。当需要撤销时,它会从备忘录中恢复原发器的状态。
class Command is
    private field backup: Snapshot

    method makeBackup() is
        backup = editor.createSnapshot()

    method undo() is
        if (backup != null)
            backup.restore()      

适合应用场景

1、当你需要创建对象状态快照来恢复其之前的状态时, 可以使用备忘录模式。

备忘录模式允许你复制对象中的全部状态 (包括私有成员变量), 并将其独立于对象进行保存。 尽管大部分人因为 “撤销” 这个用例才记得该模式, 但其实它在处理事务 (比如需要在出现错误时回滚一个操作) 的过程中也必不可少。

2、当直接访问对象的成员变量、 获取器或设置器将导致封装被突破时, 可以使用该模式。

备忘录让对象自行负责创建其状态的快照。 任何其他对象都不能读取快照, 这有效地保障了数据的安全性。

实现方式

  1. 确定担任原发器角色的类。 重要的是明确程序使用的一个原发器中心对象, 还是多个较小的对象。

  2. 创建备忘录类。 逐一声明对应每个原发器成员变量的备忘录成员变量。

  3. 将备忘录类设为不可变。 备忘录只能通过构造函数一次性接收数据。 该类中不能包含设置器。

  4. 如果你所使用的编程语言支持嵌套类, 则可将备忘录嵌套在原发器中; 如果不支持, 那么你可从备忘录类中抽取一个空接口, 然后让其他所有对象通过接口来引用备忘录。 你可在该接口中添加一些元数据操作, 但不能暴露原发器的状态。

  5. 在原发器中添加一个创建备忘录的方法。 原发器必须通过备忘录构造函数的一个或多个实际参数来将自身状态传递给备忘录。

    该方法返回结果的类型必须是你在上一步中抽取的接口 (如果你已经抽取了)。 实际上, 创建备忘录的方法必须直接与备忘录类进行交互。

  6. 在原发器类中添加一个用于恢复自身状态的方法。 该方法接受备忘录对象作为参数。 如果你在之前的步骤中抽取了接口, 那么可将接口作为参数的类型。 在这种情况下, 你需要将输入对象强制转换为备忘录, 因为原发器需要拥有对该对象的完全访问权限。

  7. 无论负责人是命令对象、 历史记录或其他完全不同的东西, 它都必须要知道何时向原发器请求新的备忘录、 如何存储备忘录以及何时使用特定备忘录来对原发器进行恢复。

  8. 负责人与原发器之间的连接可以移动到备忘录类中。 在本例中, 每个备忘录都必须与创建自己的原发器相连接。 恢复方法也可以移动到备忘录类中, 但只有当备忘录类嵌套在原发器中, 或者原发器类提供了足够多的设置器并可对其状态进行重写时, 这种方式才能实现。

优缺点

优点

  • 你可以在不破坏对象封装情况的前提下创建对象状态快照。
  • 你可以通过让负责人维护原发器状态历史记录来简化原发器代码。

缺点

  • 如果客户端过于频繁地创建备忘录, 程序将消耗大量内存。
  • 负责人必须完整跟踪原发器的生命周期, 这样才能销毁弃用的备忘录。
  • 绝大部分动态编程语言 (例如 PHP、 Python 和 JavaScript) 不能确保备忘录中的状态不被修改。

与其他模式的关系

  • 你可以同时使用命令模式备忘录模式来实现 “撤销”。 在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。
  • 你可以同时使用备忘录迭代器模式来获取当前迭代器的状态, 并且在需要的时候进行回滚。
  • 有时候原型模式可以作为备忘录的一个简化版本, 其条件是你需要在历史记录中存储的对象的状态比较简单, 不需要链接其他外部资源, 或者链接可以方便地重建。

代码示例

在Java中使用

复杂度:3

流行度:1

所有 java.io.Serializable 的实现都可以模拟备忘录。

形状编辑器和复杂的撤销/恢复功能

该图像编辑器允许修改屏幕上形状的颜色和位置。 但任何修改都可被撤销和重复。

“撤销” 功能基于备忘录和命令模式的合作。 编辑器记录命令的执行历史。 在执行任何命令之前, 它都会生成备份并将其连接到一个命令对象。 而在执行完成后, 它会将已执行的命令放入历史记录中。

当用户请求撤销操作时, 编辑器将从历史记录中获取最近的命令, 恢复在该命令内部保存的状态备份。 如果用户再次请求撤销操作, 编辑器将恢复历史记录中的下一个命令, 以此类推。

被撤销的命令都将保存在历史记录中, 直至用户对屏幕上的形状进行了修改。 这对恢复被撤销的命令来说至关重要。

  • editor

    editor/Editor.java: 编辑器代码

    import javax.swing.*;
    import java.io.*;
    import java.util.Base64;
      
    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:画布代码

    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/History.java:保存命令和备忘录的历史记录

    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: 备忘录类

    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

    commands/Command.java: 基础命令类

    public interface Command {
        String getName();
        void execute();
    }
    

    commands/ColorCommand.java:修改已选形状的颜色

    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: 移动已选形状

    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:各种形状

    shapes/Shape.java

    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

    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

    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

    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

    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

    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: 初始化代码

    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)
                    )
            );
        }
    }
    
  • OutputDemo

Post Directory