When creating software components, it is often forgotten that it has to be easy to use. A lot of components are designed with an IoC and lots of unit tests. When using the component, the parent software should not need to create all the internal dependencies. This requires internal knowledge of the library which should not be required. A default way of creating a new instance should be supplied.
The article demonstrates different ways of creating new instances of software components.
Code: https://github.com/damienbod/LibraryDependencies
Here’s an example of a basic library. The component is used by creating an instance of the IMyLibrary. This has dependencies to other interfaces. And again the child interfaces have dependencies to more interfaces.
Even from this simple library component, it requires a lot of construction to create an instance of the library. For large components, this would become unmanageable.
private static IMyLibrary ResolveLibraryNew() { return new MyLibrary( new DefaultLogProvider(), new LogicB( new DefaultLogProvider(), new LogicC(new DefaultLogProvider(), new LogicD(new DefaultLogProvider()))), new LogicA(new DefaultLogProvider()) ); }
How can we make it easier to use?
First solution; Create a basic DependencyResolver class. This class is very primitive. It just provides all the interfaces as properties and these properties can be replace with the parent project specific interface implementations, for example a logger.
using MyLibraryComponent.Log; using MyLibraryComponent.LogicA; using MyLibraryComponent.LogicB; using MyLibraryComponent.LogicB.LogicC; using MyLibraryComponent.LogicB.LogicC.LogicD; namespace MyLibraryComponent { public class DefaultDependencyRosolver { public ILogProvider LogProvider { get; set; } public ILogicA LogicA { get; set; } public ILogicB LogicB { get; set; } public ILogicC LogicC { get; set; } public ILogicD LogicD { get; set; } public IMyLibrary MyLibrary { get; set; } public void ResolveDependencies() { if (LogProvider == null) LogProvider = new DefaultLogProvider(); if (LogicD == null) LogicD = new LogicD(LogProvider); if (LogicC == null) LogicC = new LogicC(LogProvider, LogicD); if (LogicB == null) LogicB = new LogicB.LogicB(LogProvider, LogicC); if (LogicA == null) LogicA = new LogicA.LogicA(LogProvider); if (MyLibrary == null) MyLibrary = new MyLibrary(LogProvider, LogicB, LogicA); } } }
It is much easy to use the library now. The internals of the library do not need to be understood, it just needs to be constructed and it’s ready to use. If required, any part of the library can be replaced with a user specific implementation. The library remains flexible, easy to test and can be extended.
private static IMyLibrary ResolveLibraryDefaultDependencyRosolver() { var dependencyRosolver = new DefaultDependencyRosolver(); // You could use your own specific logger here... dependencyRosolver.LogProvider = new DefaultLogProvider(); dependencyRosolver.ResolveDependencies(); return dependencyRosolver.MyLibrary; }
Using the library is simple.
private static void Main(string[] args) { //IMyLibrary myLibrary = ResolveLibraryNew(); IMyLibrary myLibrary = ResolveLibraryDefaultDependencyRosolver(); //IMyLibrary myLibrary = ResolveLibraryUsingUnity(); //IMyLibrary myLibrary = ResolveLibraryDefaultDependencyRosolverWithUnity(); //IMyLibrary myLibrary = ResolveUsingAnotherDependencyResolver(); myLibrary.LibraryMethodA(); myLibrary.LibraryMethodB(); myLibrary.LibraryMethodC(); Console.ReadKey(); }
Another solution would be to use an IoC container and create everything in the parent application. This works good, but internal knowledge of the library is required. Here’s an example using Unity.
using System; using Microsoft.Practices.Unity; using MyLibraryComponent; using MyLibraryComponent.Log; using MyLibraryComponent.LogicA; using MyLibraryComponent.LogicB; using MyLibraryComponent.LogicB.LogicC; using MyLibraryComponent.LogicB.LogicC.LogicD; namespace ConsoleApp { class UnityConfig { #region Unity Container private static readonly Lazy<IUnityContainer> Container = new Lazy<IUnityContainer>(() => { var container = new UnityContainer(); RegisterTypes(container); return container; }); public static IUnityContainer GetConfiguredContainer() { return Container.Value; } #endregion public static void RegisterTypes(IUnityContainer container) { container.RegisterType(typeof(ILogProvider), typeof(DefaultLogProvider), new TransientLifetimeManager()); container.RegisterType(typeof(ILogicA), typeof(LogicA), new TransientLifetimeManager()); container.RegisterType(typeof(ILogicB), typeof(LogicB), new TransientLifetimeManager()); container.RegisterType(typeof(ILogicC), typeof(LogicC), new TransientLifetimeManager()); container.RegisterType(typeof(ILogicD), typeof(LogicD), new TransientLifetimeManager()); container.RegisterType(typeof(IMyLibrary), typeof(MyLibrary), new TransientLifetimeManager()); } } }
And using it is very simple:
private static IMyLibrary ResolveLibraryUsingUnity() { var container = UnityConfig.GetConfiguredContainer(); return container.Resolve(typeof(IMyLibrary), "") as IMyLibrary; }
The first 2 solutions could be mixed, so only specific interfaces need to be resolved with unity.
private static IMyLibrary ResolveLibraryDefaultDependencyRosolverWithUnity() { var dependencyRosolver = new DefaultDependencyRosolver(); var container = UnityConfig.GetConfiguredContainer(); dependencyRosolver.LogProvider = container.Resolve(typeof(ILogProvider), "") as ILogProvider; dependencyRosolver.ResolveDependencies(); return dependencyRosolver.MyLibrary; }
A more generic solution could also be used which would make it easier to extend the library as it increases in size. This example uses just simple construction injection for dependencies. The default library interfaces are registered. If the parent solution requires a specific interface implementation, the default registry is removed and a the new one is added. Due to this the end user of the library does not need to know anything about the internals of the component.
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using MyLibraryComponent.Log; using MyLibraryComponent.LogicA; using MyLibraryComponent.LogicB; using MyLibraryComponent.LogicB.LogicC; using MyLibraryComponent.LogicB.LogicC.LogicD; namespace MyLibraryComponent { public class AnotherDependencyResolver { public AnotherDependencyResolver() { RegisterDefaultTypes(); } private readonly Dictionary<Type, Type> _dependencies = new Dictionary<Type, Type>(); public T Resolve<T>() { return (T) Resolve(typeof (T)); } public void Register<TFrom, TTo>() { if (_dependencies.ContainsKey(typeof (TFrom))) { _dependencies.Remove(typeof (TFrom)); } _dependencies.Add(typeof (TFrom), typeof (TTo)); } private object Resolve(Type type) { Type resolvedType = LookUpDependency(type); ConstructorInfo constructor = resolvedType.GetConstructors().First(); ParameterInfo[] parameters = constructor.GetParameters(); if (!parameters.Any()) { return Activator.CreateInstance(resolvedType); } return constructor.Invoke( ResolveParameters(parameters).ToArray()); } private Type LookUpDependency(Type type) { return _dependencies[type]; } private IEnumerable<object> ResolveParameters(IEnumerable<ParameterInfo> parameters) { return parameters.Select(p => Resolve(p.ParameterType)).ToList(); } private void RegisterDefaultTypes() { Register<ILogProvider, DefaultLogProvider>(); Register<ILogicA, LogicA.LogicA>(); Register<ILogicB, LogicB.LogicB>(); Register<ILogicC, LogicC>(); Register<ILogicD, LogicD>(); Register<IMyLibrary, MyLibrary>(); } } }
This is then very easy to use.
private static IMyLibrary ResolveUsingAnotherDependencyResolver() { AnotherDependencyResolver anotherDependencyResolver = new AnotherDependencyResolver(); anotherDependencyResolver.Register<ILogProvider, DummyLogProvider>(); return anotherDependencyResolver.Resolve<IMyLibrary>(); }
In the above code, the logger was replaced with the solution specific dummy logger.
When designing libraries, always try to make it simple to use. Libraries which are complicated to use are no good. You’re better off not using them and rewriting the code yourself. Libraries also need to be flexible as some end solutions use IoC containers, others use factories and more use just plain old new…
A good library needs to support all of these. A good library is easy the extend. It is important that any part of the component can be replaced.
when all this is done, deploy it to your NuGet server or a public NuGet server. Now your component is ready to use…
KISS. (Keep it simple and stupid and keep it simple and standard)