Functional Caching Part 4: Dependency Injection
- Functional Caching
- Functional Caching Part 2: Concurrency
- Functional Caching Part 3: Decoration
- Functional Caching Part 4: Dependency Injection
Last time, we explored how to work caching functions into an application. We deconstructed the solution, then composed its elements in various ways to tweak behavior.
We also identified a pattern: assembling an object graph. The process, known as Dependency Injection, is very mechanical; it requires creating and managing an ever-increasing number of objects, each of which can be arbitrarily complex. That manual labor is a significant obstacle to writing in the Dependency Injection style.
This issue has spawned several frameworks, known as Inversion of Control containers. These systems separate the need for another object from the details of how to get it.
This is exactly what we did before: OrderDiscountService needs an instance of IOrderRepository, and doesn’t know or care that we used CachedOrderRepository to fulfill the contract. Accordingly, this technique fits neatly into a container.
A container creates and manages the object instances used in an application. It is an ecosystem, where each object is an inhabitant that has its own unique relationships with other objects. Object relationships are described as B-depends-on-A, where B is an object that calls upon object A to do some work.
Since B needs an instance of A, it has a choice:
- Create A directly
- Retrieve A from a static property or method
- Accept A through a constructor argument
- Accept A through a settable property
3 and 4 externalize the dependency; B’s creator determines the instance of A. This makes B simpler than 1 or 2, as it doesn’t have to know how to call an external accessor. B is also more cohesive because it has less infrastructure code and is more focused on its core responsibility.
The main purpose of a container is to understand the contours of these relationships and use that knowledge to create fully-functional objects. This frees up developers to concentrate on the project-specific tasks in the classes, instead of all the connective tissue between them.
My container of choice is Autofac. It is clean and cohesive, with an excellent API*. The basic flow of Dependency Injection with Autofac is:
- Create a container builder
- Register all application types with the builder
- Build the container
- Resolve a top-level object out of the container
Here is how we would configure OrderDiscountService to use the non-caching OrderRepository:
This registers the OrderRepository type under the IOrderRepository interface, meaning any other object which has a dependency on IOrderRepository will be given an instance of OrderRepository. The InstancePerDependency instruction tells the container to create a new instance every time an object has a dependency on IOrderRepository.
Here is how we would resolve an instance of OrderDiscountService from the container:
The builder creates a container with all of the registrations. We then resolve an instance of OrderDiscountService and use it. The container encapsulates the messy, rote details of wiring the object graph. It inspects the constructor to determine its dependencies. When it finds one on IOrderRepository, it knows to create an instance of OrderRepository because of our prior registration.
To add caching, we simply have to change the object which is registered as IOrderRepository:
Here, we declare that when an object depends on IOrderRepository, we create an instance of OrderRepository and wrap it in CachedOrderRepository. Other objects remain oblivious to the new capabilities.
We use the context variable c to fulfill
OrderRepository‘s dependency on ISession. We have to spell out the dependencies because we want to run arbitrary registration code. This is what the container automagically does for us when we use the RegisterType method.
We also change the lifetime to SingleInstance, which tells the container to use the same instance to fulfill all dependencies. This extends the benefits of caching to all objects with minimal effort.
This series has explored caching as implemented in Dependency Injection style. First, we defined result caching for any method with a single parameter and a return value. Next, we defined concurrent caching, allowing multiple threads to safely invoke the caching method. Then, we seamlessly weaved caching into an implementation of IOrderRepository using the Decorator pattern.
In this final installment, we expressed the same object graph as last time, but in a declarative fashion. The boring tab-A-slot-B mechanics are the container’s specialty; by delegating that task, we gain more time to concentrate on providing business value.
* I got the chance to meet Nick Blumhardt, Autofac’s founder, at PDC 2008. Let’s just say I trust his code.