⬆️ ⬇️

Do-it-yourself Property Injection (Xamarin / .Net)

In this article we will look at how Property Injection differs from Constructor Injection and will implement the first one in addition to the last on the basis of a small DI container in the source code.



This is an entry level tutorial. It will be useful to those who are not familiar with DI-containers or are interested in how it is arranged from the inside.



What is it and where is it used



Dependency injection (Injection) - a common design pattern used to create programs with weak connectivity components. Newbies usually meet him on projects where unit tests are applied.



A classic example of a rigidly connected system is when the hotel is tightly connected to the hairdryer with the power supply (the owners are worried about the safety of the device). The length of the wire is generally sufficient, but with this approach it is impossible to modify the hair dryer separately from the wall, and the wall separately from the hair dryer. The introduction of loose coupling in the form of a socket and a plug solves this problem.

')

Another example is the rigid binding of employees to the workplace, and also the need to go to work every day. This rigid connectedness in modern companies is increasingly weakened both by the opportunity to work from home and by the introduction of the practice of the absence of a permanent workplace: if you want to come to the office - do not forget to forget the place. From the point of view of the employer, the work will be done regardless of the location of the employee. There are, of course, additional risks, but such is the price of greater flexibility.



The idea of ​​introducing dependencies in the narrow sense is that the class-consumer of a certain functionality refers to the provider class not directly, by creating its instance, but indirectly, by reference to a certain interface type. The provider class is only required to comply with the specified interface.



image



Unlike the service locator, here the classes not only know nothing about each other, but also do not contain an appeal to the intermediary class (service locator).



In order to associate an interface with a specific class (or class instance), a so-called DI container is often used, in which the interface and implementation relationships are registered in advance. For languages ​​that do not support reflections, that is, they do not allow to operate with type properties, you can limit yourself to such a template as composition root (consideration of this template is beyond the scope of this article).



This approach provides flexibility: you can always use another one, depending on the circumstances, instead of one interface implementation.



The natural way to pass references to a class-consumer is to specify them in the parameters of its constructor. As a rule, when requesting the desired interface, the DI-container automatically runs through the list of parameters and recursively satisfies all dependencies.



Another way is to pass references through properties, which is called Property Injection. To satisfy the links, the DI-container goes over the list of properties and assigns the necessary links to those that match certain criteria.



There is a more rare way - Method Injection, where links are passed through a special method (for example, Init () with a list of dependencies as parameters). This approach is similar to constructor injection, but it works after the creation of an object instance. This term also means something completely different, namely, the simple transfer of dependencies as parameters to an arbitrary method.



Is this a regular bike



There are many DI containers (for example, Unity, NInject, Autofac, Spring, Castle Windsor, TinyIoc, MvvmCross). Most of them support Property Injection.



But sometimes there are situations when, for one reason or another, a third-party DI container does not suit you, and you have to write your own.



For example, corporate policies prohibit the use of third-party libraries. Or the libraries you need do not exist in nature. Or these libraries do not satisfy project requirements.



In this case, it is not difficult to make your own container - unless, of course, it is technically possible within the framework of the applied development technology, as is possible in .Net.



Why not satisfied with constructor injection



The introduction of dependencies through the parameters of the constructor requires the creation of service variables in the user class in which the references from the constructor parameters are saved. This is a routine operation that, in an amicable way, could somehow be automated. But as long as there is no such automation, the programmer has to do this work. Each new dependency is added first to the constructor parameters, then to the service variables, then a line of assignment of one to another is added to the constructor.



It is often possible to observe how any of the added parameters are pulled through the code, from the top-level class to the end user down the inheritance hierarchy. The chain of calls stretches like a bunch of sausages, from the constructor to the constructor. Not only that the serial transmission of these parameters by itself clutters the code. So also the addition of the next parameter to the end of this chain of calls requires its modification throughout. Which is quite uncomfortable. As a result, a class can solidly "swell up" only because of all this machinery, even before it starts doing anything.



Why not save the programmer from this routine work and not delegate the creation of entities to a DI container?



It can also be noted that when passing parameters to the constructor, they are usually passed without specifying the parameter name. That is, the class user is forced to rely either on the sequence number of the parameter, or explicitly indicate the name of the parameter, which will clutter the code even more.



There is a good rule that if it becomes inconvenient for you to add another dependency to the parameters of the designer, it means that there are really too many of them, and you should reconsider the design. Usually it is 5 pieces - the natural limit of objects that a person without training can hold in the area of ​​his attention.



We, however, believe that the key word in this rule is inconvenient. I would like to avoid it.



Own DI container



The implementation of the simplest DI container is not difficult to find in the source code. The author once used a small example from Xamarin University (see here ).



