searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

MongoDB 中的装饰器设计模式

2024-09-26 09:25:11
2
0

这里声明一下,MongoDB中的Decorable机制跟OOP中的装饰器模式还是有区别的。装饰器模式的一个要点是类被装饰后暴露的接口不变,利用多态的特点增加代码逻辑。

1. 基本概念

在了解Decorable机制之前,我们先考虑以下两个问题:

  1. MongoDB中有许多组件,比如Metrics、VectorClock、Top。这些组件一般在某个层次是唯一的,比如VectorClock是全局唯一的,ReadConcern、WriteConcern是每个请求唯一的。
  2. 组件之间的调用关系非常复杂。

这种情况下,我们一般会使用单例模式,提供一个静态方法获取这个全局唯一的实例。但是单例模式的缺点很明显:

  • 单例类需要自己控制自己的生命周期,违反了单一职责原则
  • 代码之间的耦合变深,很难通过fake或者mock的方法进行test
  • 相比于显式传递参数,给代码增加了隐形依赖

有一个更致命的缺点是:当一个组件不是全局唯一不太好办。而且单例模式加起来容易,去掉就难了。

所以MongoDB代码中提供了ServiceContext、OperationContext等概念,并且利用Decorable机制,来管理各个“挂件”。从上层代码来看使用方式如下,以ServiceContext为例:

// 声明AuthorizationManager附属于ServiceContext类
const auto getAuthorizationManager =
    ServiceContext::declareDecoration<std::unique_ptr<AuthorizationManager>>();

class AuthorizationManager {
public:
    // 定义一个静态方法,用来获取service中的AuthorizationManager实例
    static AuthorizationManager* get(ServiceContext* service){
        return getAuthorizationManager(service).get();
    };
}

// 通过静态方法,拿到serviceContext中的authzManager
AuthorizationManager* authzManager = AuthorizationManager::get(opCtx->getServiceContext());

2. 内部实现

一对多的关系下,一个比较简单的做法是使用composition(组合)来管理它们之间的关系。比如说逻辑时钟、认证管理、replication coordinator这些模块都变成ServiceContext的一个成员变量或方法。缺点就是每增删一个模块都要改动ServiceContext的代码,非常不方便。

那么Decorable是如何做的呢?它的定义如下,是一种非典型的 CRTP 的应用:

class ServiceContext final : public Decorable<ServiceContext> {
}

重点看这个Decorable类,它有一个嵌套类Decoration,两者是从属的关系——一个Decorable可以有若干个Decoration:

template <typename D>     // D是被“装饰”的对象,例如ServiceContext
class Decorable {         // 继承Decorable,使D变得可被装饰
public:
    template <typename T>     // T是“装饰品”,例如ServiceContext的认证管理组件
    class Decoration {       
    public:
        // 重载了双括号,用来从实例D中取出实例T,比如从一个ServiceContext实例中取出它的AuthorizationManager
        T& operator()(D& d) const {
            return static_cast<Decorable&>(d)._decorations.getDecoration(this->_raw);
        }

		// 获取实例T所属的D实例
        D* owner(T* const t) const {
            return static_cast<D*>(getOwnerImpl(t));
        }

    private:
        // 这里利用了对象的内存布局手动获取Decorable实例
        const Decorable* getOwnerImpl(const T* const t) const {
            return *reinterpret_cast<const Decorable* const*>(
                reinterpret_cast<const unsigned char* const>(t) - _raw._raw._index);
        }

        friend class Decorable;

        explicit Decoration(
            typename DecorationContainer<D>::template DecorationDescriptorWithType<T> raw)
            : _raw(std::move(raw)) {}

        typename DecorationContainer<D>::template DecorationDescriptorWithType<T> _raw;         // 此装饰品的位置/描述符
    };

