Functional Caching Part 3: Decoration
- Functional Caching
- Functional Caching Part 2: Concurrency
- Functional Caching Part 3: Decoration
- Functional Caching Part 4: Dependency Injection
Now that we have established the cached function pattern and adapted it to add concurrency support, it’s time to put the functions in action.
First, let’s create a home for our GetOrder method:
public interface IOrderRepository
{
Order GetOrder(int id);
}
This interface represents a repository (data store) which contains orders, like a database, web service, or XML file. It abstracts away persistence details such as connection strings, URLs, file paths, and query mechanisms. Using an interface communicates to the reader that GetOrder is not defined in terms of any concrete details.
The only decision we’ve made so far is the signature of GetOrder; we haven’t had to consider class vs. struct, making the class abstract, sealing it, or making GetOrder virtual. We also haven’t locked implementers into a single inheritance hierarchy. By delaying these minor decisions, interfaces keep the focus on what needs to be done, not how it’s done.
Before we implement IOrderRepository, let’s see how it would be used:
public class OrderDiscountService
{
private readonly IOrderRepository _orders;
public OrderDiscountService(IOrderRepository orders)
{
_orders = orders;
}
public void ApplyDiscount(int orderId, double percentage)
{
var order = _orders.GetOrder(orderId);
order.ApplyDiscount(percentage);
}
}
OrderDiscountService declares its dependency on IOrderRepository by accepting it in the constructor. This separates the conceptual dependency on the GetOrder operation from any details of its execution. The actual IOrderRepository implementation is left up to configuration, a concern for some other class.
There is a subtle beauty in this. Classes define their boundaries through their dependencies. When those boundaries are interfaces, which have no inherent implementation, a class stands on its own: it can be fully understood without the mental weight of those external details.
An outstanding benefit of this style is that most classes start to look and feel the same: declare constructor dependencies and implement an interface using private methods. It normalizes complexity with a format that can handle problems large and small. This consistency also pays dividends over time by making it less painful to ease back into past codebases.
The Core Implementation
We can write GetOrder by querying an NHibernate session:
public sealed class OrderRepository : IOrderRepository
{
private readonly ISession _session;
public OrderRepository(ISession session)
{
_session = session;
}
public Order GetOrder(int id)
{
return _session.Linq<Order>().Where(order => order.Id == id).SingleOrDefault();
}
}
An NHibernate session represents a single conversation with a database. We don’t want to know the gory details of creating one of those suckers, so we declare a dependency on it instead. This communicates to the reader that this class participates in an existing conversation.
The query is fairly straightforward. We select the one order whose ID matches the parameter, or null if that ID doesn’t exist. This class is really only interesting because it is the NHibernate version of it; we can imagine similar Entity Framework or LINQ to SQL implementations.
Wrappers with Mad Cache
OrderRepository is very cohesive: its only concern is to query. Adding more functionality to it, such as caching, would muddle the class’s core intent. In situations such as this, we can use the Decorator pattern to wrap the class with new functionality.
Decoration here means implementing IOrderRepository again and also accepting it in the constructor. The outer GetOrder is written in terms of the inner one, altering it by using a cached version of the function:
public sealed class CachedOrderRepository : IOrderRepository
{
private readonly Func<int, Order> _getOrder;
public CachedOrderRepository(IOrderRepository innerRepository)
{
_getOrder = innerRepository.GetOrder;
_getOrder = _getOrder.Cached();
}
public Order GetOrder(int id)
{
return _getOrder(id);
}
}
We declare that GetOrder passes the call through to the _getOrder member. To initialize the function, first we assign it to the inner, uncached implementation:
_getOrder = innerRepository.GetOrder;
This means exactly the same as:
_getOrder = new Func<int, Order>(innerRepository.GetOrder);
The C# compiler is smart enough to see that _getOrder is of type Func<int, Order>, a delegate type, and allows us to assign the method directly, rather than have us type out something it can infer. Neat.
Next, we get a cached version of the function we just assigned:
_getOrder = _getOrder.Cached();
Voila! CachedOrderRepository connects caching to GetOrder – it doesn’t actually implement either of them. All three concepts are neatly separated, and each has a very small surface area for external changes to affect it. Our intent is clear: cache the inner repository’s GetOrder method.
Some Assembly Required
Like when creating a Lego structure or writing a story, we must weave our various building blocks together in a single context. This action is called composition, the natural yang to the yin of fine-grained objects. In our case, this means creating an object graph: calling the series of constructors necessary to create an instance of OrderDiscountService:
var session = …;
var orders = new OrderRepository(session);
var discountService = new OrderDiscountService(orders);
discountService.ApplyDiscount(7, 0.10);
We used the non-cached version of the repository because we are only applying a discount to a single order. There is no return on investment for caching the instance. However, if we were to apply a discount to a series of orders which may contain duplicates, we could use the cached version to avoid retrieving the same order multiple times:
var session = …;
var orders = new CachedOrderRepository(new OrderRepository(session));
var discountService = new OrderDiscountService(orders);
foreach(var orderId in new[] { 1, 2, 2, 3, 4, 5, 5, 5, 6, 7, 7 })
{
discountService.ApplyDiscount(orderId, 0.10);
}
The only difference is that we set orders to a cached version of the repository. OrderDiscountService has no idea that the IOrderRepository it receives is actually a graph with its own composed behaviors. The structure of the graph is a configuration detail of which none of the classes has any knowledge.
Summary
We defined an interface for our GetOrder example method and saw how it would be used. We discussed some benefits of interfaces and reflected on using them to create isolatable objects. We implemented IOrderRepository in a typical query-based fashion and seamlessly added caching using the Decorator pattern, keeping it separate from the core query concern.
We also created multiple configurations of these objects to address slight differences in context. Soon, we will see how we can simply declare these relationships and hand off the manual labor to a framework.