Wednesday, August 01, 2007 10:28 PM
I'm currently reading through
Michael Feather's Working Effectively with Legacy Code. I had skimmed it sometime ago but as I'm currently trying to wedge unit tests around some 6 month old code that was written with testing as an afterthought I've been giving it a bit more serious glance.
If you've never read this book I really
really recommend it. Don't let the title fool you, this book is jammed pack with tons of tips that are invaluable for writing greenfield code as well...if anything, the lessons you learn here will keep you out of trouble in the future. Also, regardless of what the Ph.D. sounding title and stagnant cover picture would lead you to believe, this is actually a very light hearted book that reads quickly with a nice little sense of humor. Feathers, in fact, has a very enjoyable writing style that may be quite underrepresented by the first impressions of his book.
Anyway, one such gem that has came out of this book is the
Null Object Pattern. Although I had never seen this before, this is a really slick little pattern....
Imagine that you're retrieving and displaying records from a database based upon a set of IDs. The records corresponding to your IDs may or may not exist in the database. In fact, it's so common that your record doesn't exist that your retrieval method doesn't even throw an exception...it simply returns null. This means that you have to check for null every time you do anything with that returned value. This leads to a lot of ugly code bloat.
Now, imagine that you could safely call methods on a null object without any risk of a
NullReferenceException or a nasty side effect. This is what the Null Object Pattern is all about.
Here is our business object class, the Account object...
/// <summary>
/// Represents a live account.
/// </summary>
public class Account
{
private readonly string _id;
/// <summary>
/// Initializes a new instance of the <see cref="Account"/> class with the given ID.
/// </summary>
/// <param name="id">The ID of the new account.</param>
public Account(string id)
{
_id = id;
}
/// <summary>
/// Gets the ID of the account.
/// </summary>
/// <value>The ID of the account.</value>
public string ID
{
get { return _id; }
}
/// <summary>
/// Processes this account fulfilling any unfilled orders.
/// </summary>
public virtual void Process()
{
ConnectToOrderService();
FulfillAllOrders();
}
}
And this is what our retrieval method looks like...
/// <summary>
/// Gets the account which corresponds to the given ID. If no account is found
/// <c>null</c> is returned.
/// </summary>
/// <param name="id">The ID of the desired account.</param>
/// <returns>The desired account or <c>null</c> if no matching account is found.</returns>
public Account GetAccount(string id)
{
return _accounts.Find(
delegate(Account account)
{
return (account.ID == id);
});
}
And our consumer method...
foreach (string id in ids)
{
Account account = service.GetAccount(id);
if (account != null)
{
account.Process();
}
}
Now, let's think about this. What if we had a new object that was still an Account object at its base, but was perfectly safe to call this empty methods on. Meet the Null Object...
/// <summary>
/// Represents a dummy account that can perform no action.
/// </summary>
public class NullAccount : Account
{
/// <summary>
/// Initializes a new instance of the <see cref="NullAccount"/> class.
/// </summary>
/// <param name="id">The ID of the new account.</param>
public NullAccount(string id) : base(id)
{
}
/// <summary>
/// Performs no action as this is a null account.
/// </summary>
public void Process()
{
// These accounts have nothing to process
}
}
Note that this is still, at its base, an Account object. Do you spot the key difference though? All of the business methods on this object are empty. This means, that we can safely call them without any risk of runtime errors or side effects. Now that we know this, lets take another look at our retrieval method...
/// <summary>
/// Gets the account which corresponds to the given ID. If no account is found
/// an instance of <see cref="NullAccount"/> is returned.
/// </summary>
/// <param name="id">The ID of the desired account.</param>
/// <returns>The desired account or an instance of <see cref="NullAccount"/> if no
/// matching account is found.</returns>
public Account GetAccount(string id)
{
Account theAccount = _accounts.Find(
delegate(Account account)
{
return (account.ID == id);
});
if (theAccount != null)
{
return theAccount;
}
else
{
return new NullAccount("");
}
}
And our consumer method...
foreach (string id in ids)
{
Account account = service.GetAccount(id);
account.Process();
}
All that had to change for our retriever was that we decided to return a NullAccount object when no matching account is found instead of returning null. And, this simple change, has completely alleviated our consumer's need to check for null each time before doing anything. This leads to much cleaner code on the side of our consumer.
Now, although this works great in the example above, as with any pattern there are obviously some situations where its usage is not appropriate. For example, if you were keeping track of the number of valid Accounts in your data store by ticking a counter variable, this would not be a wise choice. Also, any situation in which you're relying on exceptions to fail fast would be clouded by this as well.
As with any pattern, full understanding of it means knowing not only when to use it as well as when
not to use it.