    // 唯一的公开方法,用来为此Decorable声明/申请一个装饰品
    template <typename T>
    static Decoration<T> declareDecoration() {
        return Decoration<T>(getRegistry()->template declareDecoration<T>());
    }

protected:
    Decorable() : _decorations(this, getRegistry()) {}
    ~Decorable() = default;

private:
	// 根据模板类型D的不同,会生成不同的Decorable<D>类和getRegistry方法,每个getRegistry方法都会有一个不同类型的局部静态变量
    static DecorationRegistry<D>* getRegistry() {
        static DecorationRegistry<D>* theRegistry = new DecorationRegistry<D>();
        return theRegistry;
    }

    DecorationContainer<D> _decorations;    // 唯一的成员变量
};

declareDecoration静态方法是用来添加一个Decoration的外部接口,比如往ServiceContext中添加一个AuthorizationManager组件——ServiceContext::declareDecoration<std::unique_ptr<AuthorizationManager>>();。这样,之后new出来的ServiceContext实例都会有各自的AuthorizationManager。

以基础类为 D,需要给其装饰 T1 和 T2 为例。核心思路就是给每个 D 对象分配一段内存区域存放 T1 和 T2,并且负责管理 T1 和 T2 的生命周期。

主要涉及到以下数据结构:

  • Decorable​:在定义 D 时,需要继承 public Decorable,表示 D 是一个可装饰的类。代码的其他位置就可以使用 D::declareDecoration 和 D::declareDecoration 声明 T1 和 T2 是 D 的 挂件。每次调用 declareDecoration 函数会返回一个 Decoration 对象,这个对象中会记录 T1/T2 装饰在 D 的哪个位置(可以理解为一段堆内存中的偏移),通过不同的 Decoration 对象可以快速找到具体的挂件。
  • DecorationRegistry​:当 D 通过前面的步骤声明自己为 Decorable 时,就会实例化一个 DecorationRegistry 对象(static 的),这个对象会完成装饰器模式的主要控制逻辑:比如 declareDecoration 时,会根据 T1/T2 的大小进行对齐后,统计堆 buffer 的总大小,并给 T1/T2 分配具体的位置;在 D 进行构造时,会在指定的内存位置构造 T1/T2 对象;在 D 进行析构时,会调用 T1/T2 的析构函数。
  • DecorationContainer​:每个 Decorable 对象都会实例化一个 DecorationContainer 对象,这个对象中会保存 D 的所有挂件。DecorationContainer 本质上是一段堆内存(std::unique_ptr<unsigned char[]>),内存长度由 DecorationRegistry 提供,每当构建 D 对象时,会根据 DecorationRegistry 提供的长度信息进行内存分配。

3. 示例

以 util/decorable_test.cpp中的单元测试为例。

// 定义 主类/被装饰类 是可被装饰的
class MyDecorable : public Decorable<MyDecorable> {};

// 定义 挂件
static int numConstructedAs;   // A 被构造的总数
static int numDestructedAs;    // A 被析构的总数
class A {
public:
    A() : value(0) {
        ++numConstructedAs;
    }
    ~A() {
        ++numDestructedAs;
    }
    int value;
};

