技术分享(六)- leveldb源码阅读之设计模式

本文探讨了设计模式在leveldb中的运用,重点关注了迭代器和建造者模式。迭代器模式用于顺序访问聚合对象,简化数据结构的遍历,leveldb中的MemtableIterator和Block::Iter是其实现。建造者模式则将复杂对象的构建与表示分离,提高代码可维护性,TableBuilder在leveldb中用于构建sstable文件。这两种模式都提升了leveldb的灵活性和可扩展性。

前言

本文为针对leveldb使用的一些设计模式去谈谈自己的理解,里面挑选了迭代器模式与建造者模式,这两个较为常见的设计模式,进行简单谈谈。本文可能会有引用到一些文章的内容,如果有发现,请联系我这边在文末补全。

综述

谈到软件工程,总是绕不开设计模式的。当编写的工程量大的时候,总会在不经意间使用到了一些设计模式。

个人认为,设计模式是为旧代码重构、新代码编写提供一系列拥有较高可拓展性、可维护性、可复用的解决方案。尤其是在旧代码随着架构日渐复杂,愈加难维护而开始选择重构时,设计模式将可以提供不少成熟的解决方案。

了解设计模式也对阅读一些大型源码有很大的帮助,比如ROS topic的实现,在大致了解的它是什么的时候,基本就能推论出使用的是观察者模式,底层实现也就有了大致的思路。

本次将从leveldb中使用的两个设计模式作为切入点,进行谈谈理解,事实上,leveldb中还有使用到其他的设计模式,这边就不做一一分析,从设计模式的角度,去谈谈leveldb中的一些设计。

迭代器模式

简述

迭代器算是最常见的设计模式之一,在广大语言标准库、开源库等等经常能见到,比如C++ STL库的容器,Java的基本数据类型如map等都会有使用到。

vector<int> v;  
for (int n = 0; n<5; ++n)
    v.push_back(n); 

vector<int>::iterator i;  
for (i = v.begin(); i != v.end(); ++i) {  
    cout << *i << endl;  
}

其实不难发现,在构造复杂的数据类型时,提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。

优势:

  1. 访问一个聚合对象的内容而无须暴露它的内部表示。
  2. 遍历任务交由迭代器完成,这简化了聚合类。
  3. 它支持以不同方式遍历一个聚合,甚至可以自定义迭代器的子类以支持新的遍历。
  4. 增加新的聚合类和迭代器类都很方便,无须修改原有代码。
  5. 封装性良好,为遍历不同的聚合结构提供一个统一的接口。

缺点:

  1. 增加了类的个数,这在一定程度上增加了系统的复杂性

leveldb的应用

在leveldb中也有较多的应用,主要是为自己构建的一些数据类型实现迭代器,如:

  • MemtableIterator,每一个memtable分别实现自己的迭代器以导出memtable自己的kv排序流数据
  • Block::Iter,这个时sstable的

以下摘自leveldb源码中的Iterator接口基类,其中的key和value其实就是leveldb的一个定制化操作,因为整个数据库存储是基于key-value结构进行存储

class LEVELDB_EXPORT Iterator {
 public:
  Iterator();

  Iterator(const Iterator&) = delete;
  Iterator& operator=(const Iterator&) = delete;

  virtual ~Iterator();
  virtual bool Valid() const = 0;

  virtual void SeekToFirst() = 0;
  virtual void SeekToLast() = 0;

  virtual void Seek(const Slice& target) = 0;
  virtual void Next() = 0;
  virtual void Prev() = 0;

  virtual Slice key() const = 0;
  virtual Slice value() const = 0;
  virtual Status status() const = 0;
  using CleanupFunction = void (*)(void* arg1, void* arg2);
  void RegisterCleanup(CleanupFunction function, void* arg1, void* arg2);

 private:
  struct CleanupNode {
    bool IsEmpty() const { return function == nullptr; }
    void Run() {
      assert(function != nullptr);
      (*function)(arg1, arg2);
    }

    CleanupFunction function;
    void* arg1;
    void* arg2;
    CleanupNode* next;
  };
  CleanupNode cleanup_head_;
};

建造者模式

简述

建造者模式也是较为常见的设计模式,如果有进行过安卓APP开发的话,就会发现在做一些界面创建中会经常使用到各种builder类创建一个复杂对象,如new一个弹出框。