There is a lot lacking in comparison with industrial DI-containers (singletones, domains, etc.), but this is beyond the scope of this article.



The container allows you to register the relationship between the interface and the implementation (the Register method in different versions, which simply adds an element to the Dictionary), to then, in the Resolve method, create an instance of the class that corresponds to the requested interface and do the same parameters of its constructor.



This container is based on Reflections and the Activator.CreateInstance () method. The latter method is used to create an instance of a class by its type, and reflections allow you to read type properties.



To implement property injection, we add a special attribute that will mark the properties that the container should treat as dependencies.



Name the attribute “ResolveAttribute”. To do this, you must create a descendant class of System.Attribute (see Appendix A).



Add attribute processing in the container's Resolve method.



public object Resolve(Type type) { object result = null; ... //Inject [Resolve] marked properties var props = targetType.GetRuntimeProperties() .Where(x => x.CanWrite && x.IsDefined(typeof(ResolveAttribute))); foreach (var item in props) { item.SetValue(result, Resolve(item.PropertyType)); } return result; } 


This is all that needs to be done in the source container in order for dependencies to be implemented through properties (see Appendix B for the source code).



Usage example



Suppose there is a class in which dependency injection is used through a constructor (of course, dependencies must be registered in advance - see Appendix C). Note that the parameters and the body of the constructor are a purely serving code:



  public class MediaRecorder : IMediaRecorder { private readonly IMediaPlayer player; private readonly IRestServiceClient restClient; private readonly ILog log; private readonly IFileService fileService; private readonly ISettingsProvider settingsProvider; public MediaRecorder(IMediaPlayer player, IRestServiceClient restClient, ILog log, IFileService fileService, ISettingsProvider settingsProvider) { this.player = player; this.restClient = restClient; this.log = log; this.fileService = fileService; this.settingsProvider = settingsProvider; } } 


We modify the class using property injection:



