1

In a pretty big project, I have a hierarchy of objects, let's say:

Zone object, which contains Block objects and Connection objects.

Mainliy, the Connection object links together several Block objects, all inside a Zone.

In this project, I am using the Memento pattern (together with the Command Pattern) for implementing Undo/Redo functionallity.

Also, I am reusing the "Memento" implementation to implement also Save/Load functionallity, so the Undo/Redo propertis are always in sync with what is being Saved/Loaded.

Something among the lines:

    //Base class for Zone, Connection and Block
    class Object
    {
        virtual void save(IWriter *writer) = 0;
        virtual void load(IReader *reader) = 0;
    }

    //Base classes:
    class IWriter
    {
        virtual void writeInt(int x) = 0;
    }
    class IReader
    {
        virtual int readInt() = 0;
    }

    //The Memento just saves / reads data from a memory map
    class Memento: public IWriter, public IReader
    {
    
    }

    //These classes are used to save / load from file
    class XmlWriter: public IWriter
    {
    }
    class XmlReader: public IReader
    {
    }

Now, it is easy to do the Undo / Redo command for deleting a zone:

    class DeleteZone: public QUndoCommand
    {
        void redo()
        {
            zone->save(&data);
            //NOTE: zone will save its own data, and also all its children data, in order
            //to restore completely the previous state.
            delete zone;
        }
        void undo()
        {
           Zone *zone=new Zone();
           zone->load(&data);
        }
    private:
        Memento data;
    }

And it is also easy to Save / Load the entire Zone:

    XmlWriter writer("path to file");
    zone->save(&writer);

So, using this patterns, the UI does not need to know anything about the internal logic of the model. It just saves / loads the data and it is done.

Now I wanted to repeat this pattern also for the Connection object, which seemed similar.

Here is the problem:

When a connection is created, it should perform some operations on the related Block objects. Let's call them connectionCreated()

This operations depend on the connections each Block has, so there are two different situations:

  • When only a connection is being created (like in a CreateConnectionCommand), then connectionCreated() should be called immediately.

In this case, to prevent the UI to know anything about the internal logic, I was thinking to add the connectionCreated() to the Connection constructor.

  • But when loading the entire Zone, connectionCreated() should be delayed, until all the connections have been loaded.

So it is not valid the solution of adding it to the constructor. Here it should be added to the load() function of the Zone, which is fine, BUT it should be also added to the CreateConnectionCommand, which is part of the UI and thus it seems not fine.

AND also, the Memento object should save the Connection state, together with the related objects for the case when a connection is being created, but not when the zone is created (since this will be handled by Zone::load()), again leaking the model details to the UI undo/redo functionallity.

Is there some better pattern / approach to this situation?


Note 1:

connectionCreated() does some operations to update variables inside Block to ensure consistency, and it also creates some required objects which will be visible to the user.

Furthermore, to ensure consistency, it may create other connections, which in the case of Zone::load may still to be created, thus duplicating objects.

This is why in the situation of Zone::load, this operations should be delayed until all the connections are created, but not in the case of CreateConnectionCommand, where only one NEW connection is being created.

2
  • When you say that connectionCreated() must be called, is it because of some observable result for the user? Or is is an internal operation within your model to ensure some consistency involving more than one object?
    – Christophe
    Commented Jun 20, 2021 at 11:51
  • @Christophe both, the connectionCreated does some operations to update variables inside Block and ensure consistency, and may also create some required objects which will be visible to the user. The main problem is that connectionCreated, to ensure consistency, may create other connections, which in the Zone::load case are still to be created, thus duplicating objects. See Edit 1
    – LoPiTaL
    Commented Jun 20, 2021 at 11:54

1 Answer 1

1

The memento's aim is:

Without violating encapsulation, capture an externalize an object's internal state, so that the object can be restored to this state later.

But the save/load and undo/redo functions in an application needs to save and restore the state of the whole model and not its individual parts.

In some application domains, the saving the whole just consists of saving the parts. in this case, the use of the memento as you described it is perfect.

But in more complex models, with intertwined objects, this may not be sufficient. This is what happens in your case: when restoring state of single objects, you need additional logic, such as connectionCreated() to recreate a reality that was not saved. More concretely the issue is the part of the state of individual objects that is interdependent with other objects. And this seems risky, for example:

  • the order of connectionCreated() could matter, especially if it may create additional objects in some cases.
  • the connectionCreated() implementation could be designed with the assumption of forward moving in time (e.g. use count increment in objects that were not restored), and not support well incremental backward application.

Of course, being very careful, you could engineer this to work. To address your grouped connectionCreated() issue, you may for example consider not to call it directly from the connection, but to record the need to call it like a command in a queue, that would be executed when all other commands were performed.

But a safer option seems to be to save not individual objects, but the full model. Unfortunately this can grow to become a very lare set of data. A variant could be as you did to save just the changed objects. But instead of the memento of individual objects, you would save a larger memento composed of all the mementos of objects that are affected by the changed object (i.e. a group of consistent objects)

1
  • 1
    Thanks for the answer. I already thought about both options. The "command in a queue" one seemed a bit fragile (this is the one I am currently implementing), while the other one seemed that it would grow in complexity and dependencies would leak into the Memento and the Commands, so none of them were satisfying me. I thought there would be some standard pattern for such a problem, better suited than Memento or Commnads. I will wait for other answers, and if none, accept yours as the best
    – LoPiTaL
    Commented Jun 20, 2021 at 13:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.