Memorandum mode: introduce related concepts and implement a more comprehensive Undo Manager class library.
Memento Pattern
motivation
Memo mode is also a behavioral design mode. It is most important when used in Ctrl-Z or Undo/Redo places, and here is also the best place to apply it. In addition, sometimes we can also call it the archive mode. You can generalize it to all backup, archive, and snapshot scenarios, such as the Time Machine of macOS.
The reason why Memento can become a pattern is that it has abstracted and concealed the above scenes. When discussing the memo pattern here, we must pay attention to the most powerful ability it provides as a design pattern: it is not the ability to Undo/Redo, but the ability to cover up details.
Of course, take the Undo/Redo scene of the text editor as an example to illustrate this point:
Memento mode will conceal the implementation details of the editor's editing commands, such as editing position, keystroke events, modified text content, etc., just package them into an editing record and provide it to the outside as a whole. The external user does not need to know the so-called implementation details. It only needs to issue an Undo command to extract and roll back an editing record from the editing history to complete the Undo action.
This is what the ideal Memento mode should achieve.
Lightweight classical definition
The word processor design mentioned above is a fuller case. In fact, the definitions of most classical Memento patterns such as GoF are relatively lightweight, and they usually involve three objects:
- originator: The usually refers to the object that has a state snapshot, and the state snapshot is created by the founder in order to restore it from the memo in the future.
- Memento: memo state snapshot storage, in general, this is a POJO objects.
- caretaker: The person in charge object is responsible for tracking multiple memento objects.
Its relationship diagram is like this:
FROM: Here
A slightly adjusted C++ implementation looks like this:
namespace dp { namespace undo { namespace basic {
template<typename State>
class memento_t {
public:
~memento_t() = default;
void push(State &&s) {
_saved_states.emplace_back(s);
dbg_print(" . save memento state : %s", undo_cxx::to_string(s).c_str());
}
std::optional<State> pop() {
std::optional<State> ret;
if (_saved_states.empty()) {
return ret;
}
ret.emplace(_saved_states.back());
_saved_states.pop_back();
dbg_print(" . restore memento state : %s", undo_cxx::to_string(*ret).c_str());
return ret;
}
auto size() const { return _saved_states.size(); }
bool empty() const { return _saved_states.empty(); }
bool can_pop() const { return !empty(); }
private:
std::list<State> _saved_states;
};
template<typename State, typename Memento = memento_t<State>>
class originator_t {
public:
originator_t() = default;
~originator_t() = default;
void set(State &&s) {
_state = std::move(s);
dbg_print("originator_t: set state (%s)", undo_cxx::to_string(_state).c_str());
}
void save_to_memento() {
dbg_print("originator_t: save state (%s) to memento", undo_cxx::to_string(_state).c_str());
_history.push(std::move(_state));
}
void restore_from_memento() {
_state = *_history.pop();
dbg_print("originator_t: restore state (%s) from memento", undo_cxx::to_string(_state).c_str());
}
private:
State _state;
Memento _history;
};
template<typename State>
class caretaker {
public:
caretaker() = default;
~caretaker() = default;
void run() {
originator_t<State> o;
o.set("state1");
o.set("state2");
o.save_to_memento();
o.set("state3");
o.save_to_memento();
o.set("state4");
o.restore_from_memento();
}
};
}}} // namespace dp::undo::basic
void test_undo_basic() {
using namespace dp::undo::basic;
caretaker<std::string> c;
c.run();
}
int main() {
test_undo_basic();
return 0;
}
This implementation code simplifies the responsibilities of the person in charge, and delegates quite a lot of tasks to others to complete. The purpose is to make the user's coding easier. Users only need to operate originator_t<State>
caretaker
to complete the use of memento mode.
Application scenario
Simple scenes can directly reuse the above memento_t<State>
and originator_t<State>
templates. Although they are simple, they are enough to cope with general scenes.
Looking at the memento mode in an abstract way, at the beginning we mentioned that Memento mode can be applied to all backup, archive, and snapshot scenarios, so it is always inevitable to use complex or general scenario requirements. These complex requirements are beyond what memento_t can handle.
In addition to those backup archive snapshot scenes, sometimes there are scenes you may not realize that they can also be viewed in the form of memory history. For example, the timeline display of the blog log is actually a memento list.
In fact, in order to give you a deep impression, we generally use undo_manager to express and implement the universal Memento pattern in the life of class library development. So naturally, we will try to use Memento's theory to guide the implementation of an undoable universal template class.
Memento pattern usually does not exist independently, it mostly exists as a subsystem of Command Pattern (or modules that work side by side).
Therefore, in a typical editor architecture design, text operations are always designed as Command mode, such as forwarding a word, typing in a few characters, moving the insertion point to a certain position, pasting a section of clipboard content, etc., all by one The series of Commands are implemented specifically. On this basis, Memento mode has a working space, it will be able to use EditCommand to realize the storage and replay of the editing state-by playing and replaying a (group) Edit Commands in reverse.
Undo Manager
The UndoRedo model was originally an interactive technology that provided users with the ability to go back, and then gradually evolved into a computing model. Undo models are generally divided into two types:
- Linear
- Non-linear
Linear Undo is implemented in the way of Stack. After a new command is executed, it will be added to the top of the stack. Therefore, the Undo system can roll back the executed commands in reverse order, and can only roll back in reverse order, so the Undo system implemented in this way is called linear.
There is also a strict linear mode in the linear Undo system. Usually this term means that the undo history has a size limit, just as the Stack in the CPU system also has a size limit. In many vector drawing software, Undo history is required to be set to an initial value, for example, between 1 and 99. If the rollback history table is too large, the oldest undo history entry will be discarded.
Non-linear Undo
The non-linear Undo system is similar to the linear model, and it also holds a history list of executed commands somewhere. But the difference is that when the user uses the Undo system to go back, he can choose a certain command or even a certain group of commands in the history list to undo, as if he had not executed these commands.
While Adobe Photoshop provides the operator with drawing capabilities, it maintains an almost non-linear historical operation table, and users can select a part from the list to revoke/repent. However, when PS usually cancels a certain operation record history, the subsequent updated operation records will be rolled back together. From this point of view, it can only be regarded as linear, just running batch undo.
If you want to get the non-linear undo capability, you need to enable "Allow Non-linear History" in the options of PS, which is no longer mentioned.
In fact non-linear level provided to the user in the application interface Undo / Redo ability to operate normally and no one can quite good support. One reason is that it is very easy to extract an entry from the history table and remove it. But to extract the utility of this entry on the document, it may be completely impossible. Imagine if you make the font bold for pos10..20, then italicize pos15..30, delete the text of pos16..20, and increase the font for pos 13..17. Now It is necessary to remove the italic operation done on pos15..30. Excuse me, can you Undo this step in italics? Obviously this is quite difficult: there are many interpretation methods to do this removal transaction, and they may all meet the editor's expectations. That " good " is a very subjective evaluation level.
Of course, this may not be impossible to achieve. Logically speaking, to be simple. Doesn't it mean going back three steps, giving up one operation, and then replaying (playing back) the following two operations? It may take a little bit of memory to execute. , Not necessarily how difficult it will be.
Then there must be another reason. Many interactive systems do nonlinear Undo effects, which may be difficult for users to predict mentally. Just like the undo italic operation we just exemplified, users cannot predict the consequences of undoing a record alone. , Then the interactive function provided to him actually lacks meaning-it is better to let him gradually regress, so that he will be able to accurately grasp the regressive effect of his editing utility.
No matter who makes sense in the end, it doesn't matter, they will not affect us to make software with such functions. So in fact, there are many libraries that can provide non-linear Undo capabilities, although they may not be used in a real interactive system.
In addition, there are a lot of papers on nonlinear Undo systems. This fully proves that such things as papers are often rubbish. From birth to death, don't people make rubbish as their own business? What a splendid and splendid culture that mankind thinks is really meaningless to nature and the universe-until one day in the future, mankind may be able to break the barriers and travel to a higher level of the universe, free from its innate nature. Shackled by the barriers of the universe, at that time, everything in the past may have a possible meaning.
Well, no matter how you look at the universe, I still think that the non-linear Undo Subsystem of the new memento pattern I made is meaningful, and I will show it to you below. :)
Further classification
As an additional consideration, the classification can be further organized. On top of the basic division in the previous article, further distinctions can be made:
- Undo with selection
- Groupable Undo
In general, the optional Undo is an enhanced operation of the nonlinear Undo, which allows the user to check certain records in the rollback history operation record to undo. The sub-leasable Undo means that commands can be grouped, so the user may have to roll back the grouped operation records as a whole, but this will be the responsibility of the Command Pattern to manage it, but it will only be reflected on the Undo system. That's it.
C++ implementation
In the Undo Manager implementation, there can be some typical implementation schemes:
- Script every command in command mode. This method will set up a number of checkpoints, and when Undo, first retreat to a certain checkpoint, and then replay the remaining script again, thus completing the function of undoing a certain command script
- Streamlined checkpoints. In the above method, checkpoints may consume resources very much, so sometimes it is necessary to reduce the scale of checkpoints with the help of sophisticated command system design.
- Play in reverse. This method usually only achieves linear fallback. The key idea is to execute a command in the reverse direction so as to eliminate the need to establish checkpoints. For example, the last step is to bold 8 characters, so when Undo, remove the bold for these 8 characters.
However, for a general Undo subsystem implemented by metaprogramming, the solutions mentioned above are not managed by the Undo Manager, they are managed by the Command Pattern, and in fact the specific implementation is done by the developer. . Undo Manager is only responsible for the storage, positioning and playback of states.
Main design
Let's start to really introduce the implementation ideas of undo-cxx
undoable_cmd_system_t
undoable_cmd_system_t
talk about the main body 0616bed29d16a9, which requires you to provide a main template parameter State. Adhering to the basic theory of memento mode, State refers to the state package that your Command needs to save. For example, for editor software, Command is FontStyleCmd, which means that the font style is set for the selected text, and the corresponding state package may contain A minimum description of the font style (bold, italic, etc.).
The declaration of undoable_cmd_system_t is roughly as follows:
template<typename State,
typename Context = context_t<State>,
typename BaseCmdT = base_cmd_t,
template<class S, class B> typename RefCmdT = cmd_t,
typename Cmd = RefCmdT<State, BaseCmdT>>
class undoable_cmd_system_t;
template<typename State,
typename Context,
typename BaseCmdT,
template<class S, class B> typename RefCmdT,
typename Cmd>
class undoable_cmd_system_t {
public:
~undoable_cmd_system_t() = default;
using StateT = State;
using ContextT = Context;
using CmdT = Cmd;
using CmdSP = std::shared_ptr<CmdT>;
using Memento = typename CmdT::Memento;
using MementoPtr = typename std::unique_ptr<Memento>;
// using Container = Stack;
using Container = std::list<MementoPtr>;
using Iterator = typename Container::iterator;
using size_type = typename Container::size_type;
// ...
};
template<typename State,
typename Context = context_t<State>,
typename BaseCmdT = base_cmd_t,
template<class S, class B> typename RefCmdT = cmd_t,
typename Cmd = RefCmdT<State, BaseCmdT>>
using MgrT = undoable_cmd_system_t<State, Context, BaseCmdT, RefCmdT, Cmd>;
As you can see, the State
you provided will be used by the template parameter Cmd: typename Cmd = RefCmdT<State, BaseCmdT>
.
cmd_t
And the declaration of cmd_t is like this:
template<typename State, typename Base>
class cmd_t : public Base {
public:
virtual ~cmd_t() {}
using Self = cmd_t<State, Base>;
using CmdSP = std::shared_ptr<Self>;
using CmdSPC = std::shared_ptr<Self const>;
using CmdId = std::string_view;
CmdId id() const { return debug::type_name<Self>(); }
using ContextT = context_t<State>;
void execute(CmdSP &sender, ContextT &ctx) { do_execute(sender, ctx); }
using StateT = State;
using StateUniPtr = std::unique_ptr<StateT>;
using Memento = state_t<StateT>;
using MementoPtr = typename std::unique_ptr<Memento>;
MementoPtr save_state(CmdSP &sender, ContextT &ctx) { return save_state_impl(sender, ctx); }
void undo(CmdSP &sender, ContextT &ctx, Memento &memento) { undo_impl(sender, ctx, memento); }
void redo(CmdSP &sender, ContextT &ctx, Memento &memento) { redo_impl(sender, ctx, memento); }
virtual bool can_be_memento() const { return true; }
protected:
virtual void do_execute(CmdSP &sender, ContextT &ctx) = 0;
virtual MementoPtr save_state_impl(CmdSP &sender, ContextT &ctx) = 0;
virtual void undo_impl(CmdSP &sender, ContextT &ctx, Memento &memento) = 0;
virtual void redo_impl(CmdSP &sender, ContextT &ctx, Memento &memento) = 0;
};
In other words, State will be packaged and used inside the undo system.
The Command class you should provide should be derived from cmd_t and implement the necessary pure virtual functions (do_execute, save_state_impl, undo_impl, redo_impl, etc.).
Use: Provide your command
According to the above announcement, we can achieve a command for demonstration purposes:
namespace word_processor {
template<typename State>
class FontStyleCmd : public undo_cxx::cmd_t<State> {
public:
~FontStyleCmd() {}
FontStyleCmd() {}
explicit FontStyleCmd(std::string const &default_state_info)
: _info(default_state_info) {}
UNDO_CXX_DEFINE_DEFAULT_CMD_TYPES(FontStyleCmd, undo_cxx::cmd_t);
protected:
virtual void do_execute(CmdSP &sender, ContextT &) override {
UNUSED(sender);
// ... do sth to add/remove font style to/from
// current selection in current editor ...
std::cout << "<<" << _info << ">>" << '\n';
}
virtual MementoPtr save_state_impl(CmdSP &sender, ContextT &ctx) override {
return std::make_unique<Memento>(sender, _info);
}
virtual void undo_impl(CmdSP &sender, ContextT &, Memento &memento) override {
memento = _info;
memento.command(sender);
}
virtual void redo_impl(CmdSP &sender, ContextT &, Memento &memento) override {
memento = _info;
memento.command(sender);
}
private:
std::string _info{"make italic"};
};
}
In a real editor, we believe that you have a container for all editor windows and can track the editor that currently has the input focus.
Based on this, do_execute should set the font style of the selected text in the current editor (such as bold), save_state_impl should pack the meta information of the selected text and the meta information of the Command into State
, undo should set the font in reverse Style (such as removing the bold), redo should set the font style (bold) again State
But in this case, for demonstration purposes, these specific details are represented by a _info string.
Although FontStyleCmd retains the State template parameter, State will only be equal to std::string in the demo code.
Use: Provide UndoCmd and RedoCmd
In order to customize your Undo/Redo behavior, you can implement your own UndoCmd/RedoCmd. They need a special base class different from cmd_t:
namespace word_processor {
template<typename State>
class UndoCmd : public undo_cxx::base_undo_cmd_t<State> {
public:
~UndoCmd() {}
using undo_cxx::base_undo_cmd_t<State>::base_undo_cmd_t;
explicit UndoCmd(std::string const &default_state_info)
: _info(default_state_info) {}
UNDO_CXX_DEFINE_DEFAULT_CMD_TYPES(UndoCmd, undo_cxx::base_undo_cmd_t);
protected:
void do_execute(CmdSP &sender, ContextT &ctx) override {
std::cout << "<<" << _info << ">>" << '\n';
Base::do_execute(sender, ctx);
}
};
template<typename State>
class RedoCmd : public undo_cxx::base_redo_cmd_t<State> {
public:
~RedoCmd() {}
using undo_cxx::base_redo_cmd_t<State>::base_redo_cmd_t;
explicit RedoCmd(std::string const &default_state_info)
: _info(default_state_info) {}
UNDO_CXX_DEFINE_DEFAULT_CMD_TYPES(RedoCmd, undo_cxx::base_redo_cmd_t);
protected:
void do_execute(CmdSP &sender, ContextT &ctx) override {
std::cout << "<<" << _info << ">>" << '\n';
Base::do_execute(sender, ctx);
}
};
}
Note that for them, the corresponding base class is limited to base_(undo/redo)_cmd_t, and you must include the call to the base class method in the do_execute implementation, like this:
void do_execute(CmdSP &sender, ContextT &ctx) override {
// std::cout << "<<" << _info << ">>" << '\n';
Base::do_execute(sender, ctx);
}
There is a default implementation in the base class, which looks like this:
template<typename State, typename BaseCmdT,
template<class S, class B> typename RefCmdT>
inline void base_redo_cmd_t<State, BaseCmdT, RefCmdT>::
do_execute(CmdSP &sender, ContextT &ctx) {
ctx.mgr.redo(sender, Base::_delta);
}
It actually calls ctx.mgr specifically, that is, redo() of undoable_cmd_system_t to complete specific internal affairs. Similarly, there are similar statements in undo.
The special thing about undo/redo is that their base class has special overloaded functions:
virtual bool can_be_memento() const override { return false; }
The purpose is not to consider the memento archive of the command.
So also note that save_state_impl/undo_impl/redo_impl is unnecessary.
actions_controller
We now assume that the word processor software has a command manager, which is also a command action controller, and it will be responsible for executing an editing command in a specific editor window:
namespace word_processor {
namespace fct = undo_cxx::util::factory;
class actions_controller {
public:
using State = std::string;
using M = undo_cxx::undoable_cmd_system_t<State>;
using UndoCmdT = UndoCmd<State>;
using RedoCmdT = RedoCmd<State>;
using FontStyleCmdT = FontStyleCmd<State>;
using Factory = fct::factory<M::CmdT, UndoCmdT, RedoCmdT, FontStyleCmdT>;
actions_controller() {}
~actions_controller() {}
template<typename Cmd, typename... Args>
void invoke(Args &&...args) {
auto cmd = Factory::make_shared(undo_cxx::id_name<Cmd>(), args...);
_undoable_cmd_system.invoke(cmd);
}
template<typename... Args>
void invoke(char const *const cmd_id_name, Args &&...args) {
auto cmd = Factory::make_shared(cmd_id_name, args...);
_undoable_cmd_system.invoke(cmd);
}
void invoke(typename M::CmdSP &cmd) {
_undoable_cmd_system.invoke(cmd);
}
private:
M _undoable_cmd_system;
};
} // namespace word_processor
Finally, the test function
With the aid of the improved factory mode, the controller can call editing commands. Note that when the user issues undo/redo, the controller also completes the corresponding business logic by calling UndoCmd/RedoCmd.
void test_undo_sys() {
using namespace word_processor;
actions_controller controller;
using FontStyleCmd = actions_controller::FontStyleCmdT;
using UndoCmd = actions_controller::UndoCmdT;
using RedoCmd = actions_controller::RedoCmdT;
// do some stuffs
controller.invoke<FontStyleCmd>("italic state1");
controller.invoke<FontStyleCmd>("italic-bold state2");
controller.invoke<FontStyleCmd>("underline state3");
controller.invoke<FontStyleCmd>("italic state4");
// and try to undo or redo
controller.invoke<UndoCmd>("undo 1");
controller.invoke<UndoCmd>("undo 2");
controller.invoke<RedoCmd>("redo 1");
controller.invoke<UndoCmd>("undo 3");
controller.invoke<UndoCmd>("undo 4");
controller.invoke("word_processor::RedoCmd", "redo 2 redo");
}
characteristic
In the implementation of undoable_cmd_system_t, basic Undo/Redo capabilities are included:
- Unlimited Undo/Redo
- Restricted: limit the number of history records
undoable_cmd_system_t::max_size(n)
In addition, it is fully customizable:
- Customize your own State package
- Customize your extended version of context_t to accommodate custom object references
- If necessary, you can customize base_cmd_t or cmd_t to achieve your special purpose
Group command
With the base class class composite_cmd_t
you can group commands, they are treated as a single record in the Undo history, which allows you to batch Undo/Redo.
In addition to establishing a combined command immediately during construction, a class GroupableCmd
can be constructed on the basis of composite_cmd_t. It is easy to use this class to provide the ability to combine several commands in place at runtime. In this way, you can get a more flexible command group.
Restricted nonlinearity
Restricted non-linear undo function can be realized through batch Undo/Redo.
undoable_cmd_system_t::erase(n = 1)
can delete the history of the current location.
You can think of undo i-erase j-redo k as a restricted non-linear undo/redo implementation. Note that this requires you to further package before using it (by adding _erased_count
members to UndoCmd/RedoCmd and executing ctx.mgr.erase(_erased_count)
).
A more full-featured non-linear undo may require a more complex tree-like history record instead of the current list, which needs to be implemented in the future.
summary
Due to space limitations, it is not possible to fully introduce the undo-cxx , so interested friends can directly review the Github source code.
postscript
This time, the Undo Manager has not been perfect yet, so look for opportunities to improve later.
refer to:
I'll review it after a while, so let's make a decision first.
:end:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。