
История одной миграции типов: как эволюционирует простейший редактор графов
Главный герой этой статьи — не код
Это файл сохранения
save_node_editor.qap.
Пока мы 5 раз меняли структуры данных, добавляли поля и новые типы — этот файл продолжал работать. Без миграционных скриптов, без ручных конвертеров, без потери данных пользователей.
Это возможно благодаря самоописывающейся сериализации с автоматической миграцией.
В реальном проекте сложность появляется не из-за алгоритмов, а из-за сохранённых данных пользователей.
Если формат данных не умеет расти — проект умирает.
Я покажу эволюцию простого редактора графов — от «чёрных точек» до узлов с портами, цветами и красиво переливающимися цветами каналов. Картинки для удобства пронумерованы от 006 до 011 и соответствуют разным версиям формата данных.
Проект — эксперимента��ьный визуальный редактор графов.
Он развивается итеративно: структура данных меняется чаще, чем логика.
Сохранённые файлы используются постоянно между сборками.
Прежде чем приступить опишем основные структуры редактора.
// слой/документ в котором будет хранится наша геометрия class t_layer{ public: #define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_layer) #define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\ ADDBEG()\ ADDVAR(string,name,DEF,$,$)\ ADDEND() //=====+>>>>>t_layer #include "QapGenStruct.inl" //<<<<<+=====t_layer public: }; // наш мир с котором будет работать редактор // добавим для удобства в него несколько слоёв/документов помимо текущего слоя class t_world{ public: #define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_world) #define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\ ADDBEG()\ ADDVAR(int,tick,SET,0,$)\ ADDVAR(vector<t_layer>,layers,DEF,$,$)\ ADDVAR(int,layer_id,SET,-1,$)\ ADDVAR(t_layer,cur,DEF,$,$)\ ADDEND() //=====+>>>>>t_world #include "QapGenStruct.inl" //<<<<<+=====t_world public: };
В реальном проекте изменения происходили в другом порядке и с откатами, но для статьи путь упрощён и выпрямлен — чтобы наглядно показать сам принцип разработки при использовании last_hope_loader на основе RTTI.
Начнём с минимального возможного формата.
Версия 006 — просто узлы
Модель данных
class t_node{ public: #define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_node) #define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\ ADDBEG()\ ADDVAR(TSelfPtr<SelfClass>,Self,DEF,$,$)\ ADDVAR(bool,enabled,SET,true,$)\ ADDVAR(vec2d,pos,DEF,$,$)\ ADDVAR(double,r,SET,32,$)\ ADDEND() //=====+>>>>>t_node #include "QapGenStruct.inl" //<<<<<+=====t_node }; // Добавим "vector<t_node> t_layer::nodes;" class t_layer{ public: #define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_layer) #define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\ ADDBEG()\ ADDVAR(string,name,DEF,$,$)\ ADDVAR(vector<t_node>,nodes,DEF,$,$)\ ADDEND() //=====+>>>>>t_layer #include "QapGenStruct.inl" //<<<<<+=====t_layer public: };
Никакой логики. Просто геометрия и сериализация.
На экране — только чёрные шары, раскиданные по плоскости.

Версия 007 — появляются провайдеры цвета
Миграция данных
Мы аккуратно расширяем тип, не ломая текущий файл сохранения с состоянием редактора:
// Добавляем "QapColor t_node::color=0xff000000;" // Добавляем "bool t_node::provider=false;" class t_node{ public: #define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_node) #define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\ ADDBEG()\ ADDVAR(TSelfPtr<SelfClass>,Self,DEF,$,$)\ ADDVAR(bool,enabled,SET,true,$)\ ADDVAR(vec2d,pos,DEF,$,$)\ ADDVAR(double,r,SET,32,$)\ ADDVAR(QapColor,color,SET,0xff000000,$)\ ADDVAR(bool,provider,SET,false,$)\ ADDEND() //=====+>>>>>t_node #include "QapGenStruct.inl" //<<<<<+=====t_node };
Никаких миграционных скриптов мы писать не будем в этой статье, т.к они не нужны(это не означает, что миграции не нужны вообще — это означает, что в данном классе изменений они не требуются), в этом примере используется сериализатор с поддержкой автоматической миграции типов (проект QapSerialize), поэтому отдельные миграционные скрипты здесь не требуются. Всё сделает автоматика.
Вот пользовательский код вызывающий механизм загрузки с поддержкой автоматической миграции(с помощью которой на протяжении в��ей статьи пользовательские данные будут выживать):
class TGame:public TQapGameV2{ public: IEnvRTTI*pEnv=nullptr; string fn="save_node_editor.qap"; t_world w; public: void DoMigrate(){ // пробуем загрузиться из бинарного файла методом "последняя надежда" // не самый быстрый метод, но зато самый универсальный(с миграцией). // загрузчик сопоставляет поля по именам и типам, // инициализируя новые значениями по умолчанию и игнорируя отсутствующие if(!QapPublicUberFullLoaderBinLastHope(*pEnv,QapRawUberObject(w),fn)){ DoInit(); // на случай если не получилось } } void DoMove()override{ if(kb.OnDown(VK_ESCAPE)){ // сохраняем в универсальный бинарный формат QapPublicUberFullSaverBin(*pEnv,QapRawUberObject(w),fn); TerminateProcess(GetCurrentProcess(),0); } if(w.tick==0){ DoMigrate(); } DoUpdate(); w.tick++; } ... };
Запускаем и смотрим на экран
Некоторые шары окрашены, остальные — чёрные.

Версия 008 — добавляем связи (линки)
Новые типы
class t_endpoint{ public: #define DEF_PRO_AUTO_COPY #define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_endpoint) #define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\ ADDBEG()\ ADDVAR(TWeakPtr<t_node>,n,DEF,$,$)\ ADDEND() //=====+>>>>>t_endpoint #include "QapGenStruct.inl" //<<<<<+=====t_endpoint public: }; class t_link{ public: #define DEF_PRO_AUTO_COPY #define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_link) #define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\ ADDBEG()\ ADDVAR(bool,enabled,SET,true,$)\ ADDVAR(t_endpoint,a,DEF,$,$)\ ADDVAR(t_endpoint,b,DEF,$,$)\ ADDVAR(double,line_size,SET,4,$)\ ADDEND() //=====+>>>>>t_link #include "QapGenStruct.inl" //<<<<<+=====t_link public: };
И ещё добавим vector<t_link> links; в t_layer:
class t_layer{ public: #define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_layer) #define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\ ADDBEG()\ ADDVAR(string,name,DEF,$,$)\ ADDVAR(vector<t_node>,nodes,DEF,$,$)\ ADDVAR(vector<t_link>,links,DEF,$,$)\ ADDEND() //=====+>>>>>t_layer #include "QapGenStruct.inl" //<<<<<+=====t_layer public: };
На этом этапе граф уже есть, но он ещё топологически бедный.
Запускаем и смотрим на экран
Между шарами появляются линии, соединяющие центры.

Версия 009 — распространение цвета (BFS)
Здесь появляется поведенческая логика, но формат данных почти не меняется.
Важно: сам алгоритм здесь не принципиален. Он нужен лишь для того, чтобы показать, что логика может меняться и усложняться, в то время как формат данных продолжает эволюционировать независимо.
void DoPropWithBFS(){ auto&nodes=w.cur.nodes; auto&links=w.cur.links; vector<t_node*> cur,next;map<t_node*,int> V; map<t_node*,t_color_accum> acc; for(auto&ex:nodes)if(ex.provider){cur.push_back(&ex);acc[&ex].add(ex.color);} for(int iter=1;cur.size();iter++){ for(auto&f:cur){ for(auto&ex:links){ if(!ex.a.n||!ex.b.n)continue; if(ex.a.n.get()!=f&&ex.b.n.get()!=f)continue; auto*oe=ex.a.n.get()==f?ex.b.n.get():ex.a.n.get(); if(oe->provider||oe==f)continue; acc[oe].add(acc[f].get()); auto&v=V[oe];if(v)continue;v=iter; next.push_back(oe); } } cur=std::move(next); } for(int i=0;i<nodes.size();i++){ auto&n=nodes[i]; if(n.provider)continue; if(!acc[&n].n)continue; n.color=acc[&n].get(); n.name=IToS(acc[&n].n); } }
Результат:
Цвет от провайдеров «растекается» по графу.

Версия 010 — порты
Новые изменения
// добавляем новый тип t_port class t_port{ public: #define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_port) #define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\ ADDBEG()\ ADDVAR(vec2d,offset,DEF,$,$)\ ADDVAR(double,r,SET,8,$)\ ADDVAR(string,type,DEF,$,$)\ ADDEND() //=====+>>>>>t_port #include "QapGenStruct.inl" //<<<<<+=====t_port public: }; // добавляем "vector<t_port> t_node::ports;" class t_node{ public: #define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_node) #define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\ ADDBEG()\ ADDVAR(TSelfPtr<SelfClass>,Self,DEF,$,$)\ ADDVAR(bool,enabled,SET,true,$)\ ADDVAR(vec2d,pos,DEF,$,$)\ ADDVAR(double,r,SET,32,$)\ ADDVAR(QapColor,color,SET,0xff000000,$)\ ADDVAR(bool,provider,SET,false,$)\ ADDVAR(vector<t_port>,ports,DEF,$,$)\ ADDEND() //=====+>>>>>t_node #include "QapGenStruct.inl" //<<<<<+=====t_node public: }; // добавляем "int t_endpoint::port_id;" class t_endpoint{ public: #define DEF_PRO_AUTO_COPY #define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_endpoint) #define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\ ADDBEG()\ ADDVAR(TWeakPtr<t_node>,n,DEF,$,$)\ ADDVAR(int,port_id,SET,0,$)\ ADDEND() //=====+>>>>>t_endpoint #include "QapGenStruct.inl" //<<<<<+=====t_endpoint public: };
Обратите внимание: мы не «переделываем» существующие линки, а лишь расширяем их интерпретацию. Старые данные по-прежнему валидны — просто теперь они могут быть отображены более точно.
Добавляем простой алгоритм выбора лучших портов:
void DoFixLinkPorts(t_link&ref){ if(!ref.a.n||!ref.b.n)return; struct t_best{bool ok=false;int a;int b;int d;void aib(const t_best&c){if(!ok||c.d<d)*this=c;}}; t_best best; int aid=0; for(auto&a:ref.a.n->ports){ int bid=0; for(auto&b:ref.b.n->ports){ auto pa=ref.a.n->pos+a.offset; auto pb=ref.b.n->pos+b.offset; t_best cur={true,aid,bid,pa.sqr_dist_to(pb)}; best.aib(cur); bid++; } aid++; } if(!best.ok)return; ref.a.port_id=best.a; ref.b.port_id=best.b; }
Результат:
Линки подключаются не к центру, а к портам.

Версия 011 — красивые кривые между портами
Последнее расширение
// Добавляем "vector<vec2d> t_link::line;" class t_link{ public: #define DEF_PRO_AUTO_COPY #define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_link) #define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\ ADDBEG()\ ADDVAR(bool,enabled,SET,true,$)\ ADDVAR(t_endpoint,a,DEF,$,$)\ ADDVAR(t_endpoint,b,DEF,$,$)\ ADDVAR(double,line_size,SET,4,$)\ ADDVAR(vector<vec2d>,line,DEF,$,$)\ ADDEND() //=====+>>>>>t_link #include "QapGenStruct.inl" //<<<<<+=====t_link public: };
На этом этапе формат данных позволяет хранить произвольную геометрию линков — вне зависимости от того, была ли она получена автоматически, интерактивно или внешним инструментом.
Финальный вид: линки — это уже геометрия, а не просто абстрактная связь.

Этот пример не про редактор графов, а про демонстрацию работы алгоритма сериализации. Про то, как живёт формат данных в реальном проекте.
Когда система развивается, мы неизбежно:
добавляем новые поля
меняем интерпретацию старых
усложняем логику
И если формат данных спроектирован так, чтобы расти, а не ломаться, то история проекта перестаёт быть балластом и становится активом.
Алгоритмы можно переписать. Сохранённые данные — нет.
Хорошо спроектированный формат данных — это не следствие архитектуры. Это причина, по которой архитектура вообще может меняться.
Что это даёт на практике?
Быстрые эксперименты — можно пробовать радикальные изменения структур
Параллельная разработка — разные версии редактора могут работать с одними данными
Отладка — всегда можно сохранить состояние и воспроизвести баг
Пользовательские моды — сообщество может расширять форматы без страха сломать всё
Технический итог
// Мы прошли путь от: struct t_node{vec2d pos;double r;}; // До: struct t_node{ vec2d pos; double r; QapColor color; bool provider; vector<t_port> ports; }; // + связи, геометрия, состояние
И ни один сохранённый объект не был потерян.
Хочешь так же?
Проект с сериализатором: [QapSerialize] :: Исходники редактора
Эт�� статья — лишь демо. В реальной системе есть:
Поддержка полиморфизма и наследования
Циклические ссылки без утечек
Текстовый дамп для отладки
И многое другое... Пиши в комментариях, если хочешь подробностей про внутреннее устройство!
