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:
- The implementation of GetOrInsert within each cache provider was essentially identical.
- 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.