// 具体的执行逻辑
TEST(DecorableTest, DecorableType) {
    const auto dd1 = MyDecorable::declareDecoration<A>();  // 声明 A 是 MyDecorable 的挂件,并返回 A 在堆内存上的位置
    const auto dd2 = MyDecorable::declareDecoration<A>();  // 再次声明 A 是 MyDecorable 的挂件。并返回第 2 个 A 在堆内存上的位置
    const auto dd3 = MyDecorable::declareDecoration<int>();  // 声明 int 是 MyDecorable 的挂件,并返回 int 在堆内存上的位置
    numConstructedAs = 0;
    numDestructedAs = 0;
    {
        MyDecorable decorable1;  // 构造 MyDecorable 对象
        ASSERT_EQ(2, numConstructedAs);  // 构造了 2 个 A (在内存中顺序排列)
        ASSERT_EQ(0, numDestructedAs);
        MyDecorable decorable2;  // 构造另一个 MyDecorable 对象
        ASSERT_EQ(4, numConstructedAs);  // 构造了 2 个 A (在 decorable2 d的内存中顺序排列)
        ASSERT_EQ(0, numDestructedAs);

        ASSERT_EQ(0, dd1(decorable1).value);  // decorable1 的 第 1 个 A 对象的 value
        ASSERT_EQ(0, dd2(decorable1).value);  // decorable1 的 第 2 个 A 对象的 value
        ASSERT_EQ(0, dd1(decorable2).value);  // decorable2 的 第 1 个 A 对象的 value
        ASSERT_EQ(0, dd2(decorable2).value);  // decorable2 的 第 2 个 A 对象的 value
        ASSERT_EQ(0, dd3(decorable2));  // decorable2 的 int 对象
        dd1(decorable1).value = 1;  // 给上述对象赋值 
        dd2(decorable1).value = 2;
        dd1(decorable2).value = 3;
        dd2(decorable2).value = 4;
        dd3(decorable2) = 5;
        ASSERT_EQ(1, dd1(decorable1).value);  // 再次确认赋值后的对象
        ASSERT_EQ(2, dd2(decorable1).value);
        ASSERT_EQ(3, dd1(decorable2).value);
        ASSERT_EQ(4, dd2(decorable2).value);
        ASSERT_EQ(5, dd3(decorable2));
    }
    ASSERT_EQ(4, numDestructedAs);  // decorable1 和 decorable2 都被析构,每个对象都析构 2 个 A
}

4. 构造和析构流程

整体流程如下所示:

  1. 在初始化阶段,使用 declareDecoration 函数声明装饰器,计算装饰器总内存大小,并确定每个装饰器在内存中的位置。
  2. 在运行时阶段,在构造主类对象(被装饰的对象)时,会在堆上分配内存空间并在指定位置构造装饰器对象。在析构主对象时则会析构装饰器并释放内存。

363381016-ce2dd6fd-9b9f-40cb-859d-61d71855e38a.png

0条评论
0 / 1000
彭振翼
6文章数
1粉丝数
彭振翼
6 文章 | 1 粉丝
原创

MongoDB 中的装饰器设计模式

2024-09-26 09:25:11
2
0

这里声明一下,MongoDB中的Decorable机制跟OOP中的装饰器模式还是有区别的。装饰器模式的一个要点是类被装饰后暴露的接口不变,利用多态的特点增加代码逻辑。

1. 基本概念

在了解Decorable机制之前,我们先考虑以下两个问题:

  1. MongoDB中有许多组件,比如Metrics、VectorClock、Top。这些组件一般在某个层次是唯一的,比如VectorClock是全局唯一的,ReadConcern、WriteConcern是每个请求唯一的。
  2. 组件之间的调用关系非常复杂。

这种情况下,我们一般会使用单例模式,提供一个静态方法获取这个全局唯一的实例。但是单例模式的缺点很明显:

  • 单例类需要自己控制自己的生命周期,违反了单一职责原则
  • 代码之间的耦合变深,很难通过fake或者mock的方法进行test
  • 相比于显式传递参数,给代码增加了隐形依赖

有一个更致命的缺点是:当一个组件不是全局唯一不太好办。而且单例模式加起来容易,去掉就难了。

所以MongoDB代码中提供了ServiceContext、OperationContext等概念,并且利用Decorable机制,来管理各个“挂件”。从上层代码来看使用方式如下,以ServiceContext为例:

// 声明AuthorizationManager附属于ServiceContext类
const auto getAuthorizationManager =
    ServiceContext::declareDecoration<std::unique_ptr<AuthorizationManager>>();

class AuthorizationManager {
public:
    // 定义一个静态方法,用来获取service中的AuthorizationManager实例
    static AuthorizationManager* get(ServiceContext* service){
        return getAuthorizationManager(service).get();
    };
}

// 通过静态方法,拿到serviceContext中的authzManager
AuthorizationManager* authzManager = AuthorizationManager::get(opCtx->getServiceContext());

2. 内部实现

