这里声明一下,MongoDB中的Decorable机制跟OOP中的装饰器模式还是有区别的。装饰器模式的一个要点是类被装饰后暴露的接口不变,利用多态的特点增加代码逻辑。
1. 基本概念
在了解Decorable机制之前,我们先考虑以下两个问题:
- MongoDB中有许多组件,比如Metrics、VectorClock、Top。这些组件一般在某个层次是唯一的,比如VectorClock是全局唯一的,ReadConcern、WriteConcern是每个请求唯一的。
- 组件之间的调用关系非常复杂。
这种情况下,我们一般会使用单例模式,提供一个静态方法获取这个全局唯一的实例。但是单例模式的缺点很明显:
- 单例类需要自己控制自己的生命周期,违反了单一职责原则
- 代码之间的耦合变深,很难通过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. 构造和析构流程
整体流程如下所示:
- 在初始化阶段,使用 declareDecoration 函数声明装饰器,计算装饰器总内存大小,并确定每个装饰器在内存中的位置。
- 在运行时阶段,在构造主类对象(被装饰的对象)时,会在堆上分配内存空间并在指定位置构造装饰器对象。在析构主对象时则会析构装饰器并释放内存。