Refactoring to improve maintainability & blendability using IoC part 2: Reducing IoC container coupling
Recap of part 1
In part 1 of this series we started reducing the Service Locator calls by removing them from the view models and moving it to the ViewModelFactory since this class is responsible for creating the view models. This allowed us to cut the ties in the view models where they were requesting dependencies from a global container.
Some of the benefits we gained from the first iteration of refactoring:
- Reduced coupling between view models and the IoC container.
- View model dependencies are push not pull. This removes coupling between the view model and the source of the dependencies.
- View models are more intention revealing. What view models depend on and require is explicit and not hidden.
- View models are easier to test. An IoC container isn’t required to initialize a unit test.
After all of the changes in the first iteration we still retained Blendability and did not have to compromise the design time experience.
This still left us with a global container which the ViewModelFactory calls to resolve dependencies for view models. Once our view models were up and running again it was time to remove the global container and dramatically reduce the coupling.
Why a type asking the IoC container to resolve its dependencies is bad
Having view models use a Service Locator or request the IoC container to compose the dependencies on its behalf couples the view model to the source of its dependencies. The view model does not need the IoC container itself to do any work. It only needs its dependencies. The view model requesting the dependencies has created a requirement on the view model for the IoC container to exist although it is not needed when you look at what the view model is responsible for.
This is brittle and instead should be handled declaratively. With service locator the view model is dependent on the IoC container which needs to be exposed as global state (so that it is accessible) whereas with dependency injection any IoC container can handle the creation.
This includes your tests. The global container is required for your objects to do their work. With dependency injection, testing your view models becomes simpler.
My original idea to minimize the coupling to a specific IoC container was to introduce IServiceLocator so that I could create implementations for various containers like Funq and Unity. What I found out was that it was easy to abstract the resolve methods of these IoC containers but abstracting the register methods was proving to be far more difficult. I was experiencing a lot of friction and it wasn’t clear how I could achieve this.
I learned a couple more things from this experience. It’s really difficult to abstract 2 things that do similar things but are implemented differently. The abstraction can become complex and the benefits start to fade.
What we did about the IoC container
As mentioned above rather than abstract the IoC containers with a layer of API that looks just like another IoC container we took the approach of encapsulation. Encapsulating the IoC container instead of abstracting it allowed us to still retain the ability to create new implementations based on different IoC containers but remove the coupling throughout the code base.
We introduced 2 key things to contribute to removing dependency on the IoC container. A Composition Root and ViewModelResolver.
DEFINITION A Composition Root is a (preferably) unique location in an application where modules are composed together.
A DI Container should only be referenced from the Composition Root. All other modules should have no reference to the container.
We had to bend the rules a little bit to allow for ViewModelResolver but I think you will find it an acceptable solution because we closed off any abilities of container abuse.
If you recall earlier we had an ApplicationHost.Current singleton which exposed an ApplicationHost.Current.Container property so that we could resolve abuse dependencies anywhere we wanted. We replaced this with the Composition Root which we named Bootstrapper.
|1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31||
As you can see in the singleton I exposed the container making it available for abuse to people writing code against it and coupling to it all over the place. Things can get out of control fast and become difficult to maintain and not flexible to change going forward.
|1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26||
In the Bootstrapper which is our Composition Root, we create the object graph dependencies in the Initialize() method. If you recall in part 1 we modified view models to support constructor injection. Those constructor parameters map to this composition we declare in the Bootstrapper. The types we define will get injected in the constructors of the view models when the IoC container resolves them.
It is important all your dependencies are declared in the Composition Root or else you will get a failure when you try to resolve one of the view models.
Also very key is we have not exposed the IoC container at all. We are completely closing off the ability for arbitrary code being able to call the container directly.
In the App.cs we instantiate the Bootstrapper to kick off the container registrations.
One thing still remains that is referencing the IoC container. In part 1 we moved the container resolve calls from the view models to the ViewModelFactory in our iterative refactoring. Now that view models are not aware of the IoC container and we have a Composition Root in place we need to find a solution to remove the coupling between ViewModelFactory and the IoC container.
We did this by introducing IViewModelResolver.
My original idea of abstracting the IoC container into an IServiceLocator interface introduced an array of problems such as:
- Still coupling view models to a generic Service Locator interface. View models still require a Service Locator to do work.
- Sacrificing the power of each IoC implementation because the abstract layer on top would be a compromised feature set.
- Fighting a lot of friction trying to make API’s that aren’t common work in a common way.
IViewModelResolver allows us to do the opposite. We can encapsulate the IoC container implementation and leverage that IoC containers full feature set because we aren’t exposing it. We can also implement different IoC container types easily.
In the Composition Root we declared our view models and their dependency composition. The view model resolvers will be used by the ViewModelFactory to resolve the view models. The view models just as before will be injected by their constructors.
As a side benefit we are achieving a better level of Single Responsibility Principle because the ViewModelFactory is responsible for providing run time or design time view models and the view model resolvers are responsible for resolving dependencies against a specific IoC container.
The resolver is also very easy to mock making it easier to test the ViewModelFactory than it was when we had the globally exposed IoC container with Service Locator requests.
In our case since we are using Funq, we created a FunqViewModelResolver implementation of IViewModelResolver.
|1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34||
Now that the Funq version of the resolver is implemented we need to refactor ViewModelFactory again to use the resolvers instead of the Service Locator call that we broke removed when we deleted the globally exposed IoC container.
|1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33||
|1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50||
We introduced a new static method on the ViewModelFactory named InitializeResolver(). Like the other dependencies in the system we can initialize the ViewModelFactory with the proper resolver in the Composition Root.
The reason this needs to be static is because the ViewModelFactory is created in XAML as a resource so that it can be statically bound. The static method is to allow the resolver to be injected manually because the ViewModelFactory is not created by the IoC container and cannot resolve it’s dependencies.
Now we need to update the Bootstrapper.Initialize() method.
|1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16||
|1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21||
This is how we use the ViewModelFactory in XAML (leaving out the less important markup). This is all created via Blend, no hand written XAML required. A designer can hook this up in a few clicks.
|1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21||
Here’s how that XAML is created by the designer in Blend.
This is how the view looks with the DataContext set within Expression Blend. Full design time support!
After this 2nd iteration of refactoring we have now encapsulated the IoC container and removed it from being globally accessible and abused. This is important because we enforce a clean path rather than opening the container up and allowing people to go in a wrong direction. We do use the IoC container in the ViewModelResolver but it is a specialized implementation to solve a specific problem and does not open the door for abuse.
Our code base is even more maintainable than it was after part 1 which also raises the level of testability. Our view models were already very testable after part 1 but the ViewModelFactory is now testable without requiring an IoC container by mocking the resolvers.
We reduced a lot of coupling and increased testability quite a lot in these 2 iterative refactoring steps!
Stay tuned for more improvements!
- Scalable Eventually Consistent Counters
- Create benchmarks and results that have value
- Routing aware master elections
- My new test lab
- Responsible benchmarking
- Understanding hardware still matters in the cloud
- The “network partitions are rare” fallacy
- Messaging and event sourcing
- Further reducing memory allocations and use of string functions in Haywire
- HTTP response caching in Haywire
- Atomic sector writes and misdirected writes
- How memory mapped files, filesystems and cloud storage works
- Hello haywire
- Active Anti-Entropy
- Lightning Memory-Mapped Database
- Write amplification
- Amortizing de-duplication at read time instead of write time
- LevelDB was designed for mobile devices
- AMQP and wire format interopability
- Convergent Replicated Data Types
- March 2014
- February 2014
- January 2014
- November 2013
- October 2013
- August 2013
- July 2013
- June 2013
- May 2013
- April 2013
- March 2013
- January 2013
- October 2012
- September 2012
- August 2012
- May 2012
- April 2012
- February 2012
- January 2012
- December 2011
- September 2011
- July 2011
- June 2011
- May 2011
- April 2011
- March 2011
- February 2011
- December 2010
- November 2010
- October 2010
- September 2010
- August 2010
- July 2010
- June 2010
- May 2010