Lumiera  0.pre.03
»edityourfreedom«
depend.hpp File Reference

Go to the source code of this file.

Description

Singleton services and Dependency Injection.

The Singleton Pattern provides a single access point to a class or service and exploits this ubiquitous access point to limit the number of objects of this type to a single shared instance. Within Lumiera, we mostly employ a factory template for this purpose; the intention is to use on-demand initialisation and a standardised lifecycle. In the default configuration, this Depend<TY> factory maintains a singleton instance of type TY. The possibility to install other factory functions allows for subclass creation and various other kinds of service management.

Why Singletons? Inversion-of-Control and Dependency Injection

Singletons are frequently over-used, and often they serve as disguised global variables to support a procedural programming style. As a remedy, typically the use of a »Dependency Injection Container« is promoted. And – again typically – these DI containers tend to evolve into heavyweight universal tools and substitute the original problem by metadata hell.

Thus, for Lumiera, the choice to use Singletons was deliberate: we understand the Inversion-of-Control principle, yet we want to stay just below the level of building a central application manager core. At the usage site, we access a factory for some service by name, where the »name« is actually the type name of an interface or facade. Singleton is used as an implementation of this factory, when the service is self-contained and can be brought up lazily.

Conventions, Lifecycle and Unit Testing

Usually we place an instance of the singleton factory (or some other kind of factory) as a static variable within the interface class describing the service or facade. As a rule, everything accessible as Singleton is sufficiently self-contained to come up any time – even prior to main(). But at shutdown, any deregistration must be done explicitly using a lifecycle hook. In Lumiera, destructors aren't allowed to do any significant work beyond releasing references, and we acknowledge that singletons can be released in arbitrary order.

Lifecycle and management of dependencies is beyond the scope of this access mechanism exposed here. However, the actual product to be created or exposed lazily can be configured behind the scenes, as long as this configuration is performed prior to the first access. This configuration is achieved with the help of the "sibling" template lib::DependInject, which is declared friend within Depend<T> for type T

  • a service with distinct lifecycle can be exposed through the Depend<T> front-end
  • it is possible to create a mock instance, which temporarily shadows what Depend<T> delivers on access.

Implementation and performance

Due to this option for flexible configuration, the implementation can not be built as Meyer's Singleton. Rather, Double Checked Locking of a Mutex is combined with an std::atomic to work around the known (rather theoretical) concurrency problems. Microbenchmarks indicate that this implementation technique ranges close to the speed of a direct access to an already existing object; in the fully optimised variant it was found to be roughly at ≈ 1ns and thus about 3 to 4 times slower than the comparable unprotected direct access without lazy initialisation. This is orders of magnitude better than any flavour of conventional locking.

Initialisation of the shared factory

We package the creation and destruction functors for the object to be managed into a factory, which is shared per type. Such a shared factory could live within a static member field Depend<TY>::factory – however, the definition of such a templated static member happens on first use of the enclosing template instance, and it seems the initialisation order of such fields is not guaranteed, especially when used prior to main, from static initialisation code. For that reason, we manage the factory as Meyer's singleton, so it can be accessed independently from the actual target object's lifecycle and the compiler will ensure initialisation prior to first use. To ensure the lifespan of this embedded factory object extends beyond the last instance of lib::Depend<TY>, we also need to access that factory from a ctor

See also
depend-inject.hpp
lib::DependInject
Singleton_test
DependencyConfiguration_test

Definition in file depend.hpp.

#include "lib/error.hpp"
#include "lib/nocopy.hpp"
#include "lib/nobug-init.hpp"
#include "lib/sync-classlock.hpp"
#include "lib/zombie-check.hpp"
#include "lib/meta/util.hpp"
#include <type_traits>
#include <functional>
#include <atomic>
#include <memory>

Classes

struct  DependencyFactory< OBJ >::canDefaultConstruct< X >
 metafunction: can we instantiate the desired object here? More...
 
class  Depend< SRV >
 Access point to singletons and other kinds of dependencies designated by type. More...
 
class  DependencyFactory< OBJ >
 Helper to abstract creation and lifecycle of a dependency. More...
 
class  DependInject< SRV >
 This framework allows to (re)configure the lib::Depend front-end for dependency-injection. More...
 

Namespaces

 lib
 Implementation namespace for support and library code.