MagicClass::getInstance().getFooFactory().createFoo().killMePlease();
"Addiction" and "Static typing" can hardly be called best friends for all times. Some developments in this direction exist and are easy to googling, but it is interesting how realistic it is to create your own
simple implementation of hack databases, dexterous tricks and connecting external libraries. Without much flexibility, here are just two actions - customization and implementation. Questions of multithreading will not be affected, so as not to be distracted from the main idea. So, what do I personally want from dependency injection?
Staging
Managing the lifetime of objects . The nuances relating to the lifetime should not dilute the main logic of the application. Most often, I don’t need to know that the requested object is an instance of a single class or is created using some factory method. Just need a usable copy. It is quite good if there is an opportunity to change the rules for controlling the lifetime of an object without affecting the main logic of the application.
Testing One of the primary goals of dependency injection. It implies the possibility of replacing certain objects with test stubs, again without affecting the basic implementation.
')
Simplified access to objects . Information which object from where you can get most often completely uninteresting. Moreover, it distracts from the basic functionality and contributes to a stronger binding of the subsystems of the project than one wants. Also, the programmer’s unwillingness to think through adequate access points to the services being added may adversely affect the overall system architecture.
"In recent days, I have received almost all the objects I need from module number N, and I’ll throw these in there too ..."A working version with a test example can be taken
from here .
Let's get down to business. Let's start with the time of life. I would like to be able to use the following options.
- Singleton ( Singleton ). There is only one static instance of the object during the entire lifetime of the application.
- Shared object. Looks like it alone. The main difference is that an object exists while someone is using it. All clients use the link to the same instance. It can be created and destroyed during the work several times, not even once, if no one was willing.
- Object ( Object ). The lifetime of the object coincides with the lifetime of the client.
- Runtime object. All clients use the same object, which may change while the program is running.
Naturally, the client should not care what kind of type an embedded object belongs to.
The second wish is a natural introduction. It would be desirable, that the object was implemented in client by means of syntax as close as possible to the declaration of a field of a class. Actually, it is a kind of class property that it is.
class SomeClass { public: private: inject(SomeInterface, mFieldName) };
I must immediately admit that, initially, in dreams, of course, there were templates, but somehow it was not possible to get a simple and transparent implementation of the types of implementations described above. But with mundane macros it turned out unexpectedly easy.
There must be a place in the project where dependencies are configured as follows.
inject_as_share(Interface1, Class1) inject_as_singleton(Interface2, Class1) inject_as_object(Interface3, Class1) inject_as_runtime(Interface4)
The first parameter is the interface, the second is the class that implements it. Of course, a class can act as an interface for itself. For the object runtime (runtime) will need more initialization somewhere in the depths of the program (not the most successful implementation option, but sometimes this is needed)
inject_set_runtime(Interface4, &implementation4)
The first thing that catches your eye is
inject_as_share ,
inject_as_singleton and
inject_as_object should be able to create instances of classes, which means calling their constructors. Simplicity is simplicity, but it would be too frivolous to rely solely on constructors by default. Therefore, it will be possible to pass in the initialization macros the parameters of the constructors of the corresponding classes, something like
inject_as_share(Interface1, Class1, "param1", 1234, true)
Before we delve into the implementation of the proposed concept, I would like to briefly discuss other ways of setting the dependence of an object o1 on o2
- o1 directly creates an instance of o2. The most “frontal” and non-flexible method, to replace o2, you have to shovel the sources and replace the creation of one with another. Once o1 has created o2, then he is usually responsible for the destruction.
- Passing o2 as a parameter to the constructor. A fairly common way to implement dependencies, which, however, can be inconvenient if the client class has a lot of dependencies and / or constructors.
- Factory method / class. The approach effectively encapsulates the subtleties of creating an o2 object, but the factories themselves (especially when there are many of them) add information noise to the architecture of the application. Another significant drawback of factories is that their use is not reflected in the class-client interface.
Configurator
Let's move on to the implementation itself. Let's start with the introduction of loners. For the user, the whole setup will be reduced to the string
inject_as_singleton(Interface, Class, [constructor_parameters_list])
Since the number of arguments to the constructor is generally unknown,
inject_as_singleton is a macro with a variable number of arguments. All types of injections will be used uniformly (as a class property), so you first need to come up with an interface that would allow access to objects, regardless of how they were created. In our case, this will be a
Factory structure with a single
get method that returns a reference to an object that implements the specified interface.
struct Factory { Interface& get(); };
The implementation of the loner class will be classic
Interface& getInstance() { static T instance = T(...); return instance; }
The constructor’s call had to be written in a somewhat unusual (at least for me) form, because
static T instance(...);
in the case of a constructor without parameters, it is persistently interpreted by the compiler as a function declaration.
And, to avoid conflicts, we place all this implementation into the injectorXXX namespace, where XXX is the name of the implementation interface. The final macro will look like this.
#define inject_as_singleton(Interface, T, ...) \ namespace injector##Interface{ \ Interface& getInstance() \ { \ static T instance = T(__VA_ARGS__); \ return instance; \ } \ struct Factory \ { \ Interface& get() { return getInstance(); } \ }; \ }
I will not describe this step by step further, but the approach is always the same - the implementation of the strategy for managing the lifetime “as it happens” + the fixed structure-interface
Factory , and all this in a separate namespace.
Let's go to the public facility. It exists while someone needs it, but, unlike a loner, it can be destroyed, re-created and never created at all. We implement this strategy using reference counting - how many factory owners, as many users of the class being introduced.
#define inject_as_share(Interface, T, ...) \ namespace injector##Interface{ \ struct Factory \ { \ Factory() { \ if (refCount == 0) { \ object = new T(__VA_ARGS__); \ } \ ++refCount; \ } \ ~Factory() { \ if (--refCount == 0) { \ delete object; \ } \ } \ Interface& get() { return *object; } \ static T* object; \ static unsigned int refCount; \ }; \ T* Factory::object = 0; \ unsigned int Factory::refCount = 0; \ }
The code does not need any special explanations - we increase the counter in the constructor, create an instance of the implemented class if necessary, reduce the counter in the destructor and, if the hour has passed, delete the unnecessary object.
The implementation of “your object for each client” is implemented quite simply - as a factory field.
#define inject_as_object(Interface, T, ...) \ namespace injector##Interface{ \ struct Factory \ { \ Factory() : object(__VA_ARGS__) {} \ Interface& get() { return object; } \ T object; \ }; \ }
The factory does not own an object that can be changed during the execution of the program; it simply provides the last specified instance and allows changing it.
#define inject_as_runtime(Interface) \ namespace injector##Interface{ \ struct Factory \ { \ Interface& get() { return *object; } \ static Interface* object; \ }; \ Interface* Factory::object = 0; \ }
For the final implementation of the strategy, you need a special macro to set the object.
#define inject_set_runtime(Interface, Value) injector##Interface::Factory::object = (Value);
Implementation
We finished with the factories, now let's move on to the implementation itself. The strategy will be as follows - the object owns an instance of the factory of interest, referring, if necessary, to the object it provides. The term “factory” here, of course, is imperfect, since it does not always create instances, but at the time of writing this article, nothing was better thought out.
Here we face a problem. I don’t want to force the user to manually call the
get method each time — after all, I would like to hide the fact of the existence of the factory in order to untie my hands for further improvements of the mechanism. It is necessary to somehow make the simulator normal properties. The first thing that comes to mind is the implementation of the interface through inheritance. As it comes, it leaves. If this is realizable, then it will be quite nontrivial. Therefore, we will proceed more simply - we will create a wrapper class in which we will override the -> operator, forcing it to return an instance of the class being injected. It will turn out almost perfect. Of the restrictions, the user will face only the obligation to use the embedded object as a pointer, which is already quite tolerable.
So the property injection macro will look like this.
#define inject(Interface, Name) \ struct Interface##Proxy \ { \ Interface* operator->() \ { \ return &factory.get(); \ } \ injector##Interface::Factory factory; \ }; \ Interface##Proxy Name;
Just a wrapper class that owns the desired factory and calls it at the operator ->
Example
As you can see, the implementation is quite simple. Using the proposed mechanism is even easier. Consider a simple example. Let there is an interface
class IWork { public: virtual void doSmth() = 0; virtual ~IWork() {}; };
And several classes Work1, Work2, etc., which implement it, some of these classes have constructors with parameters (the full text of the example is
here ). We first configure
inject_as_share(IWork, Work1) inject_as_singleton(Work2, Work2, 1) inject_as_object(Work3, Work3, 1, true) inject_as_runtime(Work4)
Then we create a class for experiences.
class Employee { public: void doIt() { mWork1->doSmth(); mWork2->doSmth(); mWork3->doSmth(); mWork4->doSmth(); } private: inject(IWork, mWork1) inject(Work2, mWork2) inject(Work3, mWork3) inject(Work4, mWork4) };
And we use
Work4 w4; inject_set_runtime(Work4, &w4) Employee e1; e1.doIt();
The most important thing here is that the
Employee client class makes no assumptions about the lifetime of the embedded objects at all - the configuration directives
inject_as_XXX take care of this. In the full example, you can observe in more detail how the embedded objects feel when creating and destroying client objects.
That's all. Among the shortcomings of the method, I would like to note the need for potentially quite large and tightly coupled configuration files, which will have to include almost all the project sources. However, these files will be completely linear, and no one forbids pre-thinking about the breakdown of the configuration. But in the remaining parts of the project, the level of connectedness will significantly decrease, allowing the developer to concentrate better on the logic being implemented.