TFS代码review

block标识

TFS每个block有一个唯一标识,目前的实现是一个uint32_t的整数id,每次新增一个block,就会为其分配新的id,具体实现方式是保存一个全局global_block_id的值,每次分配时就直接将这个值加1做为新的blockid;代码实现大致如下,每次需要分配时就调用generate函数。

class BlockIdFactory {
public:
   uint32_t generate()
   {
      return ++global_block_id;
   }

private:
   uint32_t global_block_id;  // 全局的ID会持久化保存,每次启动时先加载
}; 

最近由于Erasure code项目的需要,我们将blockid升至64bit,因为在代码中使用blockid时都是直接用uint32_t,而且使用的地方特别多,差不多每个源代码文件里都会涉及到。升级blockid为uint64_t类型,意味着我们要将所有使用blockid的地方都修改掉,定义时由uint32_t修改为uint64_t,序列化时(存储在磁盘或通过网络传输)要改为uint64_t的序列化接口等等,总之要修改的地方非常多。

之所以一个简单的类型修改会导致很大的工作量,是因为我们没有很好的隐藏blockid的实现细节,一开始就规定了blockid是一个uint32_t类型的整数,而实际上blockid应该可以是任意类型,数值、字符串类型、或是一个更复杂的结构体。上面的代码通过generate函数(而不是直接在代码中使用new_block_id = ++global_block_id这样的语句)隐藏了blockid的生成细节,如果生成策略改了,只需要修改generate的实现即可;但代码由于代码暴露了blockid的类型细节,还是导致其不易扩展。

如果将blockid的类型抽象为一个BlockidType,在最初实现时,由于其是一个uint32_t的整数,使用typedef将该类型定义为uint32_t,大致的实现如下,所有使用blockid的地方都是用BlockidType,而不是uint32_t,这时,如果需要将blockid升级为64bit,或者是改成字符串类型,我们只需要在BlockIdFactory这个类内部进行修改,所有的修改细节都不会暴露到BlockIdFactory以外的地方,外面看到的仍然是BlockidType类型的blockid。

 

typedef uint32_t BlockidType;
class BlockIdFactory {
public:
   BlockidType generate()
   {
      return ++global_block_id;
   }

private:
   uint32_t global_block_id;  
}; 

很多情况下,如果不能很直观的看出一个“对象”的类型,就应该将其定义为抽象数据类型,以方便扩展。比如数组的长度,一本书的页数,一个人的重量,这些能很直观的看出应该是数值类型,可直接根据实际情况使用uint32_t、uint64_t类型;而像本例里block标识,我们不能很直观的看出它究竟是什么,所以最初就应该将其设计为抽象类型。

消息序列化

TFS里所有需要在网络上传输的消息(客户端的请求消息、server的应答消息等),都会实现serialize/deserialize的接口,用于序列化/反序列化,每次增加一个新的消息,就要为这个消息写序列化接口,而这个基本上就是个机械工作,针对成员的类型,调用相应的序列化函数,代码大致如下。

class SomeMessage: public BaseMessage {
public:
   void serialize(DataBuffer& output)
   {
      output.write_int32(foo);
      output.write_int64(bar);
   }

   void deserialze(DataBuffer& input)
   {
      input.read_int32(&foo);
      input.read_int64(&bar);
   }

private:
   int32_t foo;
   int64_t bar;
}; 

可以看出序列化/反序列化的工作相当“无聊”,基本上都是重复的工作,完全可以通过更好的设计避免掉。曾经见过有人把这些重复的工作用宏代替,使得增加消息的工作量非常小,但大量使用宏终归不是个好的选择,影响代码的可读性,同时也不方便定位问题。使用google protobuf可以方便的解决问题,其编解码的效率以及空间利用率都是非常高的。使用protobuf,你只需要关注消息内容本身,序列化/反序列化的工作它会帮你搞定,要添加新消息、修改现有的消息都非常简单。开源的利器很多,很多开源产品的实现比自己去写相同作用代码的质量都要高的,借助一些成熟的开源产品会使得你要到做的事情更简单,使得你能更好的关注事情本身。

代码复用

有些懒人为了少写消息,将多个消息进行复用,在不同的场景下各个成员代表不同的含义,于是就出现了如下的代码。

class GeneralMessage:public BaseMessage {  
 public:  
    void serialize(DataBuffer& output);  
    void deserialze(DataBuffer& input);

private:  
    int32_t type;  
    int64_t value1;  
    int64_t value2;  
    int64_t value3;  
    int64_t value4;  
}; 

在实际应用时,每个请求对应一个type,在不同的type下,value1-value4代表不同的含义,比如在block在哪些server上时,value1代表blockid,而在查询server上有哪些block时,value1又代表serverid,服务器端收到消息后,根据type做不同的解释,4个成员以内的请求,基本上都可以复用这条消息,较少了很大部分的代码量,但极大的影响了代码的可读性,我至今都没有完全搞清楚,每种情况下各个value代表什么含义。其实,在上面思路的基础上,再稍加封装,就能既少写代码,又不影响代码的可读性,大致思路如下。

class GeneralMessage:public BaseMessage {  
public:  
    void serialize(DataBuffer& output);  
    void deserialze(DataBuffer& input);

protected:  
    int64_t value1;  
    int64_t value2;  
    int64_t value3;  
    int64_t value4;  
};

// 编解码消息直接继承,不用实现  
class SpecialMessage:public GeneralMessage {  
public:  
   void set_block_id(const uint64_t block_id)  
   {  
      value1 = block_id;  
   }

   uint64_t get_block_id() const  
   {  
      return value1;  
   }

   void set_server_id(const uint64_t server_id)  
   {  
      value2 = server_id;  
   }

   uint64_t get_server_id() const  
   {  
      return value2  
   }  
}; 

通过上面的封装,用户看到的是SpecialMessgae有两个重要的字段blockid,serverid,分别通过set/get来设置和获取,用户看不到实际这些数据存储在一个“命名不好”的变量里;阅读代码的人也能通过set/get接口,知道这是在设置哪些字段。封转可能带来一些额外的开销,但在绝大多数情况下,封装带来的益处会使得这些“开销”是可以忽略不计的,但也切勿“过度封装”。

posted @ 2013-04-19 14:14  ydzhang  阅读(533)  评论(0编辑  收藏  举报