А сегодня, дорогие друзья, я расскажу вам о методе, позволяющем встраивать в программу поддержку файлов данных сразу нескольких версий, не обременяя её дублированием кода для загрузки каждой отдельной версии
(читать дальше)
Итак, у нас есть прикладная программа, которая должна уметь сохранять и загружать файлы со своими собственными данными ("документами") разных версий. Предполагается, что программа активно развивается, поэтому версии файла появляются достаточно часто. В программе которая не доделывается на ходу такого быть просто не может, однако случай, когда программа делалась не по ТЗ и разработка совмещена с эксплуатацией вполне возможен. Например, если программа подключена к оборудованию, которое ещё не отлажено, но уже используется.
В таких случаях выпуск новой версии программы в которой изменён формат файла (такая может выпускаться хоть каждую неделю (% ), если ей пользуется хотя бы десяток человек, плодит некоторое количество файлов данных подходящих для этой версии. В итоге получается полная каша из файлов разных версий. Программа должна уметь их все читать, и, возможно, сохранять в заданную версию. Как это сделать? Как предусмотреть расширяемость формата?
Давным-давно был изобретён простой метод, при сохранении в файле оставляют "дыры" -- поля с пометкой "зарезервировано". Такие зарезервированные дыры до сих пор встречаются в заголовках файлов и даже в параметрах функций (это вообще сурово). Но никогда не знаешь, что придётся сохранить. Вдруг у программы появится дополнительная функция, обслуживание которой в файлах данных будет занимать (о боже!) целых четыре килобайта? Такие большие дыры, конечно, делать нельзя. Можно версии файлов делать просто независимо. Тогда по общему заголовку будет определяться версия файла, а загрузка будет идти отдельной функцией. Я видел такую реализацию.
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![;)](http://static.diary.ru/picture/1136.gif)
// и продолжит считывание первой версии
// а если версия первая, поля для второй будут пропущены и сразу будет загружаться первая.
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.
21.06.2012 в 09:48
Когда делаете новую версию, новый case должен быть добавлен во ВСЕ switch, даже если в об'ект изменений не вносилось.
А вот это зря. Лучше писать версию каждого объекта в начале объекта. (Хотя схема с for помогает и тут)