Sonntag, 24. September 2017

KittyDI or "How I wrote my own dependency injection container"

Since KittyDI is rather well developed (I mainly just miss the NuGet packaging and publishing stuff and can't find the motivtion to actually do the PRISM-Integration), this blogpost will be more of a retrospective than a developers diary. I still file it as such, because it is planned as the first part in a series about me creating two DI-Container implementations.
But first things first:

How it all began

In the beginning there was... a project at Zühlke. my employer. While I can't disclose much of the project, I can say that we worked on a complicated enterprise application using Autofac and PRISM. At one point we used Autofac to populate a list with ViewModels for the items to be displayed. I thought that to be a rather nice approach as we only had to resolve a function that takes the model as an argument and returns the viewmodel. Autofac then takes care of selecting a constructor where it can resolve all dependencies but the model with types registered to the container and put the model in as last missing dependency.
This all worked pretty well, but it took a long time for the list of viewmodels to fill (with about 100 items in the list of models). So I wondered aloud why it takes so long to create 100 instances of a type and a coworker replied "Well that's obvious. Autofac needs to go through all the strategies of creating the viewmodel for each of the 100 instances."
My first thought was "Really? Can't it just re-use the strategy it used the last time?" Of course I dismissed that thought as being too naive and instead considered implementing my own dependency injection container just to learn the pitfalls of doing so. I just never got around doing that.
In my current project we're using the lightweight Java dependency injection container "Feather" to create an Android project. Inspired by the simple idea that Feather implements (which was basically the same idea I had back then) I decided to finally start that. So yes, KittyDI is inspired by Feather, but no I never used their code as an actual example.
Why I chose KittyDI as name? Because I viewed this DI container as an experiment. A first, naive, maybe even childish try to implement a DI container and to mature by doing so. The relation to felines should be obvious while reading this blog. If it isn't, check the archives.

The goal

My main goal of course was to learn, maybe to fail and see why, to see where the complexety lies and to see if writing a DI container really is as hard as I thought and as people made me think it was. (Spoiler: It isn't.)

For dependency injection my goals were to offer a mixture of the features MEF, AutoFac and feather offer. This means:

  • Being able to resolve a type which is not yet known by the container 
  • Telling the container which type to resolve when an interface is requested 
  • Registering types as singletons 
  • Resolving all registered implementations of an Interface 
  • Resolving a factory for a type 
  • Resolving a factory that takes parameters 
  • Putting containers inside each other 
  • Creating and resolving containers 
  • Automatic disposal of container contents on container disposal 
  • Resolving generics
  • And of course remembering how a type got resolved last time
Of these features I only scrapped resolving all registered implementations of an Interface. 

The work

I implemented KittyDI in an incremental way. 
The first functionality that got added was simple resolving of an unknown type. The approach was to check the constructors of the requested type and selecting the one that fits. There was the first decision: how do I choose which constructor to use. I decided for a simple approach that doesn't force the user to add an attribute to the constructor of each type he wants to resolve (like MEF does). Instead I decided that KittyDI always uses a parameterless constructor if it is there. If there is no parameterless constructor but only a single constructor with parameters, it is used and the parameters are in turn resolved. Only if there is no parameterless constructor and instead multiple constructors taking parameters, the attribute is needed to tell KittyDI which one to use. This does two things at once: it enables the user to simply resolve types without the danger of missing the attribute and getting cryptic exceptions and it makes it possible to use types defined in libraries that don't know about KittyDI. This was a major flaw I personally saw in MEF. After KittyDI decided how to resolve a type, it wraps that in a function and stores it in a dictionary so it doesn't have to search for the right constructor again. 
In order to prevent endless resolution loops KittyDI hands the resolution stack (meaning the types of all factories currently in the call stack) to an internal factory and if a factory is called where it's type is already on the stack we are in a resolution loop and throw an exception notifying the caller of the circular dependency. 

The next step was rather simple. After refactoring out the functionality which creates the factory for a type if it's not yet in the dictionary, I added the possibility to resolve that exact factory by providing a special function for that. In fact normal resolution turned into "resolve the factory and then execute it". Spoiler: I removed that function after I added generic resolving and turned resolution of a factory into a generic resolved. 

Singletons are handled by KittyDI either by setting an optional boolean parameter to true when registering the type of by adding an attribute to the type. If the type should be a singleton, it's factory is wrapped by one that on first call executed the inner factory and then just returns the result of that call on subsequent calls. 

The next rather important point was the ability to tell KittyDI which type to use to resolve an interface. This was also pretty naïvely implemented: First the function which ensures that the factory for a given type had been resolved is called for the implementing type and then it is put into the dictionary of known factories as factory for the"contract type". This way it is not just possible to do that for interfaces but for all types provided the implementing type actually inherits from our implements the contract. 

Next up was generic resolution. This means that I created a list of "generic resolvers" which intercept requests and instead of searching for constructors on the generic type perform their own logic. This is now used to resolve factories but it was also used to resolve IEnumerable<T> - something that I removed after I also added a generic resolver for Lazy<T>. I wanted nesting of generics to be allowed and IEnumerable proved troublesome here with the architecture KittyDI evolved into. This I scraped resolution of enumerables and moved that to PantherDI where I aim to give the architecture a bit more thought instead of letting it evolve. 

Now adding the possibility to resolve a factory which takes parameters was rather easy. I just needed to register a generic resolver which all have full access to the container and trigger resolution with the tires given in the parameters already set. The change I had to make was to add another parameter to my internal factories which is a dictionary of all types provided by the caller. While doing so, I decided to move all the information passed into an internal factory into an object instead of having to change the signature of functions all over KittyDI each time I added something. 

To prepare for more complex scenarios, the container supports adding whole containers. This enables the concept of scoping. Each step that is taken to resolve a type (checking the dictionary of known resolutions, checking if a generic type can be resolved and registering the type as new type) will search the DI-Containers that have been added to the container on which the original request was made. Only if the step did not yield any result when performed on the whole tree of containers, then the next step will be started.

Creating child containers then was rather easy. The child container is an empty container which searches its parent before failing each step. After creation, Types can be registered to the child, but they won't be known to the parent.
At the same time resolving a container (resolving the types "DependencyContainer" and "IDependencyContainer") yields a new child on each resolution.
This enables services or view models to register types only to the child and then resolve a helper that uses those.

Last but not least the container supports disposal. If it is disposed, it will dispose all instances of singletons that it has created as well as all child containers too.

Conclusion

As I already said writing a DI container is not that hard. But my main takeaway is - and I think that is a general rule in software development - that you should think beforehand about how you want to achieve your goal unless you want your code to become more and more messy and complicated to read over time.
Even though the PRISM integration isn't done, I see KittyDI as done and the experiment a success (KittyDI can be used in projects) and have many ideas - also on what to think about beforehand - for the mature version "PantherDI" (by now the source of the name should be obvious to the reader).

Further links

Keine Kommentare:

Kommentar posten