在C++的世界里构建一个序列化框架;并非一件困难的事情,但也并非简单。因此,需要分成两部分来完成这项任务:
1、序列化容器。
2、序列化方式。
前者,很容易理解;但也决定着我们将要存储数据的方式:二进制抑或其他。二进制方式,很容易想到和使用的方式;但也最容易以极不安全的方式去使用;因为,为了各种原因,在存储时我们极易丢掉原本的类型信息,使得一切都靠“人工约定”这种很不靠谱的方式。而其他方式,如文本,我们则可以相对地在其中保留很多信息;即使最后的成品并非是让人类来阅读的,但构建过程中,为了各种目的(如调试),总会加入各种信息的。
后者,则决定了该框架的可用性以及健壮性;在此有两种方式可选择:接口和全局重载函数。第一个,是完全面向对象的方式,也是以侵入式地决定了一个类型是否可以被序列化;看似完美,但对于已完善的不可更改的类型,是有着致命性的不足:无法被我们的框架所包容(例如基本类型)! 第二个,使用类似“operator<<”的cout方案;可以扩展式地支持所有类型,但,是的有“但是”,其在一定程度上破坏了“封装”,C++友元函数便是这一争论的中心。当然,这无关紧要。
一、容器
第一步,自然我们需要选择是使用固定的一个完善的类来完成这项工作;还是,使用相对可扩展的接口定义。前者,在我们的RPC中是很理想的方式:我们只需要二进制。而后者,则关系到整个序列化框架本身的可复用性——如果,我们想要支撑永久化对象(即:保存到文件和从文件恢复)呢?
所以,对此,只能使用接口:
class IBuffer{//没有写所必要的virtual ~IBufer(){} public: virtual void write(const byte* buffer, size_type length) = 0; virtual bool read(byte* buffer, size_type length) = 0; public: virtual size_type length()const = 0; virtual const byte* data()const{ return nullptr;} };
以上便是我们所需要的最基本的接口了。基本上和一个文件所提供的功能一致;需要说明的是“data()”函数,其既不是纯虚的,其也有一个不怎么安全的默认返回值:nullptr。很简单:其是为了兼容MemoryBuffer和FileBuffer而设定的!在MemoryBuffer的实现中我们需要重写该函数以传递二进制序列化后的结果;而FileBuffer则无视这一接口。而且,默认返回一个"nulllptr"也只有一个作用:该函数会返回【空指针】,请注意!
当然,这并非尽头;在使用该接口时,我们将会十分的难受!因为,没有可设置的偏移量接口;是的,我们需要偏移量,当需要完成更复杂的操作时。比如,我们抛个异常:
.... buffer->read(...); throw XXX;