I've noticed that I have a 2 step pattern for learning new framework or language features. I'm guessing this is pretty typical for most people. First, I'll use the feature within framework classes or 3rd party ddls. Then I'll leverage it more directly within my own code. What's surprising to me is the length of time which occurs between step 1 and step 2.
Take generics for example. Back in the 1.x days, I wrote a ton of repetitive classes that inherited from CollectionBase. So when 2.0 came out, I immediately and aggressively started to use generic collections. However, it was quite some time later (a year?) until I wrote my own class that leveraged them directly. Today, I don't write a new generic class every day, but I do consider them an important part of my toolbox and kinda wonder what took me so long to take them up.
I have a feeling that many developers are in the same boat - it's easy to consume code that implements new features, but not so easy to grasp how to implement those same features ourselves.
As it turns out, the other day, I had another such ah-hah moment with the System.Func generic delegate. Like me, you've probably consumed it often, or at least one of its cousins: System.Action and System.Predicate. I thought I'd show how I used it, in hopes that it might open up some possibilities for you.
Ovewview
First though, a brief overview. The three delegates above are essentially shortcuts that save you from having to write your own common delegate. The most common one is probably Predicate<T>, which returns a boolean. Predicte<T> is used extensively by the List<T> and Array classes. The most obvious is the Exist method:
List<string> roles = user.Roles;
if (roles.Exists(delegate(string r) { return r == "admin";}))
{
//do something
}
or the lambda version (which I much prefer)
if (role.Exists(r => r == "admin))
{
}
Func<T> is a lot like Predicate, but instead of returning a boolean it returns T. Also, Func<T> has multiple overloads that let you pass 0 to 4 input parameters into the delegate. Action<T> is like Func<T> except it doesn't return anything - it does an action.
Code Decoupling
So, how can you make use of these within your own code? Well, here's what I did. First, I'm a big proponent of caching, as well as a big fan of unit testing. However, the two don't easily go hand-in-hand because Microsoft doesn't provide an interface to their built-in cache, which leads to tight coupling (which of course makes it difficult to change caching implementation down the road, and impossible to unit test). The first thing to do is create your own interface, a simple start might look like:
public interface ICacheManager
{
T Get<T>(string key);
void Insert(string key, object value);
}
Next comes our first implementation:
public class InMemoryCacheManager : ICacheManager
{
public T Get<T>(string key)
{
return (T) HttpRuntime.Cache.Get(key);
}
public void Insert(string key, object value)
{
HttpRuntime.Cache.Insert(key, value);
}
}
Func Fights Repitition
So, what does all this have to do with System.Func? Well, the above code is used in a very repetitive manner: get the value from the cache, if it's null, load it from somewhere and put it back in the cache. For example:
public User GetUserFromId(int userId)
{
ICacheManager cache = CacheFactory.GetInstance;
string cacheKey = string.Format("User.by_id.{0}", userId);
User user = cache.Get(cacheKey);
if (user == null)
{
user = _dataStore.GetUserFromId(userId);
cache.Insert(cacheKey, user);
}
return user;
}
After a year or so of writing code like this, I figured there must be a better way, which of course is where Func comes in. Ideally, we'd like to get the value, and provide our callback code all at once. So, let's change our interface:
public interface ICacheManager
{
T Get<T>(string key, Func<T> callback);
void Insert(string key, object value);
}
The second parameter is the delegate we'll want to execute if Get returns null. Of course our delegate will return the same type (T) as Get would - just like in the above case where we expect a User from both Get and our data store. Here's the actual implementation:
public T Get<T> (string key, Func<T> callback)
{
T item = (T) HttpRuntime.Cache.Get(key);
if (item == null)
{
item = callback();
Insert(key, item);
}
return item;
}
How do we use the above code?
public User GetUserFromId(int userId)
{
return CacheFactory.GetInstance.Get(string.Format("User.by_id.{0}", userId),
() => _dataStore.GetUserFromId(userId));
}
I know the () => syntax might be intimidating (especially if you aren't familiar with lambdas), but all it is is a parameterless delegate.
Of course, this system can easily be expanded to add additional caching instructions (absolute/sliding expiries, dependencies and so on) via overloaded Get<T> and Insert members.
(I just noticed this example also highlights how to use generics within your own code too!)
Posted
Thu, Jul 3 2008 8:15 PM
by
karl