DI: document the reworked Singleton / Dependency-Factory
- polish the text in the TiddlyWiki
- integrate some new pages in the published documentation
Still mostly placeholder text with some indications
- fill in the relevant sections in the overview document
- adjust, expand and update the Doxygen comments
TODO: could convert the TiddlyWiki page to Asciidoc and
publish it mostly as-is. Especially the nice benchmarks
from yesterday :-D
This commit is contained in:
parent
7a250ca9e5
commit
942bad5d0a
11 changed files with 393 additions and 104 deletions
51
doc/design/architecture/Subsystems.txt
Normal file
51
doc/design/architecture/Subsystems.txt
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
Layers -- Subsystems -- Lifecycle
|
||||
=================================
|
||||
:Author: Hermann Voßeler
|
||||
:Email: <Ichthyostega@web.de>
|
||||
:Date: 2018
|
||||
|
||||
WARNING: under construction [red]#TO BE WRITTEN#
|
||||
|
||||
|
||||
Terminology
|
||||
-----------
|
||||
|
||||
Layer::
|
||||
A conceptual realm within the application to group related concerns and define an ordering.
|
||||
Layers are above/below each other and may depend _solely_ on lower layers. The Application
|
||||
may be operated in a partial layer configuration with only some lower layers. Each layer
|
||||
deals with distinct topics and has its own style. In Lumiera, we distinguish three layers
|
||||
+
|
||||
* User Interface -> Interaction
|
||||
* Proc-Layer -> Processing
|
||||
* Backend -> Data manipulation
|
||||
|
||||
Subsystem::
|
||||
A runtime entity which acts as anchor point and framework to maintain a well defined lifecycle.
|
||||
While layers are conceptual realms, subsystems can actually be started and stopped and their
|
||||
dependencies are represented as data structure. A subsystem typically starts one or several
|
||||
primary components, which might spawn a dedicated thread and instantiate further components
|
||||
and services.
|
||||
|
||||
Service::
|
||||
A component within some subsystem is termed _Service_
|
||||
+
|
||||
--
|
||||
* when it exposes an interfaces with an associated contract (informal rules about usage pattern)
|
||||
* when it accepts invocations from arbitrary other components without specific wiring hard coded knowledge
|
||||
--
|
||||
+
|
||||
The service lifecycle is tied to the lifecycle of the related subsystem; whenever the subsystem is ``up and running'',
|
||||
any contained serivces can be accessed and used. Within Lumiera, there is no _service broker_ or any similar kind
|
||||
of elaborate _service discovery_ -- rather, services are accessed *by name*, where ``name'' is the _type name_
|
||||
of the service interface.
|
||||
|
||||
Dependency::
|
||||
A relation at implementation level and thus a local property of an individual component. A dependency is something
|
||||
we need to complete the task at hand, yet a dependency lies beyond that task and relates to concerns outside the scope
|
||||
and theme of this actual task. Wich means, a dependency is not brought in by the task or part of the task, rather
|
||||
the task is the reason why some entity dealing with that task needs to _pull_ the dependency, in order to be able
|
||||
to handle the task. So essentially, dependencies are accessed on-demand. Dependencies might be other components
|
||||
or services, and typically the user (consumer) of a dependency just relies on the corresponding interface and
|
||||
remains agnostic with respect to the dependencie's actual implementation or lifecycle details.
|
||||
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
Design Documents: Lumiera Architecture
|
||||
======================================
|
||||
|
||||
// Menu : sort children
|
||||
// Menu : prepend child Subsystems
|
||||
// Menu : prepend child ArchitectureSummary
|
||||
|
||||
// Menu : attach child TimeQuant after TimeUsage
|
||||
|
||||
This section will feature some rather high-level considerations regarding
|
||||
the architecture of the application.
|
||||
|
||||
|
|
|
|||
41
doc/technical/library/Dependencies.txt
Normal file
41
doc/technical/library/Dependencies.txt
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
Singletons and Dependency Handling
|
||||
==================================
|
||||
:Date: 2018
|
||||
:Toc:
|
||||
|
||||
WARNING: [red]#under construction# +
|
||||
There is a rather complete page ``DependencyFactory'' in the TiddlyWiki,
|
||||
which should be integrated here
|
||||
|
||||
We encounter _dependencies as an issue at implementation level:_ In order to deal with some task at hand,
|
||||
sometimes we need to arrange matters way beyond the scope of that task. We could just thoughtlessly reach out and
|
||||
settle those extraneous concerns -- yet this kind of pragmatism has a price tag: we are now mutually dependent
|
||||
with internals of some other part of the system we do not even care much about. A more prudent choice would be
|
||||
to let ``that other part'' provide a service for us, focussed to what we actually need right here to get _our_
|
||||
work done. In essence, we create a dependency to resolve issues of coupling and to reduce complexity
|
||||
(»divide et impera«).
|
||||
|
||||
Unfortunately this solution created a new problem: how do we get at our dependencies? We can not just step ahead
|
||||
and create them or manage them, because then we'd be ``back to square one''. Rather someone else has to care.
|
||||
Someone _needs to connect us with those dependencies,_ so we can use them. This is a special meta-service known
|
||||
as _Dependency Injection_. A dedicated part of the application wires all other components, so each component
|
||||
can focus on its specific concern and abstract away everything else. Dependency Injection can be seen as
|
||||
application of the principle »Inversion Of Control«: each part is sovereign within its own realm, but becomes
|
||||
a client (asks for help) for anything beyond that.
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
_tbw_
|
||||
|
||||
Configuration
|
||||
~~~~~~~~~~~~~
|
||||
_tbw_
|
||||
|
||||
Performance
|
||||
~~~~~~~~~~~
|
||||
_tbw_
|
||||
|
||||
Architecture
|
||||
------------
|
||||
|
||||
|
|
@ -401,11 +401,37 @@ Common Services
|
|||
|
||||
Subsystem runner
|
||||
~~~~~~~~~~~~~~~~
|
||||
_tbw_
|
||||
Conceptually, the application is structured into layers. However, as actual runtime
|
||||
entities we rely on a small number of link:{ldoc}/design/architecture/Subsystems.html[Subsystems],
|
||||
which can be started and stopped and have well defined dependencies:
|
||||
|
||||
- Engine
|
||||
- Session and Builder
|
||||
- Play and Render Subsystem
|
||||
- Lumiera GUI
|
||||
- Script runner [yellow-background small]#planned#
|
||||
- Renderfarm node [yellow-background small]#planned#
|
||||
|
||||
Lumiera's `main()` function defines an _Application Object_, which in turn operates
|
||||
the _Subsystem Runner_. Each Subsystem implements a descriptor with the necessary
|
||||
hooks callbacks to be started and stopped when necessary.
|
||||
|
||||
Lifecycle events
|
||||
~~~~~~~~~~~~~~~~
|
||||
_tbw_
|
||||
Beyond parsing the commandline and triggering start of the subsystems, the Application
|
||||
Object also issues _Lifecycle Events_. Individual components can register a callback
|
||||
to be invoked when this happens. Currently (2018) the following events are known
|
||||
(see 'include/lifecycle.h'):
|
||||
|
||||
- ON_BASIC_INIT
|
||||
- ON_GLOBAL_INIT
|
||||
- ON_GLOBAL_SHUTDOWN
|
||||
- ON_EMERGENCY_EXIT
|
||||
|
||||
-> see here for
|
||||
link:{ldoc}/design/application/SubsystemLifecycle.html#_initialisation_and_lifecycle[explanation in detail]
|
||||
[small]#(somewhat preliminary and messy, yet still valid as of [yellow-background]##2018##)#
|
||||
|
||||
|
||||
Interface system
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
|
@ -448,9 +474,6 @@ Config loader
|
|||
~~~~~~~~~~~~~
|
||||
_tbw_
|
||||
|
||||
Lua Scripting
|
||||
~~~~~~~~~~~~~
|
||||
_tbw_
|
||||
|
||||
|
||||
|
||||
|
|
@ -656,18 +679,22 @@ mayfail()
|
|||
|
||||
|
||||
|
||||
Singletons
|
||||
~~~~~~~~~~
|
||||
Singletons and Dependencies
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Deliberately, Lumiera stays below the level of utilising a dependency injection framework.
|
||||
Consequently, we access most services _by type name_, pulling up a single implementation
|
||||
instance on demand. Rather than placing this singleton lifecycle logic into the individual
|
||||
implementation classes, we use a _singleton factory_, managing the lifecycle through static
|
||||
variables and placing the singleton object into a static memory block. Singleton initialisation
|
||||
is protected by a monitor per singleton type, while shutdown is triggered by the clean-up of
|
||||
a static variable. This results in the general policy that within Lumiera, performing any
|
||||
``business'' code in the application shutdown phase (after exiting +main()+) is _strictly
|
||||
prohibited._ Generally speaking, destructors _must not perform any significant work_ and
|
||||
are are expected to be failsafe.
|
||||
instance on demand. Yet some aspects of the *Singleton Pattern* are known to be problematic.
|
||||
Rather than placing this singleton lifecycle logic into the individual implementation classes,
|
||||
we use a _singleton factory_, to manage the instance creation and lifecycle of the singleton
|
||||
or service instance separate from the singleton class itself. Singleton initialisation happens
|
||||
lazily with this approach, and thus needs to be protected in a multithreaded environment; we
|
||||
use Double Checked Locking with std::atomic for this purpose.
|
||||
|
||||
Singletons need to be written in a way to cope with that somewhat nondeterministic environment.
|
||||
They will be created on-demand, and be destroyed _eventually,_ in the application shutdown phase.
|
||||
This results in the general policy that within Lumiera, performing any ``business'' code in the
|
||||
application shutdown phase (after exiting +main()+) is _strictly prohibited._ Generally speaking,
|
||||
destructors _must not perform any significant work_ and are are expected to be failsafe.
|
||||
|
||||
.accessing the singleton instance
|
||||
By convention, when clients are expected actively to access the singleton instance,
|
||||
|
|
@ -675,16 +702,21 @@ the interface class holds the singleton factory as a public static member with t
|
|||
name +instance+. This allows clients to write `SomeService::instance()` to get a
|
||||
reference to the implementation.
|
||||
|
||||
.subclasses and mock testing support
|
||||
There is a mechanism to ``push aside'' the existing singleton instance and shadow
|
||||
it temporarily with a mock implementation. Besides, there is a variation of the
|
||||
general singleton factory, allowing to fabricate a specific subclass of the
|
||||
exposed interface. Both of these facilities are rather technically involved
|
||||
and require some specific set-up -- fortunately it turned out that they are
|
||||
used only occasionally and rarely required. (Probably this is a result of
|
||||
Lumiera being developed _test-driven_ -- things are being written mostly
|
||||
in a unit test friendly fashion).
|
||||
.subclasses and services
|
||||
Actually, what appears as a mere ``singleton'' here is a form of _Dependency Injection,_
|
||||
and can easily be extended to cover other forms of dependencies and other types of lifecycle.
|
||||
Our experience indicates that this is rarely necessary, but when it happens, it's essential.
|
||||
Lumiera's Dependency-Factory thus offers a configuration mechanism, which allows to switch
|
||||
those modes of operation for individual types; the client consuming the ``singleton'' remains
|
||||
unaware of such special setup.
|
||||
|
||||
.mock testing support
|
||||
Moreover, the Dependency-Factory also offers a mechanism to ``push aside'' the existing singleton
|
||||
or serviec instance and shadow it temporarily with a mock implementation. Again, it turned out that
|
||||
such mocking mechanisms are used only occasionally and rarely required. Likely this is a result of
|
||||
Lumiera being developed _test-driven_ -- things are being written mostly in a unit test friendly way.
|
||||
|
||||
-> more about link:{ldoc}/technical/library/Dependencies.html[dependency handling]
|
||||
|
||||
Extensible Factory
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
|
@ -749,6 +781,16 @@ only _unique values,_ to _take a snapshot on-the-fly_ etc. There are convenience
|
|||
builder functions for those operations, figuring out the actual source and destination
|
||||
types by template metaprogramming.
|
||||
|
||||
Pipeline Builder
|
||||
^^^^^^^^^^^^^^^^
|
||||
As an extension to the iterator framework, the pipeline builder is able
|
||||
to wrap arbitrary source iterators, decorate them with filters and transformations
|
||||
provided as Lambda, and finally package all together into a self contained new
|
||||
iterator (value object). This is a rather new feature [small]#(as of 2017)# and
|
||||
we still need to determine its viability, yet it proved helpful when implementing
|
||||
certain kinds of search algorithms on top of an abstracted data source exposed
|
||||
as iterator. -> 'lib/iter-tree-explorer.hpp'
|
||||
|
||||
|
||||
Front-end for boost::format
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ namespace lumiera {
|
|||
* correspond to a single component, interface or plugin. It may well be a
|
||||
* complete layer of the application (e.g. the GUI).
|
||||
*
|
||||
* \par Protocol of operation
|
||||
* ## Protocol of operation
|
||||
* The SubsystemRunner is to be configured with a lumiera::Option object first.
|
||||
* Then, primary subsystems are \link #maybeRun provided \endlink for eventual
|
||||
* startup, which may depend on conditions defined by the subsystem. When
|
||||
|
|
@ -112,7 +112,7 @@ namespace lumiera {
|
|||
* will be triggered. The #wait() function returns after shutdown of all subsystems,
|
||||
* signalling an emergency exit (caused by an exception) with its return value.
|
||||
*
|
||||
* @todo maybe use my refArray (see builder) to use Subsys& instead of Subsys* ??
|
||||
* @todo 2018 this component works well but could be (re)written in a cleaner way
|
||||
*
|
||||
* @see lumiera::AppState
|
||||
* @see lumiera::Subsys
|
||||
|
|
|
|||
|
|
@ -23,8 +23,87 @@
|
|||
|
||||
/** @file dependency-factory.hpp
|
||||
** Per type specific configuration of instances created as service dependencies.
|
||||
** @todo WIP-WIP 3/18 rework of the singleton / dependency factory is underway
|
||||
** 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
|
||||
** and 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 do the task at hand, yet
|
||||
** a dependency lies 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 complex
|
||||
** interwoven structures; it suffices to ensure that none of the participating
|
||||
** entities actually starts to 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)
|
||||
**
|
||||
** The public configuration mechanisms offered by DependInject address various concerns:
|
||||
**
|
||||
** \paragraph 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>::singleton (An InstanceHolder<SRV>).
|
||||
**
|
||||
** \paragraph Service 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>
|
||||
**
|
||||
** \paragraph 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 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 subsys.hpp
|
||||
*/
|
||||
|
||||
|
||||
|
|
@ -71,11 +150,13 @@ namespace lib {
|
|||
* which is managed automatically and removed when leaving the scope of the test.
|
||||
*/
|
||||
template<class SRV>
|
||||
struct DependInject
|
||||
class DependInject
|
||||
: util::NoInstance
|
||||
{
|
||||
using Factory = typename Depend<SRV>::Factory;
|
||||
using Lock = typename Depend<SRV>::Lock;
|
||||
|
||||
public:
|
||||
/** configure dependency-injection for type SRV to build a subclass singleton
|
||||
* @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
|
||||
|
|
@ -308,6 +389,5 @@ namespace lib {
|
|||
|
||||
|
||||
|
||||
|
||||
} // namespace lib
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@
|
|||
** 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 \c Depend<TY> factory
|
||||
** maintains a singleton instance of type TY. The possibility to install other factory
|
||||
** 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.
|
||||
**
|
||||
**
|
||||
|
|
@ -54,15 +54,32 @@
|
|||
** 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. Destructors aren't allowed to do _any significant work_
|
||||
** beyond releasing references, and we acknowledge that singletons can be released
|
||||
** in _arbitrary order_.
|
||||
** 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_.
|
||||
**
|
||||
** @todo WIP-WIP 3/18 rework of the singleton / dependency factory is underway
|
||||
** 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.
|
||||
**
|
||||
** @see lib::Depend
|
||||
** @see lib::DependencyFactory
|
||||
** @see lib::test::Depend4Test
|
||||
** ## 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) problems of this pattern.
|
||||
** 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.
|
||||
**
|
||||
** @see depend-inject.hpp
|
||||
** @see lib::DependInject
|
||||
** @see Singleton_test
|
||||
** @see DependencyConfiguration_test
|
||||
*/
|
||||
|
|
@ -98,14 +115,13 @@ namespace lib {
|
|||
{
|
||||
std::unique_ptr<TAR> instance_;
|
||||
|
||||
|
||||
public:
|
||||
TAR*
|
||||
buildInstance()
|
||||
{
|
||||
return buildInstance ([]{ return new TAR{}; });
|
||||
}
|
||||
|
||||
|
||||
template<class FUN>
|
||||
TAR*
|
||||
buildInstance(FUN&& ctor)
|
||||
|
|
@ -115,7 +131,6 @@ namespace lib {
|
|||
"Either the application logic, or the compiler "
|
||||
"or runtime system is seriously broken"
|
||||
,error::LUMIERA_ERROR_LIFECYCLE);
|
||||
|
||||
instance_.reset (ctor());
|
||||
return instance_.get();
|
||||
}
|
||||
|
|
@ -136,7 +151,8 @@ namespace lib {
|
|||
|
||||
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* @internal access point to reconfigure dependency injection on a per type base
|
||||
* @see depend-inject.hpp
|
||||
*/
|
||||
|
|
@ -145,27 +161,16 @@ namespace lib {
|
|||
|
||||
|
||||
/**
|
||||
* Access point to singletons and other kinds of dependencies.
|
||||
* Actually this is a Factory object, which is typically placed into a
|
||||
* static field of the Singleton (target) class or some otherwise suitable interface.
|
||||
* Access point to singletons and other kinds of dependencies designated *by type*.
|
||||
* Actually this is a Factory object, which is typically placed into a static field
|
||||
* of the Singleton (target) class or some otherwise suitable interface.
|
||||
* @tparam SRV the class of the Service or Singleton instance
|
||||
* @note uses static fields internally, so all factory configuration is shared per type
|
||||
* @remark there is an ongoing discussion regarding the viability of the
|
||||
* Double Checked Locking pattern, which requires either the context of a clearly defined
|
||||
* language memory model (as in Java), or needs to be supplemented by memory barriers.
|
||||
* In our case, this debate boils down to the question: does \c pthread_mutex_lock/unlock
|
||||
* constitute a memory barrier, such as to force any memory writes happening \em within
|
||||
* the singleton ctor to be flushed and visible to other threads when releasing the lock?
|
||||
* To my understanding, the answer is yes. See
|
||||
* [POSIX](http://www.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap04.html#tag_04_10)
|
||||
* @remark we could consider to rely on a _Meyers Singleton_, where the compiler automatically
|
||||
* generates the necessary code and guard variable to ensure single-threaded initialisation
|
||||
* of the instance variable. But the downside of this approach is that we'd loose access
|
||||
* to the singleton instance variable, which then resides within the scope of a single
|
||||
* access function. Such would counterfeit the ability to exchange the instance to
|
||||
* inject a mock for unit testing.
|
||||
* @todo WIP-WIP 3/18 rework of the singleton / dependency factory is underway /////////////////////TICKET #1086
|
||||
* @param SI the class of the Singleton instance
|
||||
* @remarks
|
||||
* - threadsafe lazy instantiation implemented by Double Checked Locking with std::atomic.
|
||||
* - by default, without any explicit configuration, this template creates a singleton.
|
||||
* - a per-type factory function can be configured with the help of lib::DependInject<SRV>
|
||||
* - singletons will be destroyed when the embedded static InstanceHolder is destroyed.
|
||||
*/
|
||||
template<class SRV>
|
||||
class Depend
|
||||
|
|
@ -206,11 +211,13 @@ namespace lib {
|
|||
}
|
||||
instance.store (object, std::memory_order_release);
|
||||
}
|
||||
// ENSURE (object);
|
||||
ENSURE (object);
|
||||
return *object;
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
/** @internal preconfigured factory to block any (further) on-demand instance creation */
|
||||
static SRV*
|
||||
disabledFactory()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -279,8 +279,8 @@ namespace lib {
|
|||
* Two flavours need to be distinguished:
|
||||
* - we get a _reference_ to something living elsewhere; all we know is it's iterable.
|
||||
* - we get a pointer, indicating that we must take ownership and manage the lifetime.
|
||||
* The iterable entity in this case can be assumed heap allocated, featuring a virtual
|
||||
* destructor.
|
||||
* The iterable entity in this case can be assumed to be heap allocated, featuring a
|
||||
* virtual destructor.
|
||||
* The generated front-end has identical type in both cases; it is based on a shared_ptr,
|
||||
* just a different deleter function is used in both cases. This design makes sense,
|
||||
* since the protocol requires us to invoke virtual function IterSource::disconnect()
|
||||
|
|
|
|||
|
|
@ -85,5 +85,18 @@ namespace util {
|
|||
Cloneable& operator= (Cloneable const&) = delete;
|
||||
};
|
||||
|
||||
/**
|
||||
* Not meant to be instantiated in any way.
|
||||
* Types marked this way are typically used for metaprogramming
|
||||
* or expose static factory methods to some related sibling
|
||||
* based on a template parameter or similar configuration.
|
||||
*/
|
||||
class NoInstance
|
||||
{
|
||||
protected:
|
||||
NoInstance() = delete;
|
||||
};
|
||||
|
||||
|
||||
} // namespace util
|
||||
#endif /*LIB_NOCOPY_H*/
|
||||
|
|
|
|||
|
|
@ -1927,19 +1927,19 @@ As we don't have a Prolog interpreter on board yet, we utilize a mock store with
|
|||
{{{default(Obj)}}} is a predicate expressing that the object {{{Obj}}} can be considered the default setup under the given conditions. Using the //default// can be considered as a shortcut for actually finding an exact and unique solution. The latter would require to specify all sorts of detailed properties up to the point where only one single object can satisfy all conditions. On the other hand, leaving some properties unspecified would yield a set of solutions (and the user code issuing the query had to provide means for selecting one solution from this set). Just falling back on the //default// means that the user code actually doesn't care for any additional properties (as long as the properties he //does// care for are satisfied). Nothing is said specifically on //how//&nbsp; this default gets configured; actually there can be rules //somewhere,// and, additionally, anything encountered once while asking for a default can be re-used as default under similar circumstances.
|
||||
&rarr; [[implementing defaults|DefaultsImplementation]]</pre>
|
||||
</div>
|
||||
<div title="DependencyFactory" creator="Ichthyostega" modifier="Ichthyostega" created="201803110155" modified="201803240948" tags="def Concepts draft" changecount="53">
|
||||
<div title="DependencyFactory" creator="Ichthyostega" modifier="Ichthyostega" created="201803110155" modified="201803242359" tags="def Concepts draft" changecount="77">
|
||||
<pre>//Access point to dependencies by-name.//
|
||||
In the Lumiera code base, we refrain from building or using a full-blown Dependency Injection Container. A lot of FUD has been spread regarding Dependency Injection and Singletons, to the point that a majority of developers confuses and conflates the ~Inversion-of-Control principle (which is essential) with the use of a ~DI-Container. Today, you can not even mention the word "Singleton" without everyone yelling out "Evil! Evil!" -- while most of these people just feel comfortable living in the metadata hell.
|
||||
|
||||
Not Singletons as such are problematic -- rather, the coupling of the Singleton class itself with the instantiation and lifecycle mechanism is what creates the problems. In C++ these problems can be mitigated by use of a generic //Singleton Factory// -- which can be augmented into a DependencyFactory for those rare cases where we actually need more instance and lifecycle management beyond lazy initialisation. Client code indicates the dependence on some other service by planting an instance of that Dependency Factory (for Lumiera this is {{{lib::Depend<TY>}}}) and remain unaware if the instance is created lazily in singleton style (which is the default) or has been reconfigured to expose a service instance explicitly created by some subsystem lifecycle. The //essence of a "dependency"// of this kind is that we ''access a service //by name//''. And this service name or service ID is in our case a //type name.//
|
||||
Not Singletons as such are problematic -- rather, the coupling of the Singleton class itself with the instantiation and lifecycle mechanism is what creates the problems. This situation is similar to the use of //global variables,// which likewise are not evil as such; the problems arise from an imperative, operation driven and data centric mindset, combined with hostility towards any abstraction. In C++ such problems can be mitigated by use of a generic //Singleton Factory// -- which can be augmented into a //Dependency Factory// for those rare cases where we actually need more instance and lifecycle management beyond lazy initialisation. Client code indicates the dependence on some other service by planting an instance of that Dependency Factory (for Lumiera this is {{{lib::Depend<TY>}}}) and remain unaware if the instance is created lazily in singleton style (which is the default) or has been reconfigured to expose a service instance explicitly created by some subsystem lifecycle. The //essence of a "dependency"// of this kind is that we ''access a service //by name//''. And this service name or service ID is in our case a //type name.//
|
||||
|
||||
!Requirements
|
||||
Our DependencyFactory satisfies the following requirements
|
||||
* client code is able to access some service //by-name// -- where the name is actually the //type name// of the service interface.
|
||||
* client code remains agnostic with regard to the lifecycle or backing context of the service it relies on
|
||||
* in the simplest (and most prominent case), //nothing// has to be done at all by anyone to manage that lifecycle.<br/>By default, the DependencyFactory creates a singleton instance lazily (heap allocated) on demand and ensures thread-safe initialisation and access.
|
||||
* in the simplest (and most prominent case), //nothing// has to be done at all by anyone to manage that lifecycle.<br/>By default, the Dependency Factory creates a singleton instance lazily (heap allocated) on demand and ensures thread-safe initialisation and access.
|
||||
* we establish a policy to ''disallow any significant functionality during application shutdown''. After leaving {{{main()}}}, only trivial dtors are invoked and possibly a few resource handles are dropped. No filesystem writes, no clean-up and reorganisation, not even any logging is allowed. For this reason, we established a [[Subsystem]] concept with explicit shutdown hooks, which are invoked beforehand.
|
||||
* the DependencyFactory can be re-configured for individual services (type names) to refer to an explicitly installed service instance. In those cases, access while the service is not available will raise an exception. There is a simple one-shot mechanism to reconfigure DependencyFactory and create a link to an actual service implementation, including automatic deregistration.
|
||||
* the Dependency Factory can be re-configured for individual services (type names) to refer to an explicitly installed service instance. In those cases, access while the service is not available will raise an exception. There is a simple one-shot mechanism to reconfigure Dependency Factory and create a link to an actual service implementation, including automatic deregistration.
|
||||
|
||||
!!!Configuration
|
||||
The DependencyFactory and thus the behaviour of dependency injection can be reconfigured, ad hoc, at runtime.
|
||||
|
|
@ -1950,7 +1950,7 @@ Deliberately, we do not enforce global consistency statically (since that would
|
|||
:access the singleton instance as {{{theBla().doIt();}}}
|
||||
;singleton subclass
|
||||
:{{{DependInject<Blah>::useSingleton<SubBlah>() }}}
|
||||
:causes the dependency factory {{{Depend<Bla>}}} to create a {{{SubBla}}} singleton instance from now on
|
||||
:causes the dependency factory {{{Depend<Bla>}}} to create a {{{SubBlah}}} singleton instance from now on
|
||||
;attach to service
|
||||
:{{{DependInject<Blah>::ServiceInstance<SubBlah> service{p1, p2, p3}}}}
|
||||
:* build and manage an instance of {{{SubBlah}}} in heap memory immediately (not lazily)
|
||||
|
|
@ -1970,33 +1970,41 @@ Deliberately, we do not enforce global consistency statically (since that would
|
|||
:please note //that this closure will be invoked later, on-demand.//
|
||||
|
||||
We consider the usage pattern of dependencies a question of architecture rather -- such can not be solved by any mechanism at implementation level.
|
||||
For this reason, DependencyFactory prevents reconfiguration after use, but does nothing exceeding such basic sanity checks
|
||||
For this reason, Lumiera's Dependency Factory prevents reconfiguration after use, but does nothing exceeding such basic sanity checks
|
||||
|
||||
!!!Performance considerations
|
||||
We acknowledge that such a dependency or service will be accessed frequently and even from rather performance critical parts of the application. We have to optimise for low overhead on access, while initialisation happens only once and can be arbitrarily expensive. At which point precisely initialisation happens is a question of architecture -- lazy initialisation can be used to avoid expensive setup of rarely used services, or it can be employed to simplify the bootstrap of complex subsystems, or to break service dependency cycles. All of this builds on the assumption that the global application structure is fixed and finite and well-known -- we assume we are in full control about when and how parts of the application start and stop working.
|
||||
We acknowledge that such a dependency or service will be accessed frequently and even from rather performance critical parts of the application. We have to optimise for low overhead on access, while initialisation happens only once and can be arbitrarily expensive. It is more important for configuration, setup and initialisation to be readable, and to be found at that location within the code where the related concerns are treated -- which is not at the usage site, and which is likewise not within some global central core application setup. At which point precisely initialisation happens is a question of architecture -- lazy initialisation can be used to avoid expensive setup of rarely used services, or it can be employed to simplify the bootstrap of complex subsystems, or to break service dependency cycles. All of this builds on the assumption that the global application structure is fixed and finite and well-known -- we assume we are in full control about when and how parts of the application start and stop working.
|
||||
|
||||
Our requirements on (optional) reconfigurability have some impact on the implementation technique though, since we need access to the instance pointer for individual service types. This basically rules out //Meyers Singleton// -- and so the adequate implementation technique for our usage pattern is //Double Checked Locking.// In the past, there was much debate about DCL being broken -- which indeed was true when //assuming full portability and arbitrary target platform.// Since our focus is primarily on ~PC-with-Linux systems, this argument seems rather theoretical though, since the x86/64 platform is known to employ rather strong memory and cache coherency constraints. With the advent of ARM systems, the situation has changed however. Anyway, since C++11 there is a portable solution for writing a correct DCL implementation, based on {{{std::atomic}}}.
|
||||
Our requirements on (optional) reconfigurability have some impact on the implementation technique though, since we need access to the instance pointer for individual service types. This basically rules out //Meyers Singleton// -- and so the adequate implementation technique for our usage pattern is //Double Checked Locking.// In the past, there was much debate about DCL being [[broken|http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html]] -- which indeed was true when //assuming full portability and arbitrary target platform.// Since our focus is primarily on ~PC-with-Linux systems, this argument seems to lean more to the theoretical side though, since the x86/64 platform is known to employ rather strong memory and cache coherency constraints. With the recent advent of ARM systems, the situation has changed however. Anyway, since C++11 there [[is now a portable solution available|http://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/]] for writing a correct DCL implementation, based on {{{std::atomic}}}.
|
||||
|
||||
To give some idea of the rough proportions of performance impact, in 2018 we conducted some micro benchmarks (using a 8 core AMD 64bit processor running Debian/Jessie building with GCC 4.9)
|
||||
The following table lists averaged results in relative numbers, in relation to a single threaded direct non virtual member function invocation (≈ 0.3ns)
|
||||
The idea underlying Double Checked Locking is to optimise for the access path, which is achieved by moving the expensive locking entirely out of that path. However, any kind of concurrent consistency assertion requires us to establish a "happens before" relation between two events of information exchange. Both traditional locking and lock-free concurrency implement this relation by establishing a ''synchronises-with'' relation between two actions on a common ''guard'' entity -- for traditional locking, this would be a Lock, Mutex, Monitor or Semaphore, while lock-free concurrency uses the notion of a //fence// connected with some well defined action on a guard variable. In modern C++, typically we use //Atomic variables// as guard. In addition to well defined semantics regarding concurrent visibility of changes, these [["atomics"|http://en.cppreference.com/w/cpp/atomic]] offer indivisible access and exchange operations. A correct concurrent interaction must involve some kind of well defined handshake to establish the aforementioned synchronises-with relation -- otherwise we just can not assume anything. Herein lies the problem with Double Checked Locking: when we move all concurrency precautions away from the optimised access path, we get performance close to a direct local memory access, but we can not give any correctness assertions in this setup. If we are lucky (and the underlying hardware does much to yield predictable behaviour), everything works as expected, but we can never be sure about that. A correct solution thus inevitably needs to take away some of the performance from the optimised access path. Fortunately, with properly used atomics this price tag is known to be low. At the end of the day, correctness is more important than some superficially performance boost.
|
||||
|
||||
To gain insight into the rough proportions of performance impact, in 2018 we conducted some micro benchmarks (using a 8 core AMD ~FX-8350 64bit CPU running Debian/Jessie and GCC 4.9 compiler)
|
||||
The following table lists averaged results //in relative numbers,// in relation to a single threaded optimised direct non virtual member function invocation (≈ 0.3ns)
|
||||
| !Access Technique |>| !development |>| !optimised |
|
||||
|~| single threaded|multithreaded | single threaded|multithreaded |
|
||||
|direct invoke shared local object | 15.13| 16.30| ''1.00''| 1.59|
|
||||
|direct invoke on shared local object | 15.13| 16.30| ''1.00''| 1.59|
|
||||
|invoke existing object through unique_ptr | 60.76| 63.20| 1.20| 1.64|
|
||||
|lazy init unprotected (not threadsafe) | 27.29| 26.57| 2.37| 3.58|
|
||||
|lazy init always mutex protected | 179.62| 10917.18| 86.40| 6661.23|
|
||||
|Double Checked Locking with mutex | 27.37| 26.27| 2.04| 3.26|
|
||||
|DCL with std::atomic and mutex for init | 44.06| 52.27| 2,79| 4.04|
|
||||
|DCL with std::atomic and mutex for init | 44.06| 52.27| 2.79| 4.04|
|
||||
|
||||
These benchmarks used a dummy service class holding a volatile int, initialised to a random value. The complete code was visible to the compiler and thus eligible for inlining. After accessing this dummy object through the means listed in the table, the benchmarked code retrieved this value repeatedly and compared it to zero. The concurrent measurement used 8 threads (number of cores). Some observations
|
||||
These benchmarks used a dummy service class holding a volatile int, initialised to a random value. The complete code was visible to the compiler and thus eligible for inlining. Repeatedly the benchmarked code accessed this dummy object through the means listed in the table, then retrieved the (actually constant) value from the private volatile variable within the service and compared it to zero. This setup ensures the optimiser can not remove the code altogether, while the access to the service dominates the measured time. The concurrent measurement used 8 threads (number of cores), each performing the same timing loop on a local instance. The number of invocations within each thread was high enough (several millions) to amortise the actual costs of object allocation. Some observations:
|
||||
* The numbers obtained pretty much confirm [[other people's measurments|http://www.modernescpp.com/index.php/thread-safe-initialization-of-a-singleton]]
|
||||
* Synchronisation is indeed necessary; the unprotected lazy init crashed several times randomly during those tests.
|
||||
* Synchronisation is indeed necessary; the unprotected lazy init crashed several times randomly during multithreaded tests.
|
||||
* Contention on concurrent access is very tangible; even for unguarded access the cache and memory hardware has to perform additional work
|
||||
* However, the concurrency situation in this example is rather extreme and deliberately provokes collisions; in practice we'd be closer to the single threaded case
|
||||
* Double Checked Locking is a very effective implementation strategy and results in timings within the same order of magnitude as direct access
|
||||
* Unprotected lazy initialisation performs spurious duplicate initialisations, which can be avoided by DCL
|
||||
* Naive Mutex locking is slow even without contention
|
||||
* Optimisation achieves access times of ≈ 1ns
|
||||
* Naïve Mutex locking is slow even with non-recursive Mutex without contention
|
||||
* Optimisation achieves access times around ≈ 1ns
|
||||
|
||||
!Architecture
|
||||
Dependency management does not define the architecture, nor can it solve architecture problems. Rather, its purpose is to enact the architecture. A dependency is something we need to do the task at hand, yet a dependency lies beyond that task and relates to concerns outside the scope and theme of this actual task. A naïve functional approach -- pass everything you need as argument -- would be as harmful as thoughtlessly manipulating some off-site data to fit current needs. The local function would be splendid, strict and referentially transparent -- yet anyone using it would be infected with issues of tangling and tight coupling. As remedy, a //global context// can be introduced, which works well as long as this global context does not exhibit any other state than "being available". The root of those problems lies in the drive to conceive matters simpler as they are.
|
||||
* collaboration to form a whole typically leads to indirect mutual dependency. We can only define precisely //what is required locally,// and then //pull our requirements// on demand.
|
||||
* a given local action can be part of a process, or a conversation or interaction chain, which in turn might originate from various, quite distinct contexts. At //that level,// we might find a simpler structure to hinge questions of lifecycle on.
|
||||
In Lumiera we encounter both these kinds of circumstances. On a global level, we have a simple and well defined order of dependencies, cast into [[Subsystem relations|Subsystem]]. We know e.g. that mutating changes to the session can originate from scripts or from UI interactions. It suffices thus, when the //leading subsystem// (the UI or the script runner) refrains from emitting any external activities, prior to reaching that point in the lifecycle where everything is "basically set". However self evident this insight might be, it yields some unsettling and challenging consequences: The UI //must not assume// the presence of specific data structures within the lower layers, nor is it allowed to "pull" session contents while starting up. Rather the UI-Layer is bound to bootstrap itself into completely usable and operative state, without the ability to attach anything onto existing tangible content structures. This runs completely counter common practice of UI programming, where it is customary to wire most of the application internals somehow directly below the UI "shell". Rather, in Lumiera the UI must be conceived as a [[collection of services|GuiTopLevel]] -- and when running, a //population request// can be issued to fill the UI framework with content. This is inversion-of-control at work.
|
||||
</pre>
|
||||
</div>
|
||||
<div title="DesignDecisions" modifier="Ichthyostega" created="200801062209" modified="201505310104" tags="decision design discuss Concepts" changecount="5">
|
||||
|
|
|
|||
|
|
@ -27700,28 +27700,28 @@
|
|||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521160700669" HGAP="4" ID="ID_978221585" MODIFIED="1521791636399" TEXT="Dokumentation" VSHIFT="25">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521790595841" ID="ID_1874367277" MODIFIED="1521790983987" TEXT="Doku-Text im TiddlyWiki">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521790681557" ID="ID_1346970835" MODIFIED="1521790986851" TEXT="die generelle Haltung bezügl. Performance / Korrektheit">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node COLOR="#338800" CREATED="1521790595841" ID="ID_1874367277" MODIFIED="1521936684007" TEXT="Doku-Text im TiddlyWiki">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
<node COLOR="#338800" CREATED="1521790681557" ID="ID_1346970835" MODIFIED="1521936679674" TEXT="die generelle Haltung bezügl. Performance / Korrektheit">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521790613318" ID="ID_506011313" MODIFIED="1521885094991" TEXT="Diskussion der Locking / Atomic-Problematik">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node COLOR="#338800" CREATED="1521790613318" ID="ID_506011313" MODIFIED="1521936677400" TEXT="Diskussion der Locking / Atomic-Problematik">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
<node COLOR="#338800" CREATED="1521885096858" ID="ID_1403195900" MODIFIED="1521885106977" TEXT="Benchmark-Zahlen dokumentieren">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521790709017" ID="ID_1263887689" MODIFIED="1521790982924" TEXT="Doxygen">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521790767697" ID="ID_1445751091" MODIFIED="1521790987852" TEXT="Grundsätzliches zu Singleton und Dependency-Injection (aktualisieren)">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node COLOR="#338800" CREATED="1521790709017" ID="ID_1263887689" MODIFIED="1521947771608" TEXT="Doxygen">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
<node COLOR="#338800" CREATED="1521790767697" ID="ID_1445751091" MODIFIED="1521936696493" TEXT="Grundsätzliches zu Singleton und Dependency-Injection (aktualisieren)">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521790800253" ID="ID_878208550" MODIFIED="1521790988539" TEXT="Eigenes Kapitel zum Thema Double-Checked Locking">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node COLOR="#338800" CREATED="1521790800253" ID="ID_878208550" MODIFIED="1521946968361" TEXT="Eigenes Kapitel zum Thema Double-Checked Locking">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521790816937" ID="ID_776249726" MODIFIED="1521790989859" TEXT="Grenzziehung zur Architektur">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node COLOR="#338800" CREATED="1521790816937" ID="ID_776249726" MODIFIED="1521946976350" TEXT="Grenzziehung zur Architektur">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
<node CREATED="1521790858701" ID="ID_1382870211" MODIFIED="1521790870103" TEXT="Belange die hier nicht behandelt werden"/>
|
||||
<node CREATED="1521790870898" ID="ID_662113030" MODIFIED="1521790886784">
|
||||
<richcontent TYPE="NODE"><html>
|
||||
|
|
@ -27737,22 +27737,41 @@
|
|||
</richcontent>
|
||||
</node>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521790951152" ID="ID_974126504" MODIFIED="1521790991474" TEXT="Semantik der Konfigurations-Varianten beschreiben">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node COLOR="#338800" CREATED="1521790951152" ID="ID_974126504" MODIFIED="1521946981024" TEXT="Semantik der Konfigurations-Varianten beschreiben">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521790992538" ID="ID_1892242381" MODIFIED="1521791005169" TEXT="die hier ausgeführte Analyse zusammenfassen">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node COLOR="#338800" CREATED="1521790992538" ID="ID_1892242381" MODIFIED="1521947767631" TEXT="die hier ausgeführte Analyse zusammenfassen">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521791026749" ID="ID_1199387367" MODIFIED="1521791033909" TEXT="Doku-Subsysteme">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521791034972" ID="ID_1629095174" MODIFIED="1521791050755" TEXT="überlegen, wo das Architektur-Thema dargestellt werden sollte">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node COLOR="#338800" CREATED="1521791034972" ID="ID_1629095174" MODIFIED="1521962998443" TEXT="überlegen, wo das Architektur-Thema dargestellt werden sollte">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
<node CREATED="1521962966238" ID="ID_1724029235" MODIFIED="1521962976832" TEXT="gehört in die Architektur-Sektion"/>
|
||||
<node COLOR="#338800" CREATED="1521962977140" ID="ID_1121275609" MODIFIED="1521962995498" TEXT="Neue Seite "Subsystems" angelegt">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521791058689" ID="ID_564899806" MODIFIED="1521791078375" TEXT="Hinweis auf die offene Flanke bei Emergency-Shutdown">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
</node>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521929151045" ID="ID_1800182456" MODIFIED="1521929162564" TEXT="Doku-Seite für DependencyFactory anlegen">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node CREATED="1521929165380" ID="ID_15396392" MODIFIED="1521929185272" TEXT="kann in etwa der Content aus dem TiddlyWiki sein">
|
||||
<icon BUILTIN="idea"/>
|
||||
</node>
|
||||
<node COLOR="#338800" CREATED="1521963017631" ID="ID_380828830" MODIFIED="1521963032077" TEXT="Neue Seite angelegt und von The Inner Core verlinkt">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521929186281" ID="ID_1078337367" MODIFIED="1521929199008" TEXT="ein, zwei Einleitungssätze dazu">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521929199799" ID="ID_507602296" MODIFIED="1521929217037" TEXT="Wichtig: die Benchmark-Daten hier auch darstellen">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node CREATED="1520722160591" ID="ID_135546699" MODIFIED="1520722168810" TEXT="Unit-Test">
|
||||
|
|
@ -27815,14 +27834,36 @@
|
|||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521858774321" ID="ID_302686020" MODIFIED="1521858778744" TEXT="aufräumen">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521858779614" ID="ID_62114860" MODIFIED="1521858790147" TEXT="was ist depend4test?">
|
||||
<node COLOR="#338800" CREATED="1521858779614" ID="ID_62114860" MODIFIED="1521928808594" TEXT="was ist depend4test?">
|
||||
<icon BUILTIN="help"/>
|
||||
<node CREATED="1521928764171" ID="ID_507085354" MODIFIED="1521928798462" TEXT="die alte Testmock-Lösung">
|
||||
<icon BUILTIN="idea"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521858791160" ID="ID_1863623429" MODIFIED="1521858801327" TEXT="brauchen wir DelStash noch?">
|
||||
<icon BUILTIN="help"/>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521928773218" ID="ID_1837242179" MODIFIED="1521928795280" TEXT="nach Schwenken aller Unit-Tests wegfallend">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521861040846" ID="ID_1297884815" MODIFIED="1521861055999" TEXT="brauchen wir singleton-ref?">
|
||||
</node>
|
||||
<node COLOR="#338800" CREATED="1521858791160" ID="ID_1863623429" MODIFIED="1521929094523" TEXT="brauchen wir DelStash noch?">
|
||||
<icon BUILTIN="help"/>
|
||||
<node CREATED="1521929071984" ID="ID_529951044" MODIFIED="1521929092199" TEXT="wird vom Advice-System genutzt"/>
|
||||
<node CREATED="1521929080591" ID="ID_192438703" MODIFIED="1521929097694" TEXT="bleibt weiterhin notwendig">
|
||||
<icon BUILTIN="yes"/>
|
||||
</node>
|
||||
</node>
|
||||
<node COLOR="#338800" CREATED="1521861040846" ID="ID_1297884815" MODIFIED="1521929021187" TEXT="brauchen wir singleton-ref?">
|
||||
<icon BUILTIN="help"/>
|
||||
<node CREATED="1521928945937" ID="ID_1967326849" MODIFIED="1521929017586" TEXT="die alte (partielle) Lösung für externe Services">
|
||||
<icon BUILTIN="idea"/>
|
||||
</node>
|
||||
<node CREATED="1521928963447" ID="ID_1493572426" MODIFIED="1521929015463" TEXT="ziemlich eng mit den Interface-Proxies verbandelt">
|
||||
<icon BUILTIN="messagebox_warning"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521928978853" ID="ID_47671209" MODIFIED="1521929006930" TEXT="diese sollten auf Depend umgestellt werden">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521929007449" ID="ID_451474924" MODIFIED="1521929011482" TEXT="danach kann es weg">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521433694233" ID="ID_1951071885" MODIFIED="1521433716263" TEXT="Nebenbei: Schwartz-Counter in ClassLock ablösen">
|
||||
|
|
|
|||
Loading…
Reference in a new issue