  public class MediaRecorder : IMediaRecorder { [Resolve] public IMediaPlayer Player { get; set; } [Resolve] public IRestServiceClient RestClient { get; set; } [Resolve] public ILog Log { get; set; } [Resolve] public IFileService FileService { get; set; } [Resolve] public ISettingsProvider SettingsProvider { get; set; } public MediaRecorder() { } } 


As you can see, the amount of service code has decreased, and the code itself has become more readable.



The downside of success is a reduction in the ability to control the integrity of the program from the compiler. However, this is a common consequence for all types of IoC implementations.



It should also be borne in mind that it is considered normal practice to use constructor injection for mandatory dependencies, and property injection for optional. That is why the properties in our example have the access modifier “public”.



Public access to dependencies makes it possible to substitute their values ​​from the outside, which, on the one hand, serves as a tool for implementing dependencies manually, and on the other, it allows inadvertently damage the work of the whole class if it is not written reliably.



The code in our implementation does not check the access level of the property, that is, you can use both “public” and “private”. The latter is recommended for those properties, the arbitrary modification of which is undesirable. This will protect the class from unintended misuse and, at the same time, allow the use of property injection.



Performance



As you can see from our implementation, to support Property Injection, you need to run through all the properties of the class using reflections. It may turn out that there are quite a few properties - in practice, there are more than a thousand, which slows down the work of the program.



For this reason, this approach is not very suitable for classes with a large number of properties. Here optimization is possible both from the side of the user code, and from the side of the container itself.



For example, it would be possible to reduce the list of properties by placing in the service class those of them that are not labeled “ResolveAttribute” (in our implementation), to think about deferred reading (lazy loading). And the container could cache the list of properties for implementation. But we will talk about this some other time.



At the same time, Moore's law is still working, and the computing power of computers is growing. This allows us to use more and more complex algorithms.



In conclusion, we note that different DI containers have different performance, and this difference may be significant (see a small study ).



Appendix A
 using System; namespace Bricks { [AttributeUsage(AttributeTargets.All)] public sealed class ResolveAttribute : Attribute { } } 




Appendix B
 using System; using System.Collections.Generic; using System.Linq; using System.Reflection; namespace Bricks { /// <summary> /// This is a very simple example of a DI/IoC container. /// </summary> public sealed class DependencyContainer : IDependencyContainer { readonly Dictionary<Type, Func<object>> registeredCreators = new Dictionary<Type, Func<object>>(); readonly Dictionary<Type, Func<object>> registeredSingletonCreators = new Dictionary<Type, Func<object>>(); readonly Dictionary<Type, object> registeredSingletons = new Dictionary<Type, object>(); object locker = new object(); /// <summary> /// Register a type with the container. This is only necessary if the /// type has a non-default constructor or needs to be customized in some fashion. /// </summary> /// <typeparam name="TAbstraction">Abstraction type<typeparam> /// <typeparam name="TImpl">Type to create</typeparam> public void Register<TAbstraction, TImpl>() where TImpl : new() { registeredCreators.Add(typeof(TAbstraction), () => new TImpl()); } /// <summary> /// Register a type with the container. This is only necessary if the /// type has a non-default constructor or needs to be customized in some fashion. /// </summary> /// <param name="creator">Function to create the given type.</param> /// <typeparam name="T">Type to create</typeparam> public void Register<T>(Func<object> creator) { registeredCreators.Add(typeof(T), creator); } /// <summary> /// Register a type with the container. This is only necessary if the /// type has a non-default constructor or needs to be customized in some fashion. /// </summary> /// <typeparam name="TAbstraction">Abstraction type<typeparam> public void RegisterInstance<TAbstraction>(object instance) { registeredCreators.Add(typeof(TAbstraction), () => instance); } /// <summary> /// Creates a factory for a type so it may be created through /// the container without taking a dependency on the container directly. /// </summary> /// <returns>Creator function</returns> /// <typeparam name="T">The 1st type parameter.</typeparam> public Func<T> FactoryFor<T>() { return () => Resolve<T>(); } /// <summary> /// Creates the given type, either through a registered function /// or through the default constructor. /// </summary> /// <typeparam name="T">Type to create</typeparam> public T Resolve<T>() { return (T)Resolve(typeof(T)); } /// <summary> /// Creates the given type, either through a registered function /// or through the default constructor. /// </summary> /// <param name="type">Type to create</param> public object Resolve(Type type) { object result = null; var targetType = type; TypeInfo typeInfo = type.GetTypeInfo(); if (registeredSingletonCreators.TryGetValue(type, out Func<object> creator)) { lock (locker) { if (registeredSingletons.ContainsKey(type)) { result = registeredSingletons[type]; } else { result = registeredSingletonCreators[type](); registeredSingletons.Add(type, result); } } if (result != null) { targetType = result.GetType(); } } else if (registeredCreators.TryGetValue(type, out creator)) { result = registeredCreators[type](); if (result != null) { targetType = result.GetType(); } } else { var ctors = typeInfo.DeclaredConstructors.Where(c => c.IsPublic).ToArray(); var ctor = ctors.FirstOrDefault(c => c.GetParameters().Length == 0); if (ctor != null || ctors.Count() == 0) { result = Activator.CreateInstance(type); } else { // Pick the first constructor found and create any parameters. ctor = ctors[0]; var parameters = new List<object>(); foreach (var p in ctor.GetParameters()) { parameters.Add(Resolve(p.ParameterType)); } result = Activator.CreateInstance(type, parameters.ToArray()); } } //Create [Resolve] marked property var props = targetType.GetRuntimeProperties() .Where(x => x.CanWrite && x.IsDefined(typeof(ResolveAttribute))); foreach (var item in props) { item.SetValue(result, Resolve(item.PropertyType)); } return result; } public void Clear() { registeredCreators.Clear(); registeredSingletonCreators.Clear(); registeredSingletons.Clear(); } public void RegisterSingleton(Type tInterface, object service) { RegisterSingleton(tInterface, () => service); } public void RegisterSingleton<TAbstraction>(TAbstraction service) where TAbstraction : class { registeredSingletonCreators.Add(typeof(TAbstraction), () => service); } public void RegisterSingleton<TAbstraction, TImpl>() where TAbstraction : class where TImpl : new() { registeredSingletonCreators.Add(typeof(TAbstraction), () => new TImpl()); } public void RegisterSingleton(Type tInterface, Func<object> serviceConstructor) { registeredSingletonCreators.Add(tInterface, serviceConstructor); } public void RegisterSingleton<TAbstraction>(Func<TAbstraction> serviceConstructor) where TAbstraction : class { registeredSingletonCreators.Add(typeof(TAbstraction), serviceConstructor); } } } 


Appendix C
  public static class Bootstrap { public static readonly IDependencyContainer Container = new DependencyContainer(); public static void Init() { Container.Clear(); Container.RegisterSingleton<ILog, LogWriter>(); Container.RegisterSingleton<ISettingsProvider>(() => new SettingsProvider()); Container.RegisterSingleton<IFileService>(() => new FileService()); Container.RegisterSingleton<IMessagesProvider>(() => new MessagesProvider()); Container.Register<IMediaPlayer, MediaPlayerService>(); Container.Register<IRestServiceClient, RestServiceClient>(); Container.Register<IMediaRecorder, MediaRecorder>(); ... } } 

Source: https://habr.com/ru/post/352304/



All Articles