final AlertDialog.Builder normalDialog = 
            new AlertDialog.Builder(MainActivity.this);
normalDialog.setIcon(R.drawable.icon_dialog);
normalDialog.setTitle("删除");
normalDialog.setMessage("是否需要删除?");
normalDialog.setPositiveButton("确定", new ...);
normalDialog.setNegativeButton("关闭", new ...);
normalDialog.show();

定义:指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。

该模式的主要优点如下:

  1. 封装性好,构建和表示分离。
  2. 扩展性好,各个具体的建造者相互独立,有利于系统的解耦。
  3. 客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险。

其缺点如下:

  1. 产品的组成部分必须相同,这限制了其使用范围。
  2. 如果产品的内部变化复杂,如果产品内部发生变化,则建造者也要同步修改,后期维护成本较大。

建造者(Builder)模式和工厂模式的关注点不同:建造者模式注重零部件的组装过程,而工厂模式更注重零部件的创建过程,但两者可以结合使用。

举个栗子,我们工厂中组装机器人的过程,可以认为是“建造者模式”,我们选择了整个组装过程,最终组装成了机器人。而工厂模式,一个明显的就是选型,如雷达,选择EAI雷达还是国科雷达。

leveldb的应用

在leveldb中,以TableBuilder为例,感受一下leveldb怎么将复杂类进行拆解。

首先从功能性去分析,TableBuilder实际上就是sstable(sorted string table)文件生成器,一个简单的使用就是:

auto tb = new TableBuilder();
tb.Add(key1, val1);
tb.Add(key2, val2);
tb.Flush();
tb.Finish();

整个TableBuilder类实现就如下,它将核心的其实核心是下面的struct Rep这个内部类, 里面封装了整个TableBuilder的成员变量,也就是sstable的数据结构的实现。

class LEVELDB_EXPORT TableBuilder {
 public:
  TableBuilder(const Options& options, WritableFile* file);

  TableBuilder(const TableBuilder&) = delete;
  TableBuilder& operator=(const TableBuilder&) = delete;
  ~TableBuilder();

  Status ChangeOptions(const Options& options);

  void Add(const Slice& key, const Slice& value);
  void Flush();
  Status status() const;
  Status Finish();
  void Abandon();

  uint64_t NumEntries() const;
  uint64_t FileSize() const;

 private:
  bool ok() const { return status().ok(); }
  void WriteBlock(BlockBuilder* block, BlockHandle* handle);
  void WriteRawBlock(const Slice& data, CompressionType, BlockHandle* handle);

  struct Rep;
  Rep* rep_;
};

针对这个类的设计其实可以挖掘出一些挺有意思的编程思想,为什么将成员变量全部封装到Rep中?

  • TableBuilder作为外部接口,不必要公开数据的内部表示形式,隐藏不必要的细节的也是库开发的重要思路
  • 一个直观的感受,使得整个类不那么臃肿,降低耦合度,站在使用者的角度,不需要观察过多的细节
  • 在接口文件中生命,而源文件定义和使用,这样也使得库的升级,公共接口头文件没有变化,可以直接替换库文件的时候直接重新链接到新版本的库即可

事实上,这是库开发的一个常见用法,尤其在存在大量成员变量时,考虑封装也不失为一种很好的方式。

重新讲回Rep,以下为源码

struct TableBuilder::Rep {
  Rep(const Options& opt, WritableFile* f)
      : options(opt),
        index_block_options(opt),
        file(f),
        offset(0),
        data_block(&options),
        index_block(&index_block_options),
        num_entries(0),
        closed(false),
        filter_block(opt.filter_policy == nullptr
                         ? nullptr
                         : new FilterBlockBuilder(opt.filter_policy)),
        pending_index_entry(false) {
    index_block_options.block_restart_interval = 1;
  }

  Options options;
  Options index_block_options;
  WritableFile* file;
  uint64_t offset;
  Status status;
  BlockBuilder data_block;
  BlockBuilder index_block;
  std::string last_key;
  int64_t num_entries;
  bool closed;  
  FilterBlockBuilder* filter_block;

  bool pending_index_entry;
  BlockHandle pending_handle;  

  std::string compressed_output;
};

其实不难发现里面包含了BlockBuilder、FilterBlockBuilder这样的一个builder封装,整体组装成了一个大的TableBuilder。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值