Ознакомьтесь с нашей политикой обработки персональных данных
23:53 

Загрузка и сохранение файлов разных версий

zHz00
А сегодня, дорогие друзья, я расскажу вам о методе, позволяющем встраивать в программу поддержку файлов данных сразу нескольких версий, не обременяя её дублированием кода для загрузки каждой отдельной версии


Итак, у нас есть прикладная программа, которая должна уметь сохранять и загружать файлы со своими собственными данными ("документами") разных версий. Предполагается, что программа активно развивается, поэтому версии файла появляются достаточно часто. В программе которая не доделывается на ходу такого быть просто не может, однако случай, когда программа делалась не по ТЗ и разработка совмещена с эксплуатацией вполне возможен. Например, если программа подключена к оборудованию, которое ещё не отлажено, но уже используется.

В таких случаях выпуск новой версии программы в которой изменён формат файла (такая может выпускаться хоть каждую неделю (% ), если ей пользуется хотя бы десяток человек, плодит некоторое количество файлов данных подходящих для этой версии. В итоге получается полная каша из файлов разных версий. Программа должна уметь их все читать, и, возможно, сохранять в заданную версию. Как это сделать? Как предусмотреть расширяемость формата?

Давным-давно был изобретён простой метод, при сохранении в файле оставляют "дыры" -- поля с пометкой "зарезервировано". Такие зарезервированные дыры до сих пор встречаются в заголовках файлов и даже в параметрах функций (это вообще сурово). Но никогда не знаешь, что придётся сохранить. Вдруг у программы появится дополнительная функция, обслуживание которой в файлах данных будет занимать (о боже!) целых четыре килобайта? Такие большие дыры, конечно, делать нельзя. Можно версии файлов делать просто независимо. Тогда по общему заголовку будет определяться версия файла, а загрузка будет идти отдельной функцией. Я видел такую реализацию.

BOOL LoadFile_v1(CString sFileName);
BOOL LoadFile_v2(CString sFileName);
BOOL LoadFile_v3(CString sFileName);
...


(всего таких функций было 27 (двадцать семь) штук)

Спустя пару новых версий мне это безобразие надоело и я ввёл свою систему, с шахматами и поэтессами. Удобно её использование, правда, только в Си-подобных языках.

В чём суть.

Новые поля/массивы добавляются в начало файла, сразу после сигнатуры и номера версии. Если добавляется массив, сначала указывается его размер, потом идёт он сам. Тогда при помощи switch(номер_версии) оператора можно будет загружать файл так, как будто он определённой версии, пропуская поля от слишком новых файлов.

Приведу пример. Пусть есть первая версия файла, в ней три целых поля (: a,b,c. Во второй добавляется поле d.

int load_file(char *fname)
{
int desc;// дескриптор файла
desc=open(fname, O_RDONLY);
if(!desc)
return -1;// файл не открывается
char buf[48];
int ver;
int a,b,c,d;
read(desc,buf,4);
// ...
// ^ тут проверяется сигнатура
read(desc,&ver, sizeof(ver));
switch(ver)
{
case 2://сначала вторая версия
read(desc,&d,sizeof(d));
case 1:// если версия файла вторая, то после выполнения case 2: оператор не прервётся (нету break;)
// и продолжит считывание первой версии
// а если версия первая, поля для второй будут пропущены и сразу будет загружаться первая.
read(desc,&a,sizeof(a));
read(desc,&b,sizeof(b));
read(desc,&c,sizeof(c));
break;// других версий файла нет
default:
return -2;// неверная версия файла
}
close(desc);
// ..
// тут считанные данные отправляются по назначению
return 0;
}


Так можно держать всего одну функцию загрузки и сколько угодно версий файлов. Если какие-то поля удаляются, то будут висеть в файле мёртвым грузом.

С массивами (строками):


char *buf;
int size;
...
read(desc, &size);
buf=malloc(size);
read(desc, buf, size);
// ...
// тут отправляем данные по назначению
free(buf);
// ну или если максимальный размер массива ограничен, можно использовать и статический


С сохранением файла: либо сохраняем всегда в последнюю версию и добавляем новые write(...) в начало, либо делаем аналогичный переключатель, но на запись. Тогда в параметрах должен быть номер версии, в которую записывать.

Что же делать со старыми файлами и набором файлов лоад-лоад-лоад?
1. Ничего. Пусть лежат мёртвым грузом и используются если надо загрузить старую версию.
2. Ничего, т.к. их нет. Вы с самого начала перешли на эту систему или до её ввода модернизация формата файла проводилась за счёт резервных полей.
3. Сделать из них отдельную утилиту конвертации и вырезать из вашей программы.
4. Просто вырезать и сказать, что старые версии больше не поддерживаются.

Напоследок, хочу сказать вот что. Есть ещё одна вещь -- "контейнеры". Файл может содержать в себе десятки разнородных об'ектов, в том числе вложенных. Для логичности желательно тогда сделать загрузку каждого об'екта как отдельную функцию. Тогда в каждой функции должен быть свой оператор switch с номерами версий. С этим связан один подводный камень. Когда делаете новую версию, новый case должен быть добавлен во ВСЕ switch, даже если в об'ект изменений не вносилось. Будет так:

switch(ver)
{
case 171:
case 170:
case 169:// последние два обновления не внесли изменений в текущий объект
// тут уже идёт загрузки
...


Иначе считывание внутри этого оператора не начнётся, поскольку все метки не будут подходить. Подойдёт метка default (:.

Ну и, конечно, нельзя не сказать о бронебойном способе, заменяющим этот (и любой другой). Можно использовать XML.

@темы: Программирование, Статьи

URL
Комментарии
2012-06-21 в 09:48 

himself
Интересно, хотя уж лучше новые поля добавлять в конец, а свитч сделать: for(int i=0; i<117; i++) switch(i) { ... }. Оно и универсальней, т.к. фокус с проваливанием по вариантам работает даже не во всех C-подобных языках, чего уж говорить о других.

Когда делаете новую версию, новый case должен быть добавлен во ВСЕ switch, даже если в об'ект изменений не вносилось.
А вот это зря. Лучше писать версию каждого объекта в начале объекта. (Хотя схема с for помогает и тут)

   

Untitled

главная