Сохранение состояния сложного объекта с помощью шаблона Memento (и команды)

StackOverflow https://stackoverflow.com/questions/211034

Вопрос

Я работаю над небольшим проектом редактора UML на Java, который я начал пару месяцев назад.Через несколько недель я получил рабочую копию для редактора диаграмм классов UML.

Но теперь я полностью переделываю его для поддержки других типов диаграмм, таких как последовательность, состояние, класс и т.д.Это делается путем реализации фреймворка построения графиков (я очень вдохновлен работой Кэй Хорстманн по этому вопросу с редактором Violet UML).

Редизайн шел гладко, пока один из моих друзей не сказал мне, что я забыл добавить функциональность Do / Undo в проект, что, на мой взгляд, жизненно важно.

Вспоминая курсы объектно-ориентированного проектирования, я сразу подумал о Memento и Command pattern.

Вот в чем дело.У меня есть абстрактный класс AbstractDiagram, который содержит два ArrayLists :один для хранения узлов (называемых Элементами в моем проекте), а другой для хранения ребер (называемых ссылками в моих проектах).На диаграмме, вероятно, будет сохранен набор команд, которые можно отменить / Повторить.Довольно стандартно.

Как я могу эффективно выполнить эти команды?Скажем, например, что я хочу переместить узел (узлом будет тип интерфейса с именем INode, и будут конкретные узлы, производные от него (ClassNode, InterfaceNode, NoteNode и т.д.)).

Информация о местоположении хранится как атрибут в узле, поэтому, изменяя этот атрибут в самом узле, изменяется состояние.Когда отображение обновится, узел будет перемещен.Это памятная часть шаблона (я думаю), с той разницей, что объектом является само состояние.

Более того, если я сохраню клон исходного узла (до его перемещения), я смогу вернуться к его старой версии.Тот же метод применяется к информации, содержащейся в узле (имя класса или интерфейса, текст для узла примечания, имя атрибутов и так далее).

Дело в том, как мне заменить на диаграмме узел его клоном при отмене / повторении операции?Если я клонирую исходный объект, на который ссылается диаграмма (находящийся в списке узлов), клон не является ссылкой на диаграмме, и единственное, на что указывает сама команда!Должен ли я включить в диаграмму механизмы для поиска узла по идентификатору (например), чтобы я мог заменить на диаграмме узел его клоном (и наоборот)?Зависит ли это от шаблонов Memento и Command , чтобы сделать это ?А как насчет ссылок?Они тоже должны быть подвижными, но я не хочу создавать команду только для ссылок (и одну только для узлов), и я должен иметь возможность изменять правильный список (узлы или ссылки) в соответствии с типом объекта, на который ссылается команда.

Как бы вы поступили дальше?Короче говоря, у меня возникли проблемы с представлением состояния объекта в шаблоне command / memento, чтобы его можно было эффективно восстановить и исходный объект был восстановлен в списке диаграмм, и в зависимости от типа объекта (узел или ссылка).

Большое спасибо!

Гийом.

P.S.:если я не совсем ясно выразился, скажите мне, и я уточню свое сообщение (как всегда!).

Редактировать

Вот мое реальное решение, которое я начал внедрять перед публикацией этого вопроса.

Во-первых, у меня есть класс AbstractCommand, определенный следующим образом :

public abstract class AbstractCommand {
    public boolean blnComplete;

    public void setComplete(boolean complete) {
        this.blnComplete = complete;
    }

    public boolean isComplete() {
        return this.blnComplete;
    }

    public abstract void execute();
    public abstract void unexecute();
}

Затем каждый тип команды реализуется с использованием конкретного вывода AbstractCommand.

Итак, у меня есть команда для перемещения объекта :

public class MoveCommand extends AbstractCommand {
    Moveable movingObject;
    Point2D startPos;
    Point2D endPos;

    public MoveCommand(Point2D start) {
        this.startPos = start;
    }

    public void execute() {
        if(this.movingObject != null && this.endPos != null)
            this.movingObject.moveTo(this.endPos);
    }

    public void unexecute() {
        if(this.movingObject != null && this.startPos != null)
            this.movingObject.moveTo(this.startPos);
    }

    public void setStart(Point2D start) {
        this.startPos = start;
    }

    public void setEnd(Point2D end) {
        this.endPos = end;
    }
}

У меня также есть MoveRemoveCommand (to...переместить или удалить объект/узел).Если я использую идентификатор метода instanceof, мне не нужно передавать диаграмму фактическому узлу или ссылке, чтобы он мог удалить себя с диаграммы (что, по-моему, плохая идея).

Диаграмма абстрактной диограммы;Добавляемый объект;AddRemoveType тип;

@SuppressWarnings("unused")
private AddRemoveCommand() {}

public AddRemoveCommand(AbstractDiagram diagram, Addable obj, AddRemoveType type) {
    this.diagram = diagram;
    this.obj = obj;
    this.type = type;
}

public void execute() {
    if(obj != null && diagram != null) {
        switch(type) {
            case ADD:
                this.obj.addToDiagram(diagram);
                break;
            case REMOVE:
                this.obj.removeFromDiagram(diagram);
                break;
        }
    }
}

public void unexecute() {
    if(obj != null && diagram != null) {
        switch(type) {
            case ADD:
                this.obj.removeFromDiagram(diagram);
                break;
            case REMOVE:
                this.obj.addToDiagram(diagram);
                break;
        }
    }
}

