It's been a while science I did something on jet framework but today I'm going to wrap up with Singleton Registry. One problem that I encountered is that Singleton can't impose restriction on managed class constructor signature. The simplest case of a class with just default constructor is what I'm trying to solve, but any other "requirements" for construction interface are not less restrictive.
Ideally a user should be given option to define some "conventional" constructor or if it's not possible the user should provide constructor argument adaptor and Singleton Registry should prefer this adaptor to any other ways of managed object creation.
The reason I don't want to fallback to usual "factory" approach when user provides "create/destroy" pair of functions is that logically an object lifecycle should be managed by Singleton Registry and not by its client.
I came to the thought that I need a way to categorise constructors of class managed by Singleton. Here is my list of Singleton construction cases:
1. Default constructor;
2. Constructor that accepts Application Context;
3. Constructor that accepts single parameter and an adaptor provided by user which accepts Application Context and returns object of type that can be used to invoke this constructor;
4. Constructor that accepts multiple parameters and an adaptor provided by user which accepts Application Context and returns std::tuple with elements that can be used to invoke this constructor;
These categories are presented in order of preference which means that if some class has default constructor and constructor with the single Application Context parameter then App Context will be preferred for construction of the Singleton instance (because it gives more information for the class).
In my previous post I mentioned that I'm going to implement Singleton Registry but didn't give any details of what I meant by that. Another missing point is what Application Context is. I will give detailed description of Singleton Registry design later but from the name it's already obvious that it's an object that is used to register singletons.
The idea is to separate a Singleton creation and usage. In "Classical Singleton" both creation and usage is combined together which causes much trouble for a user if he wants some special parameterisation or be more specific with the moment when Singleton has to be actually created. This is mostly solved with boost::singularity but it also takes away most of the Singleton attraction. In the ideal world where a developer controls all parts of an application boost::singularity works fine but in practice the requirement to manage Singleton lifecycle explicitly is too inconvenient. I'm talking about usual workflow when all Singletons are initialised at the beginning of function main().
One of the problems with this approach is dependency tracking - only Singleton object knows what other Singletons it depends on and this information is required for correct order of initialisation. If Singleton initialisation is done manually then there is a big chance that some dependency will be missed.
Another problem is that a Singleton parameterisation should be done by library that uses it and not by the function main(). There are other reasons not to allow a developer to manage Singleton lifecycle manually.
The solution for these problems is Singleton Registry that manages Singletons lifecycle, dependencies and parameterisation. So basically a Singleton registers itself in the Singleton Registry and then everything is done by Singleton Registry which initialises Singletons at the beginning of the function main() (or any other place).
Here comes Application Context. Let's assume that in general the only "genuine" Singleton for an Application is the Application itself (this is viable assumption although I'm not ready to prove it). In this case all other Singletons created by specific application should depend on the Application Singleton. Another assumption is - whatever information is required for creating Application Singleton should be enough for creating any other Singletons (this is Application Context definition).
As an example - possible Application Context of a simple console application consists of environment variables and command line parameters. For more complex cases it can be configuration files, database tables, Windows registry records, etc...
This view on Application means that a Singleton is just an Application property so in fact it can be implemented as a data member of Application class (I saw such implementations). The reason why you may not like this implementation is lack of flexibility. Consider the case when an application consists of several libraries and each library contains some singletons - every time a library adds a singleton we need to update the Application class.
These two assumptions greatly simplifies all answers on Singleton related questions, for instance:
1. When a Singleton has to be created?
The answer: A Singleton lifecycle is bound to the Application (and other Singletons that it depends on) thus logically it has to be created after all its dependencies and destroyed before them;
2. Can a Singleton be recreated?
The answer: No, a Singleton can not be recreated because it will break its possible dependencies. But if you wish you can recreate the whole tree of Singleton objects starting from the Application;
3. How Singleton can be used with unit tests?
The answer: Every unit tests that require new state for particular Singleton has to recreate the Application Singleton and all other Singletons. If some unit test can share Application Singleton state and others Singletons' states then all Singletons have to be initialised only once before execution of these unit tests;
4. What about lazy initialisation of Singleton - is it allowed?
The answer: Yes, it is. But you should consider overhead of thread synchronisation which prevents data races in multi-threaded environment. Singletons that are initialised at the beginning of an application execution don't need this synchronisation.
5. What if a Singleton requires some initialisation parameters that are not in Application Context (for instance user input)?
The answer: Since a Singleton is a property of the Application then any information available during the Application construction should be enough for construction of the Singleton. If it's not the case then this object is not a Singleton.
This is the weakest point but I don't like the idea of mutable Application Context. This problem can be solved with hierarchies of components. For instance, the Application is top level component of a program. Any Singleton attributed to the Application uses its Application Context (constant during initialisation of the Application). There could be plugins loaded later during execution of a program. Any Singleton attributed to the plugin will use that plugin Context for initialisation (which can be different from the Application Context). Lifecycle of those Singletons depends on the plugin's lifecycle.
The problem with Application Context is that it has to be general enough for any kind of applications. Initially I thought that it can't be achieved but then a help came from boost::application library. It implemented boost::application::context type which is actually an aspect_map. I'm leaving explanation of application::context and aspect_map to boost::application documentation
In fact I was really lucky to find boost::application - otherwise I had to write something similar.
After this long side-story I can return to initial problem of constructor classification. Implementation is a bit tricky with a lot of obscure templates but this is normal for SFINAE way of programming. In general classification method is simple - the code associates some number with each category:
1. Default constructor: 4;
2. Application contest constructor: 8;
3. One argument constructor with suitable adaptor: 16;
4. Multiple arguments constructor with suitable adaptor: 32;
Then algorithm tests whether a type can be constructed in particular way (this is SFINAE part). If a test fails then result values for the category is 1, if it succeeds then category result equals to this category number.
After that all 4 results are summarised and this value is used by Singleton Registry to chose suitable constructor.
For instance:
A class with just default constructor will have classification result value = 4 + 1 + 1 + 1 = 7
A class with default constructor and multiple arguments constructor will have result = 4 + 1 + 1 + 32 = 40.
For any possible combination:
If the result value falls into the range of (4:8] then Singleton Registry uses default constructor;
If the result is in the range of (8:16] then Application Context constructor is used;
If the result is in the range of (16:32] then single argument constructor with adaptor is used;
If the result is in greater then 32 then multiple arguments constructor with adaptor is used;
Ideally a user should be given option to define some "conventional" constructor or if it's not possible the user should provide constructor argument adaptor and Singleton Registry should prefer this adaptor to any other ways of managed object creation.
The reason I don't want to fallback to usual "factory" approach when user provides "create/destroy" pair of functions is that logically an object lifecycle should be managed by Singleton Registry and not by its client.
I came to the thought that I need a way to categorise constructors of class managed by Singleton. Here is my list of Singleton construction cases:
1. Default constructor;
2. Constructor that accepts Application Context;
3. Constructor that accepts single parameter and an adaptor provided by user which accepts Application Context and returns object of type that can be used to invoke this constructor;
4. Constructor that accepts multiple parameters and an adaptor provided by user which accepts Application Context and returns std::tuple with elements that can be used to invoke this constructor;
These categories are presented in order of preference which means that if some class has default constructor and constructor with the single Application Context parameter then App Context will be preferred for construction of the Singleton instance (because it gives more information for the class).
In my previous post I mentioned that I'm going to implement Singleton Registry but didn't give any details of what I meant by that. Another missing point is what Application Context is. I will give detailed description of Singleton Registry design later but from the name it's already obvious that it's an object that is used to register singletons.
The idea is to separate a Singleton creation and usage. In "Classical Singleton" both creation and usage is combined together which causes much trouble for a user if he wants some special parameterisation or be more specific with the moment when Singleton has to be actually created. This is mostly solved with boost::singularity but it also takes away most of the Singleton attraction. In the ideal world where a developer controls all parts of an application boost::singularity works fine but in practice the requirement to manage Singleton lifecycle explicitly is too inconvenient. I'm talking about usual workflow when all Singletons are initialised at the beginning of function main().
One of the problems with this approach is dependency tracking - only Singleton object knows what other Singletons it depends on and this information is required for correct order of initialisation. If Singleton initialisation is done manually then there is a big chance that some dependency will be missed.
Another problem is that a Singleton parameterisation should be done by library that uses it and not by the function main(). There are other reasons not to allow a developer to manage Singleton lifecycle manually.
The solution for these problems is Singleton Registry that manages Singletons lifecycle, dependencies and parameterisation. So basically a Singleton registers itself in the Singleton Registry and then everything is done by Singleton Registry which initialises Singletons at the beginning of the function main() (or any other place).
Here comes Application Context. Let's assume that in general the only "genuine" Singleton for an Application is the Application itself (this is viable assumption although I'm not ready to prove it). In this case all other Singletons created by specific application should depend on the Application Singleton. Another assumption is - whatever information is required for creating Application Singleton should be enough for creating any other Singletons (this is Application Context definition).
As an example - possible Application Context of a simple console application consists of environment variables and command line parameters. For more complex cases it can be configuration files, database tables, Windows registry records, etc...
This view on Application means that a Singleton is just an Application property so in fact it can be implemented as a data member of Application class (I saw such implementations). The reason why you may not like this implementation is lack of flexibility. Consider the case when an application consists of several libraries and each library contains some singletons - every time a library adds a singleton we need to update the Application class.
These two assumptions greatly simplifies all answers on Singleton related questions, for instance:
1. When a Singleton has to be created?
The answer: A Singleton lifecycle is bound to the Application (and other Singletons that it depends on) thus logically it has to be created after all its dependencies and destroyed before them;
2. Can a Singleton be recreated?
The answer: No, a Singleton can not be recreated because it will break its possible dependencies. But if you wish you can recreate the whole tree of Singleton objects starting from the Application;
3. How Singleton can be used with unit tests?
The answer: Every unit tests that require new state for particular Singleton has to recreate the Application Singleton and all other Singletons. If some unit test can share Application Singleton state and others Singletons' states then all Singletons have to be initialised only once before execution of these unit tests;
4. What about lazy initialisation of Singleton - is it allowed?
The answer: Yes, it is. But you should consider overhead of thread synchronisation which prevents data races in multi-threaded environment. Singletons that are initialised at the beginning of an application execution don't need this synchronisation.
5. What if a Singleton requires some initialisation parameters that are not in Application Context (for instance user input)?
The answer: Since a Singleton is a property of the Application then any information available during the Application construction should be enough for construction of the Singleton. If it's not the case then this object is not a Singleton.
This is the weakest point but I don't like the idea of mutable Application Context. This problem can be solved with hierarchies of components. For instance, the Application is top level component of a program. Any Singleton attributed to the Application uses its Application Context (constant during initialisation of the Application). There could be plugins loaded later during execution of a program. Any Singleton attributed to the plugin will use that plugin Context for initialisation (which can be different from the Application Context). Lifecycle of those Singletons depends on the plugin's lifecycle.
The problem with Application Context is that it has to be general enough for any kind of applications. Initially I thought that it can't be achieved but then a help came from boost::application library. It implemented boost::application::context type which is actually an aspect_map. I'm leaving explanation of application::context and aspect_map to boost::application documentation
In fact I was really lucky to find boost::application - otherwise I had to write something similar.
After this long side-story I can return to initial problem of constructor classification. Implementation is a bit tricky with a lot of obscure templates but this is normal for SFINAE way of programming. In general classification method is simple - the code associates some number with each category:
1. Default constructor: 4;
2. Application contest constructor: 8;
3. One argument constructor with suitable adaptor: 16;
4. Multiple arguments constructor with suitable adaptor: 32;
Then algorithm tests whether a type can be constructed in particular way (this is SFINAE part). If a test fails then result values for the category is 1, if it succeeds then category result equals to this category number.
After that all 4 results are summarised and this value is used by Singleton Registry to chose suitable constructor.
For instance:
A class with just default constructor will have classification result value = 4 + 1 + 1 + 1 = 7
A class with default constructor and multiple arguments constructor will have result = 4 + 1 + 1 + 32 = 40.
For any possible combination:
If the result value falls into the range of (4:8] then Singleton Registry uses default constructor;
If the result is in the range of (8:16] then Application Context constructor is used;
If the result is in the range of (16:32] then single argument constructor with adaptor is used;
If the result is in greater then 32 then multiple arguments constructor with adaptor is used;