Написание класса / структуры, которая часто меняется - PullRequest
4 голосов
/ 02 сентября 2010

Резюме:
У меня есть структура, которая читается / записывается в файл.
Эта структура часто изменяется, и это приводит к тому, что моя read() функция становится сложной.

Мне нужно найти хороший способ справиться с изменениями, сохраняя при этом низкое количество ошибок. Оптимально, код должен облегчать обнаружение изменений между версиями.

Я продумал пару шаблонов, но не знаю, прошел ли я все возможные варианты.

Как вы увидите, код был в основном похож на C, но я нахожусь в процессе превращения его в C++.


Детали
Как я уже сказал, моя структура часто меняется (почти в каждой версии программы).

  • Некоторые участники удаляются, некоторые добавляются, некоторые становятся более сложными. Это не простой случай, когда в структуре появляется новый член.

До настоящего времени изменения в структуре обрабатывались следующим образом:

  • in version_1 , я использовал таблицу цветовых карт:

struct Obj {
    int color_index;  
};  

void Read_Obj( File *f, Obj *o ) {
    f->read( f, &o->color_index );
}

void Write_Obj( File *f, Obj *o ) {
    f->write( f, o->color_index );
}
  • в следующей версии , я изменил ее в [r, g, b] форму

struct Obj {
    int color_r;
    int color_g;
    int color_b;  
};  

void Read_Obj( File *f, Obj *o ) {

    if( f->version() == File::Version1 ) {
        int color_index;
        f->read( f, &color_index );
        ColorIndex_to_RGB( o, color_index ); // we used color maps back then
    }
    else {
        f->read( f, &o->color_r );
        f->read( f, &o->color_g );
        f->read( f, &o->color_b );
    }
}      

void Write_Obj( File *f, Obj *o ) {
    f->write( f, o->color_r );
    f->write( f, o->color_g );
    f->write( f, o->color_b );
}

[краткая заметка]

Обратите внимание, что я мог бы использовать


void Read_Obj( File *f, Obj *o ) {

    if( f->version() == File::Version1 ) {
        Read_Obj_V1( f, o );
    }
    else {
        Read_Obj_V2( f, o );
    }
}      

но это имеет тенденцию к дублированию кода между каждой из подфункций, поскольку в реальной жизни только 1-2 из ~ 20 членов структуры изменяются в каждой версии. Итак, остальные 18 строк остаются прежними.

Конечно, я могу изменить эту политику, если на то есть веская причина

[конец краткой заметки]


Теперь эти структуры стали сложными, и мне нужно преобразовать их в класс и работать более объектно-ориентированным образом.

Я видел шаблон, в котором вы используете один класс для чтения для каждой старой версии, а затем преобразуете данные в более новый класс.


class Obj_v1 {
    int m_color_index;
    read( File *f ) {
        f->read( f, &m_color_index );
   }

   void convert_to( Obj * ) { /* code to convert the older object */  }
};

class Obj {
    int m_r;
    int m_g;
    int m_b;
    read( File *f ) {
        f->read( f, &m_r );
        f->read( f, &m_g );
        f->read( f, &m_b );
   }

};

void Read_Obj( File *f, Obj *o ) {

    if( f.version() == File::Version1 ) {
        Obj_v1 old();
        old.read( f );
        old.convert_to( o );
    }
    else {
        o.read( f );
    }
}      

void Write_Obj( File *f, Obj *o ) {
    o->write( f );
}

Однако есть две стратегии борьбы с изменениями:

Стратегия 1 : прямые преобразования

void Read_Obj( File *f, Obj *o ) {

    if( f->version() == File::Version1 ) {
        Obj_v1 old();
        old.read( f );
        old.convert_to( o );
    }
    else if( f->version() == File::Version2 ) {
        Obj_v2 old();
        old.read( f );
        old.convert_to( o );
    }
    else {
        o.read( f );
    }
}      

Неудобство:

  • Это означает, что вам необходимо обновлять convert_to() из всех Obj_vX классов каждый раз, когда вы меняете класс Obj. Слишком много возможностей для ошибок, выбрасываемых в каждый раз.

Преимущество:

  • Вы всегда можете приспособить старую концепцию (структуру) к новой - сравните с каскадной стратегией (следующая), где некоторая информация может быть потеряна по пути, поэтому ее нельзя использовать.

Стратегия 2 : каскадные преобразования

void Read_Obj( File *f, Obj *o ) {

    Obj_v1 o1();
    Obj_v2 o2();

    if( f->version() == File::Version1 ) {
        o1.read( f );
        o1.convert_to( o2 );
        o2.convert_to( o );
    }
    else if( f->version() == File::Version2 ) {
        o2.read( f );
        o2.convert_to( o );
    }
    else {
        o.read( f );
    }
}      