Наконец, у меня есть ModificationCommand, которая используется для изменения информации узла или ссылки (имя класса и т.д.).В будущем это может быть объединено с MoveCommand.На данный момент этот класс пуст.Вероятно, я буду использовать идентификатор с механизмом, чтобы определить, является ли измененный объект узлом или ребром (через instanceof или специальное обозначение в идентификаторе).

Является ли это хорошим решением?

Это было полезно?

Решение

Я думаю, вам просто нужно разложить вашу проблему на более мелкие.

Первая проблема:Q:Как представить шаги в вашем приложении с помощью шаблона memento / command?Во-первых, я понятия не имею, как именно работает ваше приложение, но, надеюсь, вы поймете, к чему я клоню.Допустим, я хочу разместить ClassNode на диаграмме со следующими свойствами

{ width:100, height:50, position:(10,25), content:"Am I certain?", edge-connections:null}

Это было бы обернуто как командный объект.Допустим, это относится к DiagramController.Тогда ответственность контроллера диаграммы может заключаться в записи этой команды (я бы поставил на то, чтобы поместить ее в стек) и передаче команды, например, DiagramBuilder.DiagramBuilder фактически будет отвечать за обновление отображения.

DiagramController
{
  public DiagramController(diagramBuilder:DiagramBuilder)
  {
    this._diagramBuilder = diagramBuilder;
    this._commandStack = new Stack();
  }

  public void Add(node:ConditionalNode)
  {
    this._commandStack.push(node);
    this._diagramBuilder.Draw(node);
  }

  public void Undo()
  {
    var node = this._commandStack.pop();
    this._diagramBuilderUndraw(node);
  }
}

Что-то подобное должно это сделать, и, конечно, нужно будет уладить множество деталей.Кстати, чем большим количеством свойств обладают ваши узлы, тем более детальной должна быть отрисовка.

Хорошей идеей может быть использование идентификатора для привязки команды в вашем стеке к нарисованному элементу.Это может выглядеть примерно так:

DiagramController
{
  public DiagramController(diagramBuilder:DiagramBuilder)
  {
    this._diagramBuilder = diagramBuilder;
    this._commandStack = new Stack();
  }

  public void Add(node:ConditionalNode)
  {
    string graphicalRefId = this._diagramBuilder.Draw(node);
    var nodePair = new KeyValuePair<string, ConditionalNode> (graphicalRefId, node);
    this._commandStack.push(nodePair);
  }

  public void Undo()
  {
    var nodePair = this._commandStack.pop();
    this._diagramBuilderUndraw(nodePair.Key);
  }
} 

На данный момент вы не совсем должны иметь объект, поскольку у вас есть идентификатор, но это будет полезно, если вы решите также реализовать функцию повтора.Хорошим способом сгенерировать идентификатор для ваших узлов было бы реализовать для них метод hashcode, за исключением того факта, что вам не будет гарантировано, что вы не будете дублировать свои узлы таким образом, чтобы хэш-код был идентичным.

Следующая часть проблемы находится в вашем DiagramBuilder, потому что вы пытаетесь понять, как, черт возьми, работать с этими командами.Для этого все, что я могу сказать, - это просто убедиться, что вы можете создать обратное действие для каждого типа компонента, который вы можете добавить.Чтобы обработать удаление ссылок, вы можете посмотреть на свойство пограничного соединения (я думаю, ссылки в вашем коде) и уведомить каждое из пограничных соединений о том, что они должны отключиться от определенного узла.Я бы предположил, что при отключении они могли бы соответствующим образом перерисовать себя.

Чтобы как бы подвести итог, я бы рекомендовал не сохранять ссылку на ваши узлы в стеке, а вместо этого просто своего рода токен, который представляет состояние данного узла в этот момент.Это позволит вам представлять один и тот же узел в вашем стеке отмены в нескольких местах, не ссылаясь при этом на один и тот же объект.

Напишите, если у вас есть вопросы.Это сложный вопрос.

Другие советы

По моему скромному мнению, вы думаете об этом более сложным образом, чем это есть на самом деле.Для того чтобы вернуться к предыдущему состоянию, клонирование всего узла вообще не требуется.Скорее, каждый ** Командный класс будет иметь -

  1. ссылка на узел, на который он воздействует,
  2. объект memento (имеющий переменные состояния, достаточные только для того, чтобы узел мог вернуться к ним)
  3. метод execute()
  4. метод undo().

Поскольку классы команд имеют ссылку на узел, нам не нужен механизм идентификаторов для ссылки на объекты на диаграмме.

В примере из вашего вопроса мы хотим переместить узел в новую позицию.Для этого у нас есть класс NodePositionChangeCommand.

public class NodePositionChangeCommand {
    // This command will act upon this node
    private Node node;

    // Old state is stored here
    private NodePositionMemento previousNodePosition;

    NodePositionChangeCommand(Node node) {
        this.node = node;
    }

    public void execute(NodePositionMemento newPosition) {
        // Save current state in memento object previousNodePosition

        // Act upon this.node
    }

    public void undo() {
        // Update this.node object with values from this.previousNodePosition
    }
}

А как насчет ссылок?Они тоже должны быть подвижными, но я не хочу создавать команду только для ссылок (и одну только для узлов).

Я читал в книге GoF (в обсуждении шаблона memento), что перемещение ссылки с изменением положения узлов обрабатывается каким-то решателем ограничений.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top