A Decoration is an object that is stored as part of another object, called the Decorable.
The lifetime of the Decoration is tied to the lifetime of the Decorable. This means the Decoration is constructed when the Decorable's constructor is called, and the Decoration is destructed when the Decorable's destructor is called.
Finally, the decoration can only be accessed through the decorable.
The Decorator pattern is an easy way to add parts to an existing object without changing the object.
We'll start with an example of a Decorable
object and a Decoration
on it.
Then we'll explain the internals of the Decorable
class.
MongoDB's codebase implements the decorator pattern through its own Decorable
class.
For this example, we'll use
- the
Client
class as an example of aDecorable
object - the
ClusterLastErrorInfo
class as aDecoration
onClient
Any class that extends Decorable
can have decorations added to it. For example, Client
extends Decorable
:
/**
* The database's concept of an outside "client".
* */
class Client final : public Decorable<Client> {
public:
The Decorable
class has a static declareDecoration()
method that can be used to declare other classes as decorations on the Decorable
:
template <typename T>
static Decoration<T> declareDecoration();
For example, the ClusterLastErrorInfo
class is declared as a decoration on Client
by calling Client::declareDecoration<ClusterLastErrorInfo>
:
class ClusterLastErrorInfo {
public:
static const Client::Decoration<ClusterLastErrorInfo> get;
cluster_last_error_info.cpp:38-39
const Client::Decoration<ClusterLastErrorInfo> ClusterLastErrorInfo::get =
Client::declareDecoration<ClusterLastErrorInfo>();
Note, get
is not a function! (The variable get
could have been more aptly named something like 'decorationHandle').
This static "handle" can be used to obtain an actual instance of a Decoration
by passing it a particular instance of the Decorable
, via Decoration
's parenthesis operator:
class Decoration { // the "handle" class
public:
T& operator()(D& d) const;
Let's look at an example.
This parenthesis operator is used on the ClusterLastErrorInfo::get
variable in mongos's getLastError
command.
A particular Client
(obtained through cc()
), is passed as an argument to the parenthesis operator:
cluster_get_last_error_cmd.cpp:244
const HostOpTimeMap hostOpTimes(ClusterLastErrorInfo::get(cc()).getPrevHostOpTimes());
The particular ClusterLastErrorInfo
associated with that particular Client
is obtained, and getPrevHostOpTimes() is called on it.
Now we'll look at how Decorable
objects in MongoDB store, construct, and destruct their decorations.
This will explain the two methods we glossed over above, Decorable::declareDecoration()
and Decoration<T>::operator()(D& d)
.
Each instance of a Decorable
has a DecorationContainer
that has the actual storage space for its decorations (for example, remember that each Client
has its own ClusterLastErrorInfo
).
Each Decorable
also has a static DecorationRegistry
that assigns an index into the storage space for each decoration that is declared through Decorable::declareDecoration()
.
The registry and indices are static, because a particular Decoration
always has the same index in the list of decorations on a particular Decorable
. (That is, ClusterLastErrorInfo
always has the same index in the storage space on a Client
).
Each Decorable
has a private DecorationContainer
:
DecorationContainer _decorations;
The DecorationContainer
's field _decorationData
contains the storage space:
decoration_container.h:126-127
const std::unique_ptr<unsigned char[]> _decorationData;
And DecorationContainer
has methods to return a Decoration
by its index into the storage space:
void* getDecoration(DecorationDescriptor descriptor) {
return _decorationData.get() + descriptor._index;
}
Each Decorable
also has a static DecorationRegistry
for assigning the indices:
static DecorationRegistry* getRegistry() {
static DecorationRegistry* theRegistry = new DecorationRegistry();
return theRegistry;
}
In the DecorationRegistry
, each decoration's index, along with wrappers around its constructor and destructor, is stored in a DecorationInfo
struct:
struct DecorationInfo {
DecorationInfo() {}
DecorationInfo(DecorationContainer::DecorationDescriptor descriptor,
DecorationConstructorFn constructor,
DecorationDestructorFn destructor);
DecorationContainer::DecorationDescriptor descriptor;
DecorationConstructorFn constructor;
DecorationDestructorFn destructor;
};
The wrapper methods are straightforward; they just allow the decoration to be constructed at a specific location, so that they can be constructed in the storage space:
template <typename T>
static void constructAt(void* location) {
new (location) T();
}
Let's now look at Decorable::declareDecoration()
more closely:
template <typename T>
static Decoration<T> declareDecoration() {
return Decoration<T>(getRegistry()->declareDecoration<T>());
}
It calls the DecorationRegistry
's declareDecoration()
method.
This method describes the size of the decoration and passes the wrapper methods:
template <typename T>
DecorationContainer::DecorationDescriptorWithType<T> declareDecoration() {
MONGO_STATIC_ASSERT_MSG(std::is_nothrow_destructible<T>::value,
"Decorations must be nothrow destructible");
return DecorationContainer::DecorationDescriptorWithType<T>(std::move(declareDecoration(
sizeof(T), std::alignment_of<T>::value, &constructAt<T>, &destructAt<T>)));
}
The DecorationRegistry
's private helper method for declareDecoration()
takes those size requirements and assigns the decoration its index in the storage space:
DecorationContainer::DecorationDescriptor DecorationRegistry::declareDecoration(
const size_t sizeBytes,
const size_t alignBytes,
const DecorationConstructorFn constructor,
const DecorationDestructorFn destructor) {
const size_t misalignment = _totalSizeBytes % alignBytes;
if (misalignment) {
_totalSizeBytes += alignBytes - misalignment;
}
DecorationContainer::DecorationDescriptor result(_totalSizeBytes);
_decorationInfo.push_back(DecorationInfo(result, constructor, destructor));
_totalSizeBytes += sizeBytes;
return result;
}
So, when Client::declareDecoration<ClusterLastErrorInfo>()
is called (at program startup time), an index for ClusterLastErrorInfo
is assigned into the storage space of all Client
's.
The index is returned and stored into the static const member variable get
of ClusterLastErrorInfo
.
Let's now look more closely at the second method we glossed over, T& Decoration::operator()(D& d) const
:
class Decoration {
public:
T& operator()(D& d) const {
return static_cast<Decorable&>(d)._decorations.getDecoration(_raw);
}
...
DecorationContainer::DecorationDescriptorWithType<T> _raw;
The index returned by declareDecoration()
(e.g., what was stored in ClusterLastErrorInfo::get
) was of type Decoration
, so it has this parenthesis operator.
This parenthesis operator takes a particular instance of the Decorable
, reaches into the Decorable
's storage space _decorations
, and using its stored assigned index (in _raw
), pulls out the particular decoration for the particular Decorable
.
Notice that declaring the decoration does not actually construct an instance of the decoration.
This is because all of the decorations are constructed in one go, through the DecorationRegistry::construct()
method, by iterating over all the DecorationInfo
's and calling their constructors:
void DecorationRegistry::construct(DecorationContainer* decorable) const {
auto iter = _decorationInfo.cbegin();
try {
for (; iter != _decorationInfo.cend(); ++iter) {
iter->constructor(decorable->getDecoration(iter->descriptor));
}
} catch (...) {
try {
while (iter != _decorationInfo.cbegin()) {
--iter;
iter->destructor(decorable->getDecoration(iter->descriptor));
}
} catch (...) {
std::terminate();
}
throw;
}
}
This overarching DecorationRegistry::construct()
method is called by the DecorationContainer
's constructor, which passes itself as the argument:
decoration_container.cpp:37-41
DecorationContainer::DecorationContainer(const DecorationRegistry* registry)
: _registry(registry),
_decorationData(new unsigned char[registry->getDecorationBufferSizeBytes()]) {
_registry->construct(this);
}
This is how the decorations get constructed in the Decorable
's storage space.
So, when a Client
is constructed, its DecorationContainer
's constructor is called, which calls DecorationRegistry::construct()
, which calls the constructors of all the decorations that have been declared.
(If you declare a primitive as a decoration, it will be initialized to the zero/false/null value, since the decorations go through C++ value initialization).
@drusong , glad it was helpful. Thanks for commenting.