Я пытался полностью переварить шаблон отмены, показанный в Доклад Шона Родителя «Наследование — базовый класс зла». В докладе рассказывается о нескольких основах, включая семантику перемещения C ++ и использование концепций для реализации полиморфизма вместо наследования, но я пытался разобраться с шаблоном хранения delta-undo. Вот рабочая адаптация примера, который Родитель привел в своем выступлении:
#include <iostream>
#include <memory>
#include <vector>
#include <assert.h>
using namespace std;
template <typename T>
void draw(const T& x, ostream& out, size_t position)
{
out << string(position, ' ') << x << endl;
}class object_t {
public:
template <typename T>
object_t(T x) : self_(make_shared<model<T>>(move(x))) {}
friend void draw(const object_t& x, ostream& out, size_t position)
{ x.self_->draw_(out, position); }
private:
struct concept_t {
virtual ~concept_t() = default;
virtual void draw_(ostream&, size_t) const = 0;
};
template <typename T>
struct model : concept_t {
model(T x) : data_(move(x)) { }
void draw_(ostream& out, size_t position) const
{ draw(data_, out, position); }
T data_; };
shared_ptr<const concept_t> self_;
};// The document itself is drawable
using document_t = vector<object_t>;
void draw(const document_t& x, ostream& out, size_t position)
{
out << string(position, ' ') << "<document>" << endl;
for (const auto& e : x) draw(e, out, position + 2);
out << string(position, ' ') << "</document>" << endl;
}
// An arbitrary class
class my_class_t {
/* ... */
};
void draw(const my_class_t&, ostream& out, size_t position)
{ out << string(position, ' ') << "my_class_t" << endl; }
// Undo management...
using history_t = vector<document_t>;
void commit(history_t& x) { assert(x.size()); x.push_back(x.back()); }
void undo(history_t& x) { assert(x.size()); x.pop_back(); }
document_t& current(history_t& x) { assert(x.size()); return x.back(); }
// Usage example.
int main(int argc, const char * argv[])
{
history_t h(1);current(h).emplace_back(0);
current(h).emplace_back(string("Hello!"));
draw(current(h), cout, 0);
cout << "--------------------------" << endl;
commit(h);
current(h).emplace_back(current(h));
current(h).emplace_back(my_class_t());
current(h)[1] = string("World");
draw(current(h), cout, 0);
cout << "--------------------------" << endl;
undo(h);
draw(current(h), cout, 0);
return EXIT_SUCCESS;
}
Вместо отслеживания отмены в виде стека команд, которые фиксируют их состояния до и после, этот шаблон отслеживает состояния отмены в виде стека «целых документов», где каждая запись фактически является полной копией документа. Хитрость паттерна заключается в том, что хранение / распределение производятся только для тех частей документа, которые различаются в каждом состоянии, используя некоторую косвенность и shared_ptr
, Каждая «копия» влечет за собой штраф за хранение в зависимости от того, что отличается между ней и предыдущим состоянием.
Шаблон в примере Parent показывает, что «текущий» документ является полностью изменяемым, но он фиксируется в истории, когда вы вызываете commit
по истории. Это помещает «копию» текущего состояния в историю.
В резюме я нахожу эту модель убедительной. Пример, который Родитель представляет в этом выступлении, был явно придуман прежде всего для демонстрации его идей о концептуальном полиморфизме и семантике перемещения. По сравнению с этим, шаблон отмены кажется вспомогательным, хотя я думаю, что его роль заключается в том, чтобы указать значение семантики значения.
В этом примере документ «модель» — это просто «вектор объектов, соответствующих концепции». Это послужило своей цели для демонстрации, но мне трудно экстраполировать от «вектора понятий» до «реального мира, типизированной модели». (Давайте просто скажем, что для целей этого вопроса концептуальный полиморфизм не имеет значения.) Так, например, рассмотрим следующую тривиальную модель, где «документ» является company
с некоторым количеством employees
каждый с именем, зарплатой и фотографией:
struct image {
uint8_t bitmapData[640 * 480 * 4];
};
struct employee {
string name;
double salary;
image picture;
};
struct company {
string name;
string bio;
vector<employee> employees;
};
У меня такой вопрос: как я могу ввести косвенное направление, необходимое для совместного использования хранилища, не теряя возможности прямого и простого взаимодействия с моделью? Под простотой взаимодействия я подразумеваю, что вы должны иметь возможность продолжать взаимодействовать с моделью простым способом без большого количества RTTI или приведения и т. Д. Например, если вы пытались дать каждому по имени «Сьюзен» повышение на 10% , фиксируя состояние отмены после каждого изменения, простое взаимодействие может выглядеть примерно так:
using document_t = company;
using history_t = vector<document_t>;
void commit(history_t& x) { assert(x.size()); x.push_back(x.back()); }
void undo(history_t& x) { assert(x.size()); x.pop_back(); }
document_t& current(history_t& x) { assert(x.size()); return x.back(); }
void doStuff()
{
history_t h(1);
for (auto& e : current(h).employees)
{
if (e.name.find("Susan") == 0)
{
e.salary *= 1.1;
commit(h);
}
}
}
Уловка, кажется, вводит косвенность, обеспеченную object_t
, но не понятно, как я могу оба ввести необходимую косвенность а также впоследствии прозрачно пройти через эту косвенность. Я обычно могу обойтись в коде C ++, но это не мой повседневный язык, так что это может быть очень просто. Несмотря на это, неудивительно, что пример Родителя не охватывает это, так как большая часть его точки зрения была способность скрывать тип с помощью понятий.
У кого-нибудь есть мысли по этому поводу?
В то время как документ является изменяемым, объекты не являются.
Для того, чтобы отредактировать объект, вам нужно создать новый.
В практическом решении каждый объект может быть интеллектуальным держателем указателя при записи, к которому вы можете получить доступ, читая или записывая. Доступ на запись дублирует объект, если он имеет счетчик ссылок выше единицы.
Если вы хотите ограничить все мутации методами доступа, вы можете сделать копию при записи в них. Если нет, то get_writable
метод делает копию на запись. Обратите внимание, что модификация обычно подразумевает модификацию вплоть до корня, поэтому вашему методу записи может потребоваться указать путь к корню, где копия при записи распространяется туда. В качестве альтернативы, вы можете использовать контекст документа и эквивалентные идентификаторы guid и хэш-карту, поэтому редактирование foo, содержащегося в строке, оставляет панель неизменной, так как она идентифицирует foo по имени, а не по указателю.
Других решений пока нет …