一对多的关系下,一个比较简单的做法是使用composition(组合)来管理它们之间的关系。比如说逻辑时钟、认证管理、replication coordinator这些模块都变成ServiceContext的一个成员变量或方法。缺点就是每增删一个模块都要改动ServiceContext的代码,非常不方便。

那么Decorable是如何做的呢?它的定义如下,是一种非典型的 CRTP 的应用:

class ServiceContext final : public Decorable<ServiceContext> {
}

重点看这个Decorable类,它有一个嵌套类Decoration,两者是从属的关系——一个Decorable可以有若干个Decoration:

template <typename D>     // D是被“装饰”的对象,例如ServiceContext
class Decorable {         // 继承Decorable,使D变得可被装饰
public:
    template <typename T>     // T是“装饰品”,例如ServiceContext的认证管理组件
    class Decoration {       
    public:
        // 重载了双括号,用来从实例D中取出实例T,比如从一个ServiceContext实例中取出它的AuthorizationManager
        T& operator()(D& d) const {
            return static_cast<Decorable&>(d)._decorations.getDecoration(this->_raw);
        }

		// 获取实例T所属的D实例
        D* owner(T* const t) const {
            return static_cast<D*>(getOwnerImpl(t));
        }

    private:
        // 这里利用了对象的内存布局手动获取Decorable实例
        const Decorable* getOwnerImpl(const T* const t) const {
            return *reinterpret_cast<const Decorable* const*>(
                reinterpret_cast<const unsigned char* const>(t) - _raw._raw._index);
        }

        friend class Decorable;

        explicit Decoration(
            typename DecorationContainer<D>::template DecorationDescriptorWithType<T> raw)
            : _raw(std::move(raw)) {}

        typename DecorationContainer<D>::template DecorationDescriptorWithType<T> _raw;         // 此装饰品的位置/描述符
    };

    // 唯一的公开方法,用来为此Decorable声明/申请一个装饰品
    template <typename T>
    static Decoration<T> declareDecoration() {
        return Decoration<T>(getRegistry()->template declareDecoration<T>());
    }

protected:
    Decorable() : _decorations(this, getRegistry()) {}
    ~Decorable() = default;

private:
	// 根据模板类型D的不同,会生成不同的Decorable<D>类和getRegistry方法,每个getRegistry方法都会有一个不同类型的局部静态变量
    static DecorationRegistry<D>* getRegistry() {
        static DecorationRegistry<D>* theRegistry = new DecorationRegistry<D>();
        return theRegistry;
    }

    DecorationContainer<D> _decorations;    // 唯一的成员变量
};

declareDecoration静态方法是用来添加一个Decoration的外部接口,比如往ServiceContext中添加一个AuthorizationManager组件——ServiceContext::declareDecoration<std::unique_ptr<AuthorizationManager>>();。这样,之后new出来的ServiceContext实例都会有各自的AuthorizationManager。

以基础类为 D,需要给其装饰 T1 和 T2 为例。核心思路就是给每个 D 对象分配一段内存区域存放 T1 和 T2,并且负责管理 T1 和 T2 的生命周期。

主要涉及到以下数据结构:

  • Decorable​:在定义 D 时,需要继承 public Decorable,表示 D 是一个可装饰的类。代码的其他位置就可以使用 D::declareDecoration 和 D::declareDecoration 声明 T1 和 T2 是 D 的 挂件。每次调用 declareDecoration 函数会返回一个 Decoration 对象,这个对象中会记录 T1/T2 装饰在 D 的哪个位置(可以理解为一段堆内存中的偏移),通过不同的 Decoration 对象可以快速找到具体的挂件。
  • DecorationRegistry​:当 D 通过前面的步骤声明自己为 Decorable 时,就会实例化一个 DecorationRegistry 对象(static 的),这个对象会完成装饰器模式的主要控制逻辑:比如 declareDecoration 时,会根据 T1/T2 的大小进行对齐后,统计堆 buffer 的总大小,并给 T1/T2 分配具体的位置;在 D 进行构造时,会在指定的内存位置构造 T1/T2 对象;在 D 进行析构时,会调用 T1/T2 的析构函数。
  • DecorationContainer​:每个 Decorable 对象都会实例化一个 DecorationContainer 对象,这个对象中会保存 D 的所有挂件。DecorationContainer 本质上是一段堆内存(std::unique_ptr<unsigned char[]>),内存长度由 DecorationRegistry 提供,每当构建 D 对象时,会根据 DecorationRegistry 提供的长度信息进行内存分配。