Недостатки:

  • В v1 может существовать некоторая информация, которая была бесполезна в v3, но v5 может использовать ее; однако каскадные преобразования уничтожили эти данные.

  • В старых версиях для создания объектов требуется больше времени.

Преимущество:

  • Вам нужно писать только один convert_to() каждый раз, когда вы меняете класс Obj. Однако одна ошибка в одном из преобразователей в линии может иметь более серьезные последствия и может нарушить целостность базы данных. У вас есть больше шансов найти такую ​​ошибку.

Обеспокоенность:

  • Может ли быть так, что при преобразовании после преобразования вы получаете слишком много шума в объектах более старых версий, что они ошибаются?

Вопрос:

  • Есть ли другие модели, которые лучше справляются с этим?

  • Те из вас, кто имел некоторый опыт работы с моими предложениями, что вы думаете о моих опасениях по поводу вышеуказанных реализаций?

  • Какие решения предпочтительнее?

Большое спасибо

Ответы [ 2 ]

3 голосов
/ 02 сентября 2010

void Read_Obj (Файл * f, Obj * o) {
if (f-> version () == File :: Version1) {

if - это, так сказать, скрытый переключатель / чехол. И переключатель / регистр в C ++, как правило, взаимозаменяемы с полиморфизмом . Пример:

struct Reader {
   virtual void Read_Obj( File *f, Obj *o ) = 0;
   /* methods to read further objects */
}

struct ReaderV1 : public Reader {
   void Read_Obj( File *f, Obj *o ) { /* ... */ };
   /* methods to read further objects */
}

struct ReaderV2 : public Reader {
   void Read_Obj( File *f, Obj *o ) { /* ... */ };
   /* methods to read further objects */
}

И затем создать экземпляр соответствующего потомка Reader после открытия файла и определения номера версии. Таким образом, вы будете иметь только одну проверку версии файла в коде верхнего уровня вместо того, чтобы загрязнять весь код низкого уровня проверками.

Если код является общим для версии файла, для удобства вы также можете поместить его в базовый класс считывателя.

Я бы настоятельно рекомендовал не использовать вариант с class Obj_v1 и class Obj, где метод read() относится к самому Obj. Таким образом, можно легко получить круговые зависимости, а также плохая идея, чтобы объект осознал свое постоянное представление. IME (по моему опыту) лучше, чтобы за это отвечала иерархия сторонних читателей. (Как в std::iostream против std::string против operator <<: поток не знает строку, строка не знает поток, только оператор << знает оба.)

В остальном я лично не вижу большой разницы между вашей "Стратегией 1" и "Стратегией 2". Они оба используют convert_to(), что я лично считаю поверхностным. Вместо этого следует использовать решение IME с полиморфизмом - автоматическое преобразование всего в актуальную версию объекта class Obj, без промежуточных class Obj_v1 и class Obj_v2. Поскольку при полиморфизме у вас будет отдельная функция чтения для каждой версии, обеспечить правильное воссоздание объекта из прочитанной информации очень просто.

Есть ли другие модели, которые лучше справляются с этим? Те из вас, кто имел некоторый опыт работы с моими предложениями, что вы думаете о моих опасениях по поводу вышеуказанных реализаций? Какие предпочтительные решения?

Это именно то, к чему должен был стремиться полиморфизм, и как я обычно выполняю такие задачи самостоятельно.

Это связано с сериализацией объекта , но я не видел ни одной инфраструктуры сериализации (моя информация, вероятно, устарела), которая могла бы поддерживать несколько версий одного класса.

Я лично несколько раз получал следующую иерархию классов сериализации / десериализации:

  • интерфейс абстрактного ридера (очень тонкий по определению)
  • служебные классы, реализующие чтение и запись реальных объектов из / в потоки (толстый, многократно используемый код, также использовался для сетевых передач)
  • версионные реализации интерфейса считывателя (относительно тонкие, многократно использовать классы утилит)
  • интерфейс / класс модуля записи (я всегда писал актуальную версию файла. Версионирование использовалось только во время чтения.)

Надеюсь, это поможет.

2 голосов
/ 02 сентября 2010

Вы можете использовать буфер протокола Google для работы.

Основная идея, стоящая за пределами protobuf, состоит в том, чтобы декоррелировать фактическую сериализацию из информации о классе, потому что вы создаете класс, предназначенный для сериализации ... нореальная выгода лежит в другом месте.

Информация, закодированная с помощью protobuf, естественно совместима как с обратной, так и с прямой совместимостью, поэтому, если вы добавите информацию и расшифруете старый файл: новой информации там не будет.С другой стороны, если вы удалите информацию, она пропустит ее во время декодирования.

Это означает, что вы оставляете обработку версии на protobuf (без какого-либо реального номера версии на самом деле), а затем при изменении класса:

  • вы перестаете получать информацию, которая вам больше не нужна
  • вы добавляете новые поля для новых фрагментов информации, которыми вы располагаете

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...