More documentation and occasional rearrangements made as an aside of debugging issues around handling of Lumiera iterators vs. STL iterators...
459 lines
18 KiB
C++
459 lines
18 KiB
C++
/*
|
||
DEPEND-INJECT.hpp - managing the lifecycle of singletons and dependencies
|
||
|
||
Copyright (C)
|
||
2018, Hermann Vosseler <Ichthyostega@web.de>
|
||
|
||
**Lumiera** is free software; you can redistribute it and/or modify it
|
||
under the terms of the GNU General Public License as published by the
|
||
Free Software Foundation; either version 2 of the License, or (at your
|
||
option) any later version. See the file COPYING for further details.
|
||
|
||
*/
|
||
|
||
|
||
/** @file depend-inject.hpp
|
||
** Per type specific configuration of instances created as service dependencies.
|
||
** This is the _"Backstage Area"_ of lib::Depend, where the actual form and details
|
||
** of instance creation can be configured in various ways. Client code typically
|
||
** plants an instance of lib::Depend, templated to the actual type of the dependency.
|
||
** This is a way to express the dependency on some interface or service, while not
|
||
** expanding on any details as to when and how this dependency is created. Without
|
||
** an explicit configuration, lib::Depend will automatically create and manage a
|
||
** singleton instance of the type given as type parameter.
|
||
**
|
||
** # Architecture
|
||
** A _dependency_ is understood as something we need to perform the task at hand,
|
||
** yet a dependency referres beyond that task and relates to concerns outside the
|
||
** scope and theme of this actual task. The usage site of the dependency is only
|
||
** bound and coupled to the _interface_ exposed as dependency, and the associated
|
||
** _contract_ of a service. Initially, such a dependency is _dormant_ and will
|
||
** be activated on first access. This simplifies the bootstrap of complexly
|
||
** interwoven structures; it suffices to ensure that none of the participating
|
||
** entities actually starts its work before all of the setup and wiring is done.
|
||
**
|
||
** For that reason, lib::DependInject<SRV> is meant to be used at the site providing
|
||
** the _actual_ service or implementation subtype -- not at the site consuming a
|
||
** dependency (through lib::Depend<SRV>). This choice also means that the actual
|
||
** configuration of services is not centralised, and can not be static; it need
|
||
** to happen prior to any service access (on violation error::Logic is raised)
|
||
**
|
||
** # Configuration
|
||
** The public configuration mechanisms offered by DependInject address various concerns:
|
||
**
|
||
** ## Wiring
|
||
** Even when relying on lazy on-demand initialisation, a concrete service implementation
|
||
** typically needs to connect to further services, and maybe even decide upon the actual
|
||
** subclass to be instantiated. By invoking the DependInject<SRV>::useSingleton(FUN)
|
||
** function, a functor or lambda can be installed into the static factory of lib::Depend.
|
||
** Especially, a lambda could be bound into the internal context of the service provider.
|
||
** This function is expected to deliver a heap allocated instance on invocation, which
|
||
** will be owned and managed by lib::Depend<SRV>::factory (A DependencyFactory<SRV>).
|
||
**
|
||
** ## Lifecycle
|
||
** Whenever a module or subsystem can be started and stopped, several interconnected
|
||
** services become operational together, with dependent lifecycle. It is possible to
|
||
** expose such services through a lib::Depend<SRV> front-end; this way, the actual
|
||
** usage context remains agnostic with respect to details of the lifecycle. Any
|
||
** access while the service is _not_ available will just raise an error::Logic.
|
||
** This kind of configuration can be achieved by planting a smart-handle of type
|
||
** DependInject<SRV>::ServiceInstance<IMP>
|
||
**
|
||
** ## Unit Testing with Mock Services
|
||
** Dependencies tend to hamper unit testing, but global variables and actively
|
||
** linked and collaborating implementations are worse and often prevent test coverage
|
||
** altogether. Preferably dependencies are limited to an interface and a focused topic,
|
||
** and then it might be possible to inject a _mock implementation_ locally within the
|
||
** unit test. Such a mock instance temporarily shadows any previously existing state,
|
||
** instance or configuration for this dependency; the mock object can be rigged and
|
||
** instrumented by the test code to probe or observe the subject's behaviour. This
|
||
** concept can only work when the test subject does not cache any state and really
|
||
** pulls the dependency whenever necessary.
|
||
**
|
||
**
|
||
** ## Performance, concurrency and sanity
|
||
**
|
||
** The lib::Depend<SRV> front-end is optimised for the access path. It uses an
|
||
** std::atomic to hold the instance pointer and a class-level Mutex to protect
|
||
** the initialisation phase. On the other hand, initialisation happens only once
|
||
** and will be expensive anyway. And, most importantly, for any non-standard
|
||
** configuration we assume, that -- by architecture -- there is _no contention_
|
||
** between usage and configuration. Services are to be started in a dedicated bootstrap
|
||
** phase, and unit tests operated within a controlled, single threaded environment.
|
||
** For this reason, any configuration grabs the lock, and publishes via the default
|
||
** memory order of std::atomic (which is std::memory_order_seq_cst). Any spotted
|
||
** collision or inconsistency raises an exception, which typically should not
|
||
** be absorbed, but rather trigger component, subsystem or application shutdown.
|
||
** @warning there is a known coherency breach in "emergency shutdown": When a
|
||
** subsystem collapses unexpectedly, its root handler signals the subsystem
|
||
** runner to initiate emergency shutdown. However, the collapsed subsystem
|
||
** is already defunct at that point, which breaks the general contract of
|
||
** prerequisite subsystems to be available in operative mode. Lumiera
|
||
** is not built as a resilient service in that respect, but we also
|
||
** mandate for any parts not to cache essential work results in
|
||
** transient memory; actions are logged and performed for
|
||
** persistent history and UNDO.
|
||
**
|
||
** @see DependencyConfiguration_test
|
||
** @see SingletonSubclass_test
|
||
** @see subsys.hpp
|
||
*/
|
||
|
||
|
||
|
||
|
||
#ifndef LIB_DEPEND_INJECT_H
|
||
#define LIB_DEPEND_INJECT_H
|
||
|
||
|
||
|
||
#include "lib/error.hpp"
|
||
#include "lib/nocopy.hpp"
|
||
#include "lib/depend.hpp"
|
||
#include "lib/meta/trait.hpp"
|
||
#include "lib/meta/function.hpp"
|
||
#include "lib/sync-classlock.hpp"
|
||
|
||
#include <type_traits>
|
||
#include <utility>
|
||
#include <memory>
|
||
|
||
|
||
namespace lib {
|
||
namespace error = lumiera::error;
|
||
|
||
using std::forward;
|
||
using std::move;
|
||
|
||
|
||
|
||
|
||
/**
|
||
* This framework allows to (re)configure the lib::Depend front-end for dependency-injection.
|
||
* By default, `Depend<TY>` will create a singleton instance of `TY` lazily, on demand.
|
||
* When instantiating one of the configuration handles provided here -- _prior_ to anyone
|
||
* retrieving the instance through `Depend<TY>` -- this default (singleton) behaviour
|
||
* can be reconfigured in various ways, without the client being aware of it:
|
||
* - instead of a singleton, a service instance with well defined lifecycle can be
|
||
* exposed through the `Depend<TY>` front-end. When the service is shut down,
|
||
* clients will receive an exception on access.
|
||
* - instead of the interface type `TY` mentioned in `Depend<TY>`, an implementation subclass
|
||
* can be specified, optionally with a closure for the actual creation of this subclass
|
||
* singleton type, which still happens lazily, on demand
|
||
* - the current state and configuration can be shadowed temporarily by a test mock instance,
|
||
* which is managed automatically and removed when leaving the scope of the test.
|
||
* @note DependInject<SRV> is declared fried by Depend<SRV> to reconfigure the latter's internals
|
||
*/
|
||
template<class SRV>
|
||
class DependInject
|
||
: util::NoInstance
|
||
{
|
||
using Factory = Depend<SRV>::Factory;
|
||
using Lock = Depend<SRV>::Lock;
|
||
|
||
public:
|
||
/** configure dependency-injection for type SRV to build a subclass singleton.
|
||
* @note actually a delegation to `Depend<SUB>` is installed into `Depend<SRV>`
|
||
* @tparam SUB concrete subclass type to build on demand when invoking `Depend<SRV>`
|
||
* @throws error::Logic (LUMIERA_ERROR_LIFECYCLE) when the default factory has already
|
||
* been invoked at the point when calling this (re)configuration function.
|
||
*/
|
||
template<class SUB>
|
||
static void
|
||
useSingleton()
|
||
{
|
||
__assert_compatible<SUB>();
|
||
installFactory<SUB>();
|
||
}
|
||
|
||
/** configure dependency-injection for type SRV to manage a subclass singleton,
|
||
* which is created lazily on demand by invoking the given builder function.
|
||
* @note actual Subclass type is determined from the given functor and then
|
||
* a delegation to `Depend<Subclass>` is installed into `Depend<SRV>`
|
||
* @param ctor functor to create a heap allocated instance of the subclass
|
||
* @throws error::Logic (LUMIERA_ERROR_LIFECYCLE) when the default factory has already
|
||
* been invoked at the point when calling this (re)configuration function.
|
||
*/
|
||
template<class FUN>
|
||
static void
|
||
useSingleton (FUN&& ctor)
|
||
{
|
||
using Sub = SubclassFactoryType<FUN>::Subclass;
|
||
using Fun = SubclassFactoryType<FUN>::Functor;
|
||
|
||
__assert_compatible<Sub>();
|
||
installFactory<Sub,Fun> (forward<FUN> (ctor));
|
||
}
|
||
|
||
|
||
/**
|
||
* Configuration handle to expose a service implementation through the `Depend<SRV>` front-end.
|
||
* This non-copyable (but movable) handle shall be planted within the context operating the service
|
||
* to be exposed. It will immediately create (in RAII style) and manage a heap-allocated instance
|
||
* of the subclass `IMP` and expose a baseclass pointer to this specific instance through `Depend<SRV>`.
|
||
* Moreover, the implementation subclass can be accessed through this handle, which acts as smart-ptr.
|
||
* When the handle goes out of scope, the implementation instance is destroyed and the access through
|
||
* `Depend<SRV>` is closed and inhibited, to prevent on-demand creation of a baseclass `SRV` singleton.
|
||
* @tparam IMP concrete service implementation subclass to build, manage and expose.
|
||
* @throws error::Logic (LUMIERA_ERROR_LIFECYCLE) when the default factory has already
|
||
* been invoked at the point when calling this (re)configuration function.
|
||
*/
|
||
template<class IMP =SRV>
|
||
class ServiceInstance
|
||
: util::MoveOnly
|
||
{
|
||
std::unique_ptr<IMP> instance_;
|
||
|
||
public:
|
||
template<typename...ARGS>
|
||
ServiceInstance(ARGS&& ...ctorArgs)
|
||
: instance_{new IMP(forward<ARGS> (ctorArgs)...)}
|
||
{
|
||
__assert_compatible<IMP>();
|
||
activateServiceAccess (*instance_);
|
||
}
|
||
|
||
enum StartMode { NOT_YET_STARTED };
|
||
|
||
/** create in deactivated state. Can be [activated](\ref #createInstance) later */
|
||
ServiceInstance(StartMode)
|
||
: instance_{}
|
||
{
|
||
__assert_compatible<IMP>();
|
||
}
|
||
|
||
~ServiceInstance()
|
||
{
|
||
shutdown();
|
||
}
|
||
|
||
template<typename...ARGS>
|
||
IMP&
|
||
createInstance(ARGS&& ...ctorArgs)
|
||
{
|
||
instance_.reset (new IMP(forward<ARGS> (ctorArgs)...));
|
||
activateServiceAccess (*instance_);
|
||
return *instance_;
|
||
}
|
||
|
||
void
|
||
shutdown() noexcept
|
||
{
|
||
if (instance_)
|
||
{
|
||
deactivateServiceAccess();
|
||
instance_.reset();
|
||
}
|
||
}
|
||
|
||
|
||
explicit
|
||
operator bool() const
|
||
{
|
||
return bool(instance_);
|
||
}
|
||
|
||
IMP&
|
||
operator* () const
|
||
{
|
||
ENSURE (instance_);
|
||
return *instance_;
|
||
}
|
||
|
||
IMP*
|
||
operator->() const
|
||
{
|
||
ENSURE (instance_);
|
||
return instance_.get();
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Configuration handle for temporarily shadowing a dependency by a test mock instance.
|
||
* This non-copyable (but movable) handle shall be planted within the immediate test context.
|
||
* It immediately stashes away the existing state and configuration from `Depend<SRV>`, but
|
||
* waits for actual invocation of the `Depend<SRV>`-front-end to create a heap-allocated
|
||
* instance of the `MOC` subclass, which it manages and exposes like a smart-ptr.
|
||
* When the handle goes out of scope, the original state and configuration is restored
|
||
*/
|
||
template<class MOC =SRV>
|
||
class Local
|
||
: util::MoveOnly
|
||
{
|
||
std::unique_ptr<MOC> mock_;
|
||
|
||
SRV* origInstance_;
|
||
Factory origFactory_;
|
||
|
||
public:
|
||
Local()
|
||
: Local([]{ return new MOC{}; })
|
||
{ }
|
||
|
||
template<class FUN>
|
||
explicit
|
||
Local (FUN&& _buildInstance)
|
||
{
|
||
__assert_compatible<MOC>();
|
||
__assert_compatible<typename SubclassFactoryType<FUN>::Subclass>();
|
||
|
||
temporarilyInstallAlternateFactory (origInstance_, origFactory_
|
||
,[this, buildInstance=forward<FUN> (_buildInstance)]
|
||
{
|
||
mock_.reset (buildInstance());
|
||
return mock_.get();
|
||
});
|
||
}
|
||
~Local()
|
||
{
|
||
restoreOriginalFactory (origInstance_, move(origFactory_));
|
||
}
|
||
|
||
explicit
|
||
operator bool() const
|
||
{
|
||
return bool(mock_);
|
||
}
|
||
|
||
/** trigger lazy service object instantiation */
|
||
MOC&
|
||
triggerCreate()
|
||
{
|
||
Depend<SRV>{}.operator()();
|
||
ENSURE (mock_);
|
||
return *mock_;
|
||
}
|
||
|
||
MOC&
|
||
operator* () const
|
||
{
|
||
REQUIRE (mock_);
|
||
return *mock_;
|
||
}
|
||
|
||
MOC*
|
||
operator-> () const
|
||
{
|
||
REQUIRE (mock_);
|
||
return mock_.get();
|
||
}
|
||
};
|
||
|
||
|
||
|
||
protected: /* ======= internal access-API for those configurations to manipulate Depend<SRV> ======= */
|
||
template<class IMP>
|
||
friend class ServiceInstance;
|
||
template<class MOC>
|
||
friend class Local;
|
||
|
||
|
||
template<class SUB>
|
||
static void
|
||
__assert_compatible()
|
||
{
|
||
static_assert (meta::is_Subclass<SUB,SRV>()
|
||
,"Installed implementation class must be compatible to the interface.");
|
||
}
|
||
|
||
static void
|
||
__ensure_pristine()
|
||
{
|
||
if (Depend<SRV>::instance)
|
||
throw error::Logic("Attempt to reconfigure dependency injection after the fact. "
|
||
"The previously installed factory (typically Singleton) was already used."
|
||
, error::LUMIERA_ERROR_LIFECYCLE);
|
||
}
|
||
|
||
template<typename FUN>
|
||
struct SubclassFactoryType
|
||
{
|
||
static_assert (meta::_Fun<FUN>(),
|
||
"Need a Lambda or Function object to create a heap allocated instance");
|
||
|
||
using Functor = meta::_Fun<FUN>::Functor; // suitable type to store for later invocation
|
||
using ResultVal = meta::_Fun<FUN>::Ret;
|
||
using Subclass = meta::Strip<ResultVal>::TypePlain;
|
||
|
||
static_assert (std::is_pointer_v<ResultVal>,
|
||
"Function must yield a pointer to a heap allocated instance");
|
||
};
|
||
|
||
|
||
template<class SUB, typename FUN>
|
||
static void
|
||
installFactory (FUN&& ctor)
|
||
{
|
||
Lock guard;
|
||
if (std::is_same<SRV,SUB>())
|
||
{
|
||
__ensure_pristine();
|
||
Depend<SRV>::factory().defineCreatorAndManage (forward<FUN> (ctor));
|
||
}
|
||
else
|
||
{
|
||
__ensure_pristine();
|
||
Depend<SRV>::factory().defineCreator ([]{ return & Depend<SUB>{}(); });
|
||
DependInject<SUB>::useSingleton (forward<FUN> (ctor));
|
||
} // delegate actual instance creation to Depend<SUB>
|
||
}
|
||
|
||
template<class SUB>
|
||
static void
|
||
installFactory ()
|
||
{
|
||
if (not std::is_same<SRV,SUB>())
|
||
{
|
||
Lock guard;
|
||
__ensure_pristine();
|
||
Depend<SRV>::factory().defineCreator ([]{ return & Depend<SUB>{}(); });
|
||
}
|
||
// note: we do not install an actual factory; rather we use the default for SUB
|
||
}
|
||
|
||
|
||
template<typename FUN>
|
||
static void
|
||
temporarilyInstallAlternateFactory (SRV*& stashInstance, Factory& stashFac, FUN&& newFac)
|
||
{
|
||
Lock guard;
|
||
stashFac.transferDefinition (move (Depend<SRV>::factory()));
|
||
stashInstance = Depend<SRV>::instance;
|
||
Depend<SRV>::factory().defineCreator (forward<FUN>(newFac));
|
||
Depend<SRV>::instance = nullptr;
|
||
}
|
||
|
||
static void
|
||
restoreOriginalFactory (SRV*& stashInstance, Factory&& stashFac)
|
||
{
|
||
Lock guard;
|
||
Depend<SRV>::factory().transferDefinition (move (stashFac));
|
||
Depend<SRV>::instance = stashInstance;
|
||
}
|
||
|
||
static void
|
||
activateServiceAccess (SRV& newInstance)
|
||
{
|
||
Lock guard;
|
||
if (Depend<SRV>::instance)
|
||
throw error::Logic("Attempt to activate an external service implementation, "
|
||
"but another instance has already been dependency-injected."
|
||
, error::LUMIERA_ERROR_LIFECYCLE);
|
||
Depend<SRV>::instance = &newInstance;
|
||
Depend<SRV>::factory().disable();
|
||
}
|
||
|
||
static void
|
||
deactivateServiceAccess()
|
||
{
|
||
Lock guard;
|
||
Depend<SRV>::instance = nullptr;
|
||
Depend<SRV>::factory().disable();
|
||
}
|
||
};
|
||
|
||
|
||
|
||
} // namespace lib
|
||
#endif /*LIB_DEPEND_INJECT_H*/
|