编写经常更改的类/结构

时间:2010-09-02 16:33:16

标签: c++ c design-patterns serialization oop

要点:
我有一个读/写文件的结构 此结构经常更改,这会导致我的read()函数变得复杂。

我需要找到一种处理变更的好方法,同时保持低错误数。 最理想的是,代码应该能够让人们轻松发现版本之间的变化。

我想过几种模式,但我不知道我是否已经完成了所有可能的选择。

正如您将看到的,代码主要位于C - 就像,但我正在将其转换为C++


详情
正如我所说,我的结构经常更改(几乎在每个版本的程序中)。

  • 删除了一些成员,添加了一些成员,一些成员变得更加复杂。这不是一个新成员出现结构的简单情况。

到目前为止,对结构的更改已经处理如下:

  • 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个结构会发生变化。所以,其他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_vXObj。每次抛出错误的可能性太多。

好处:

  • 您始终能够将旧概念(结构)与新概念相结合 - 与级联策略(下一步)进行比较,其中某些信息可能会在此过程中丢失,因此无法使用。

策略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 个答案:

答案 0 :(得分:3)

  

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

if就是说隐藏的开关/案例。 C ++中的switch / case通常可以与polymorphism互换。例如:

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_v1class Obj的变体,其中read()方法属于Obj本身。这样就可以轻松地结束循环依赖,并且让对象意识到它的持久表示也是一个坏主意。 IME(根据我的经验)最好让第三方读者类层次结构对此负责。 (与std::iostreamstd::stringoperator <<的对比:stream不知道字符串,字符串不知道流,只有opeartor <<知道两者。)

否则,我个人认为您的“策略1”和“策略2”之间没有任何重大差异。他们都使用我个人认为肤浅的convert_to()。应该使用具有多态性的IME解决方案 - 自动将所有内容转换为对象class Obj的最新版本,而不使用中间class Obj_v1class Obj_v2。由于具有多态性,因此您可以为每个版本提供专用的读取功能,从而确保从读取信息中进行适当的对象重新创建。

  

还有其他模式可以做得更好吗?   那些对我的建议有一定经验的人,您如何看待我对上述实施的担忧?   哪个是更好的解决方案?

这正是多态性旨在解决的问题,以及我自己一般如何执行此类任务。

这与object serialization有关,但我没有看到一个序列化框架(我的信息可能过时),它能够支持同一类的多个版本。

我个人最终使用以下序列化/反序列化类层次结束了几次:

  • 抽象阅读器界面(根据定义非常苗条)
  • 实现从/向流读取和写入实际对象的实用程序类(胖,高度可重用的代码,也用于网络传输)
  • 阅读器界面的版本化实现(相对较小,重用胖实用程序类)
  • 编写器接口/类(我总是在写文件的最新版本。版本控制仅在阅读时使用。)

希望有所帮助。

答案 1 :(得分:2)

您可以使用Google协议缓冲区。

除了protobuf之外的主要想法是从类信息中去除实际的序列化,因为你创建了一个专门用于序列化的类......但真正的好处在于其他地方。

protobuf编码的信息自然都是向后兼容的,所以如果你添加信息并解码旧文件,那么你就不会有新信息。另一方面,如果您删除信息,它将在解码过程中跳过它。

这意味着您将版本处理保留为protobuf(实际上没有任何实际版本号),然后在更改您的类时:

  • 您停止检索不再需要的信息
  • 您为新信息添加新字段

它也可以帮助你更好地思考保存的内容和格式,可以在保存(编码)之前转换数据并在读取(解码)时将其转换回来,因此保存的实际格式应该是变化频率较低(您可以添加项目,但不必过于频繁地重构已编码的数据)。