Extension Methods on Types You Own?

It’s no secret that I’m a fan of using extension methods to make code more concise and expressive. This is particularly handy for enhancing APIs outside of your control, from the base class library to ASP.NET MVC and SharePoint. However, there are certain situations where it might be useful to use extension methods even though you have the option to add those methods to the class or interface itself. Consider this simplified caching interface:

public interface ICacheProvider
{
    T Get<T>(string key);
    void Insert<T>(string key, T value);
}

And a simple application of the decorator pattern to implement a cached repository:

public class CachedAwesomeRepository : IAwesomeRepository
{
    private readonly IAwesomeRepository awesomeRepository;
    private readonly ICacheProvider cacheProvider;

    public CachedAwesomeRepository(IAwesomeRepository awesomeRepository, ICacheProvider cacheProvider)
    {
        this.awesomeRepository = awesomeRepository;
        this.cacheProvider = cacheProvider;
    }

    public Awesome GetAwesome(string id)
    {
        var awesome = cacheProvider.Get<Awesome>(id);
        if(awesome == null)
            cacheProvider.Insert(id, (awesome = awesomeRepository.GetAwesome(id)));
        return awesome;
    }
}

So far, so good. However, as caching is used more often it becomes clear that there’s a common pattern that we might want to extract:

    T ICacheProvider.GetOrInsert<T>(string key, Func<T> valueFactory)
    {
        T value = Get<T>(key);
        if(value == default(T))
            Insert(key, (value = valueFactory()));
        return value;
    }

Which would reduce GetAwesome() to a single, simple expression:

    public Awesome GetAwesome(string id)
    {
        return cacheProvider.GetOrInsert(id, () => awesomeRepository.GetAwesome(id));
    }

Now I just need to decide where GetOrInsert() lives. Since I control ICacheProvider, I could just add another method to the interface and update all its implementers. However, after starting down this path, I concluded this was not desirable for a number of reasons:

  1. The implementation of GetOrInsert within each cache provider was essentially identical.
  2. Tests using a mocked ICacheProvider now needed to know if the code under test used GetOrInsert() or Get() + Insert(), coupling the test too tightly to the implementation. Furthermore, natural tests along the lines of “Should return cached value” and “Should insert value from repository if not cached” were replaced with a single implementation-specific test: “Should return value from GetOrInsert”.
  3. Most importantly, I came to realize that GetOrInsert() really just isn’t something that a cache does, so why should it be part of the interface?

So instead I have a handy GetOrInsert() extension method (conversion is left as an exercise for the reader) that I can use to clean up my caching code without needing to change any of my cache providers or tests for existing consumers.

The question is really analogous to whether or not Select() and Where() should be part of IEnumerable<T>. They are certainly useful ways to consume the interface, just as GetOrInsert() is, but they exist outside of what an IEnumerable<T> really is.

About these ads
Posted in .NET. Tags: . Comments Off
Follow

Get every new post delivered to your Inbox.

%d bloggers like this: