要点:
我有一个读/写文件的结构
此结构经常更改,这会导致我的read()
函数变得复杂。
我需要找到一种处理变更的好方法,同时保持低错误数。 最理想的是,代码应该能够让人们轻松发现版本之间的变化。
我想过几种模式,但我不知道我是否已经完成了所有可能的选择。
正如您将看到的,代码主要位于C
- 就像,但我正在将其转换为C++
。
详情
正如我所说,我的结构经常更改(几乎在每个版本的程序中)。
到目前为止,对结构的更改已经处理如下:
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 );
}
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_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
。但是,该行中某个转换器中的一个错误可能会产生更严重的影响,并可能破坏数据库的一致性。但是,你增加了发现这种错误的机会。 忧虑:
问题:
还有其他模式可以做得更好吗?
那些对我的建议有一定经验的人,您如何看待我对上述实施的担忧?
哪个是更好的解决方案?
非常感谢你
答案 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_v1
和class Obj
的变体,其中read()
方法属于Obj
本身。这样就可以轻松地结束循环依赖,并且让对象意识到它的持久表示也是一个坏主意。 IME(根据我的经验)最好让第三方读者类层次结构对此负责。 (与std::iostream
与std::string
与operator <<
的对比:stream不知道字符串,字符串不知道流,只有opeartor <<
知道两者。)
否则,我个人认为您的“策略1”和“策略2”之间没有任何重大差异。他们都使用我个人认为肤浅的convert_to()
。应该使用具有多态性的IME解决方案 - 自动将所有内容转换为对象class Obj
的最新版本,而不使用中间class Obj_v1
和class Obj_v2
。由于具有多态性,因此您可以为每个版本提供专用的读取功能,从而确保从读取信息中进行适当的对象重新创建。
还有其他模式可以做得更好吗? 那些对我的建议有一定经验的人,您如何看待我对上述实施的担忧? 哪个是更好的解决方案?
这正是多态性旨在解决的问题,以及我自己一般如何执行此类任务。
这与object serialization有关,但我没有看到一个序列化框架(我的信息可能过时),它能够支持同一类的多个版本。
我个人最终使用以下序列化/反序列化类层次结束了几次:
希望有所帮助。
答案 1 :(得分:2)
您可以使用Google协议缓冲区。
除了protobuf之外的主要想法是从类信息中去除实际的序列化,因为你创建了一个专门用于序列化的类......但真正的好处在于其他地方。
protobuf编码的信息自然都是向后兼容的,所以如果你添加信息并解码旧文件,那么你就不会有新信息。另一方面,如果您删除信息,它将在解码过程中跳过它。
这意味着您将版本处理保留为protobuf(实际上没有任何实际版本号),然后在更改您的类时:
它也可以帮助你更好地思考保存的内容和格式,可以在保存(编码)之前转换数据并在读取(解码)时将其转换回来,因此保存的实际格式应该是变化频率较低(您可以添加项目,但不必过于频繁地重构已编码的数据)。