3. 示例

以 util/decorable_test.cpp中的单元测试为例。

// 定义 主类/被装饰类 是可被装饰的
class MyDecorable : public Decorable<MyDecorable> {};

// 定义 挂件
static int numConstructedAs;   // A 被构造的总数
static int numDestructedAs;    // A 被析构的总数
class A {
public:
    A() : value(0) {
        ++numConstructedAs;
    }
    ~A() {
        ++numDestructedAs;
    }
    int value;
};

// 具体的执行逻辑
TEST(DecorableTest, DecorableType) {
    const auto dd1 = MyDecorable::declareDecoration<A>();  // 声明 A 是 MyDecorable 的挂件,并返回 A 在堆内存上的位置
    const auto dd2 = MyDecorable::declareDecoration<A>();  // 再次声明 A 是 MyDecorable 的挂件。并返回第 2 个 A 在堆内存上的位置
    const auto dd3 = MyDecorable::declareDecoration<int>();  // 声明 int 是 MyDecorable 的挂件,并返回 int 在堆内存上的位置
    numConstructedAs = 0;
    numDestructedAs = 0;
    {
        MyDecorable decorable1;  // 构造 MyDecorable 对象
        ASSERT_EQ(2, numConstructedAs);  // 构造了 2 个 A (在内存中顺序排列)
        ASSERT_EQ(0, numDestructedAs);
        MyDecorable decorable2;  // 构造另一个 MyDecorable 对象
        ASSERT_EQ(4, numConstructedAs);  // 构造了 2 个 A (在 decorable2 d的内存中顺序排列)
        ASSERT_EQ(0, numDestructedAs);

        ASSERT_EQ(0, dd1(decorable1).value);  // decorable1 的 第 1 个 A 对象的 value
        ASSERT_EQ(0, dd2(decorable1).value);  // decorable1 的 第 2 个 A 对象的 value
        ASSERT_EQ(0, dd1(decorable2).value);  // decorable2 的 第 1 个 A 对象的 value
        ASSERT_EQ(0, dd2(decorable2).value);  // decorable2 的 第 2 个 A 对象的 value
        ASSERT_EQ(0, dd3(decorable2));  // decorable2 的 int 对象
        dd1(decorable1).value = 1;  // 给上述对象赋值 
        dd2(decorable1).value = 2;
        dd1(decorable2).value = 3;
        dd2(decorable2).value = 4;
        dd3(decorable2) = 5;
        ASSERT_EQ(1, dd1(decorable1).value);  // 再次确认赋值后的对象
        ASSERT_EQ(2, dd2(decorable1).value);
        ASSERT_EQ(3, dd1(decorable2).value);
        ASSERT_EQ(4, dd2(decorable2).value);
        ASSERT_EQ(5, dd3(decorable2));
    }
    ASSERT_EQ(4, numDestructedAs);  // decorable1 和 decorable2 都被析构,每个对象都析构 2 个 A
}

4. 构造和析构流程

整体流程如下所示:

  1. 在初始化阶段,使用 declareDecoration 函数声明装饰器,计算装饰器总内存大小,并确定每个装饰器在内存中的位置。
  2. 在运行时阶段,在构造主类对象(被装饰的对象)时,会在堆上分配内存空间并在指定位置构造装饰器对象。在析构主对象时则会析构装饰器并释放内存。

363381016-ce2dd6fd-9b9f-40cb-859d-61d71855e38a.png

文章来自个人专栏
MongoDB
6 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
1
1