@sirQaziop

Как реализовать обратную совместимость пользовательских данных в программе?

Разрабатывается десктоп-приложение на c#, в котором пользователь сможет создавать и вести собственный проект данных. Разработка итерационная, с каждой новой версией появляется и новая функциональность. Соответственно, в проекте растет количество данных, формат сохраняемого проекта также меняется (точнее, дополняется).
Требуется реализовать поддержку проектов старых версий программы, в частности:
1) Загрузку всех предыдущих форматов пользовательского проекта.
2) Сохранение текущего проекта в проект старого формата.
Собственно, вопрос: как лучше реализовать обратную совместимость форматов, где можно об этом почитать и существуют ли готовые решения?
  • Вопрос задан
  • 169 просмотров
Решения вопроса 1
jamakasi666
@jamakasi666
Просто IT'шник.
Если формат данных только дополняется то решение элементарное. Скажем у вас в проекте есть 2 класса реализующих чтение и сохранение данных. С каждой версией у вас просто появляются новые данные и старые точно не меняются. Добавляете(если не сделали уже) в каждом файле номер версии. В программе с каждым нововведением просто делаете еще пару классов чтения\записи под новую версию. Потом просто при открытие файла смотреть версию и использовать нужный класс для чтения.

Другой вариант более тупой, поправить класс чтения\записи так чтобы он игнорировал неизвестные ему данные. Т.е. если вы откроете в старой версии программы файл от более новой версии то он просто проигнорирует неизвестные ему данные.

На практике видел реализацию очень интересную. Там было очень хитро устроено чтение. Правда проект на яве был.
Был класс чтения\записи файла, псевдокод:
class CReader{
public CReader(URL file);
void readData(){
   someStructs;
}
void writeData(){
   someStructs;
}
... другие методы
}

То была первая версия программы, потом выходит новая версия в которой появились некие новые данные и структуры но старые не изменялись. Псевдокод:
class CReader1 extends CReader{
@Override
void readData(){
   super(); //Выполнить родительский метод
   someNewStructs;
}
@Override
void writeData(){
   super(); //Выполнить родительский метод
   someNewStructs;
}
}

Т.е. принцип такой что в конечном счете все новые данные которые вводятся с новой версией программы всегда пишутся в конце файла. Файл прекрасно открывается в старых версиях программы и без каких либо ошибок, просто если проект хотят сохранить в старой версии и присутствуют данные которых не было в старых версиях то выводится предупреждение при сохранении файла о частичной потере информации. Решение до глупости простое и в тоже время гениальное.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 3
index0h
@index0h
PHP, Golang. https://github.com/index0h
Храните набор миграция от старых версий до последней. При сохранении старого проекта - выполняйте обязательное приведение к последнему формату. При открытии проекта - тоже самое
Ответ написан
@mikhail_404
В таких случая пишется свой VersionUpdater, который перестраивает структуру хранения данных, т.е. если мы хотим добавить новое поле в БД, то нужно сохранить старую информацию и добавить к ней новую (например, перенос в новую таблицу с добавленным полем). Такой подход позволит корректно обрабатывать старые данные и добавлять новые прямо в коде, а в VersionUpdater корректно все завершаем.
Ответ написан
Комментировать
@Mercury13
Программист на «си с крестами» и не только
Проще всего это сделать в XML. Тогда мы легко можем добавлять новые теги и игнорировать то, что в XML не входит.

Двоичный файл надо делать на манер XML — в виде иерархической конструкции из коротеньких потоков. Самое сложное — чтобы по этому двоичному файлу не надо было бегать туда-сюда, как по гоночному треку. Для этого на каждом из уровней иерархии бывает одно из двух.
1. Запросить некую последовательность кодов, а затем спрашивать: есть этот код? А есть этот? Что-то типа (для простоты пишу как на C++)…
BlockReader blk;
int order1[] = { opHeader, opSettings, dirData };
BlockOrder order(blk, order1);

order.require(opHeader);
// считать заголовок
author = blk.readString();

if (order.get(opSettings) {
  // считать настройки
}

order.require(dirData);
blk.enterDir();
  // считать данные таким же образом — там может быть свой BlockOrder
blk.leaveDir();


Да, для чего мы запрашиваем порядок блоков дважды?
Первый раз — вот для чего. Представим себе устаревший файл, в котором нет блока настроек. Мы считываем заголовок, видим вместо блока настроек каталог с данными, и сразу вопрос: тут что-то пропущено или что-то лишнее?
Второй раз — для наглядности (код комментирует сам себя) и защиты от ошибок, когда заявленный в начале порядок не совпадает с реальным.

2. Просто считываем блоки по одному и интерпретируем. Обычно это бывает во всяких коллекциях.
while (blk.getBlock()) {
  switch (blk.opcode) {
  case dirTiledLevel:
     // считать плиточный уровень
  case dirGraphicLevel:
     // считать уровень с фоном — цельной картинкой
  }
}

Запрещается смешивать упорядоченное с коллекциями.

Можно также в заголовке каждого блока сделать бит: Essential. Старая версия, наткнувшись на такой блок и не считав ни байта (или наткнувшись на каталог и не войдя), выводит ошибку: версия явно новее, считать невозможно. Это бывает важно, когда в файле есть перекрёстные ссылки.

Для записи подобных файлов приходится накапливать блок в памяти, а затем сваливать его в один присест (ведь надо записать длину блока). Впрочем, есть и версия для структур, которые заранее знают свою длину — тогда накапливать в памяти излишне.

Далее. В заголовке можно сохранить поля «SavedWithVersion», «MinRequiredVersion», «MinFullySupportingVersion».
Добавляем новый блок (или новое поле в имеющийся блок) — поднимаем MinFullySupportingVersion до текущей. Изменяем структуру так, что ломаем совместимость — поднимаем MinRequiredVersion.

То, что версия слегка устарела, можно ловить и по косвенным признакам — какой-то блок считан не полностью, в какой-то каталог не вошли. У блока/каталога может быть флаг Compatibility — их проверять не надо. И наоборот — если важный блок отмечен как Compatibility, тоже версия устарела. Ясное дело, флаги Essential и Compatibility не могут попадаться вместе.

Как это делать, если формат сохранения — база данных (например, SQLite), я не в курсе.

А вот сохранение в устаревший формат, когда структуры данных ушли далеко вперёд, придётся налаживать руками. Можно ли в принципе, как преобразовать в старый формат, и т.д.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы