Understanding IoC Container - Part 2
I try to lower expectations in order not to be disappointed, but in this case I was asked by several individuals to address the fact that IoC container power is in the ability to "hook" implementer with the contract through an external file, leaving application code unaware of the actual implementer till the run-time, having no reference to implementers' assembly or whatsoever. I am going to expand the sample from the part 1 post to achieve that goal in a couple of days.
In the last post we left with the application with an ApplicationStartup() method that would register all implementers against the contracts they implement. That causes a serious coupling between the ConsoleApp assembly and the one that implements the XmlLogger, AssemblyTwo. This is not a good idea, especially when we want to be able to replace the implementer without touching/modifying the application itself (by recompiling it).
Solution would be to take the code found in ApplicationStartup() method out of the code and express in a form of some configuration file that container would process. By doing that, we minimize coupling of the AssemblyTwo to Core only, and completely removing coupling of ConsoleApp on AssemblyTwo.
Now ConsoleApp has only references to what it really utilizes directly.
using System; using AssemblyOne; // IGadget using Core.IoC; // ILogger and Container
Next step is to describe the relationships between contracts and implementers. Something like an XML file should be ok.
Once this is place, the rest is just forcing Container to scan a file and look for Container.xml, registering all of the contracts and their implementers and passing that information back to the container for registration. To transform from a string presentation into a .NET type, I use reflection and that takes care of loading the desired assembly into memory. From there we get the type and register in container, allowing later instantiation.
The code that loads the XML file:
namespace Core.IoC { public sealed class XmlConfiguration { private readonly string filename; public XmlConfiguration() : this("Container.xml") { } public XmlConfiguration(string filename) { this.filename = filename; } public Dictionary<Type, Type> GetAllRegistrations() { Dictionary<Type, Type> result = new Dictionary<Type, Type>(); string fileWithPath = GetPathAndName(); if (File.Exists(fileWithPath)) { XmlReader reader = XmlTextReader.Create(fileWithPath); reader.MoveToContent(); while (reader.Read()) { if (reader.LocalName == "Register") { Type contract = BuildTypeFromAssemblyAndTypeName( reader.GetAttribute("Contract")); Type implementer = BuildTypeFromAssemblyAndTypeName( reader.GetAttribute("Implementer")); result.Add(contract, implementer); } } } return result; } private Type BuildTypeFromAssemblyAndTypeName(string fullTypeName) { int commaIndex = fullTypeName.IndexOf(","); string typeName = fullTypeName.Substring(0, commaIndex).Trim(); string assemblyName = fullTypeName.Substring(commaIndex + 1).Trim(); return Assembly.Load(assemblyName).GetType(typeName, false, false); } private string GetPathAndName() { string[] split = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, filename) .Split(new string[] {"\\bin\\"}, StringSplitOptions.RemoveEmptyEntries); return Path.Combine(split[0], filename); } } }
Updated Container class will have to reflect changes:
using System; using System.Collections.Generic; namespace Core.IoC { public class Container : IContainer { public static readonly IContainer Instance = new Container(); private readonly Dictionary<Type, Type> container; private Container() : this(new XmlConfiguration()) { } private Container(XmlConfiguration xmlConfiguration) { container = new Dictionary<Type, Type>(); foreach (KeyValuePair<Type, Type> pair in xmlConfiguration.GetAllRegistrations()) { AddImplemeterTypeForContractType(pair.Key, pair.Value); } } public void AddImplementerFor<ContractType>(Type implementer) { AddImplemeterTypeForContractType(typeof (ContractType), implementer); } private void AddImplemeterTypeForContractType(Type contractType, Type implementerType) { container.Add(contractType, implementerType); } public ContractType GetImplementerOf<ContractType>() { return (ContractType) Activator.CreateInstance( container[typeof (ContractType)]); } } }
What we've got now? An option of specifying implementers outside of the application code. Having this, teams (team members) can split up, code, and configure an external file to do the mapping between contract and implementer without touching the application code itself. Implementer is only coupled to the Core (where contract ILogger) is defined. The Core / AssemblyOne / ConsoleApp know nothing about implementer, but are able to leverage it to do the work.
Next steps? Well, we could talk about chains of dependencies, parameterized constructors, factories, etc. But this is where I am pausing and suggesting not to re-invent the wheel. Go grab some existing IoC container and pump your application to achieve it's best relying on technology that now you do not consider to be a magical anymore.
Updated code is here.