Nerdy tidbits from my life as a software engineer

Monday, March 10, 2008

Fun With Extension Methods

I have come to believe that extension methods can add a lot of simplicity and elegance to your source code in ways that go far beyond LINQ, so I thought I would share some of the ideas that I have added into my content editor project. I should say that while I personally see these additions as ways to simplify source code, I can see how abuse of extension methods could make programs considerably more difficult to comprehend. The real danger here is the illusion that you are extending base classes by adding new functionality to existing classes, when in fact you are simply converting one way of calling a function into another. For instance, take the following extension method:

/// <summary>
/// This class holds extension methods that are used throughout the application.
/// </summary>
public static class ExtensionMethods
{
    /// <summary>
    /// Shows a message box that displays an error.
    /// </summary>
    /// <param name="e"></param>
    /// <param name="message"></param>
    public static void DisplayError(this Exception e, string message)
    {
        StringBuilder errorBuilder = new StringBuilder();
        errorBuilder.Append(message);
        errorBuilder.Append(Environment.NewLine);
        
        errorBuilder.Append("Error Type: ");
        errorBuilder.Append(e.GetType().Name);
        errorBuilder.Append(Environment.NewLine);

        errorBuilder.Append("Error Message: ");
        errorBuilder.Append(e.Message);
        errorBuilder.Append(Environment.NewLine);

        MessageBox.Show(Application.Current.MainWindow,
            errorBuilder.ToString(),
            "Error",
            MessageBoxButton.OK,
            MessageBoxImage.Error);

    }

}

All that I've done is define a method that takes an exception and a string as input. This method could easily be called as follows:

...
catch(Exception e)
{
    ExtensionMethods.DisplayError(e, "Some exception occurred");
}
...

But the extension method allows us to simplify this call into the following:

...
catch(Exception e)
{
    e.DisplayError("Some exception occurred");
}
...

So, what is the benefit of making DisplayError an extension method? I would say that there are actually a number of them. The first is that the code looks more natural. Why wouldn't an Exception object have a method called DisplayError? It seems to make sense in my mind. In my experience, the way that people get around this is to extend Exception and add their own custom exceptions that have methods called DisplayError. This is obviously one way to extend the functionality across an object hierarchy, but there is one glaring problem with this approach: it does not allow pre-existing Exception objects from having the same functionality! I could have an exception called MikesException, for instance, which has a method called DisplayError - but this will only work if I catch a MikesException. What would I do if I caught a normal exception?

...
catch(MikesException e)
{
    e.DisplayError("Some exception");
}
catch(Exception e)
{
    // ...not sure what to do
}
...

By having an extension method on the base Exception class, we can essentially extend all exceptions without needing to recompile mscorlib.dll, or define our own Exception hierarchy that lets us add this sort of functionality.
But there are other benefits to using extension methods for these sorts of scenarios. Take for instance, the ubiquitous message dialog which you end up using over and over throughout an application:

...
MessageBoxResult result = MessageBox.Show(this,   
    "Some Message", 
    "Some Title",
    MessageBoxButton.YesNo); 

if(result == MessageBoxResult.Yes)
{
...
}
...

This code will naturally repeat itself throughout any normal GUI, which can create a headache for maintenance and look and feel management. What if you wanted to change all question dialogs to use a custom object instead of the default MessageBox? Or what if you wanted to make sure that they all used the same MessageBoxIcon? In the normal scenario you would have to hunt down every instance of MessageBox.Show in your application and change them so that they did whatever you wanted them to do. Obviously, the preferably solution is to add a utility method somewhere in your application that abstracted this logic and put it all in one place:

/// <summary>
/// Displays a dialog with a yes / no option and returns true if the user
/// answers yes to the question.
/// </summary>
/// <param name="question">The string of the question to ask.</param>
/// <param name="title">The title of the question.</param>
/// <returns>True if the user answers yes; otherwise, false.</returns>
public static bool DisplayQuestionDialog(string question, string title)
{
    MessageBoxResult result = MessageBox.Show(Application.Current.MainWindow,
        question,
        title,
        MessageBoxButton.YesNo,
        MessageBoxImage.Question);

    return (result == MessageBoxResult.Yes);

}

This is definitely a step up from our previous implementation. Now we can throw up a dialog using something like this:

if(Utilities.DisplayQuestionDialog("Some Message", "Some Title")
{
... // Do something
}

All is well and good with our new question-box API, until somebody forgets (or simply doesn't know) that all message box dialogs need to go through the Utilities class. That's when you get some nasty inconsistency bug somewhere in your application where, for whatever reason, all dialogs look and behave one way except for one, random rogue dialog that looks different. These sorts of policies are difficult to enforce, of course, but extension methods take one difficulty out of the way: they eliminate the need for people to know where to look to find the method they need to call.

public static class ExtensionMethods
{
   ...
    public static bool DisplayQuestionDialog(this string question, string title)
    {
        MessageBoxResult result = MessageBox.Show(Application.Current.MainWindow,
            question,
            title,
            MessageBoxButton.YesNo,
            MessageBoxImage.Question);

        return (result == MessageBoxResult.Yes);

    }
    ...
}

... 

public class Client 
{
    public void Caller()
    {
        if("Some Message".DisplayQuestionDialog("Some Title"))
        {
            // Do something
        }
    }
}

Aside from looking more natural, the preceding code has another benefit: it hides the location and the source of DisplayQuestionDialog. Users no longer need to know that this method resides in such and such assembly in such and such a class. DisplayQuestionDialog simply appears to be a method of the String class. Of course there is one big pitfall that we haven't talked about. In cases where multiple assemblies define the same extension method, the compiler needs to resolve which one to call. I can see this being both a blessing and a curse. What if you want all calls to DisplayQuestionDialog in one namespace to go to one place and all calls to go somewhere else in another namespace? This can be accomplished by defining a new extension method that is local to that namespace and assembly - a solution that sounds grand, until you realize that you can no longer reliably determine which implementation of DisplayQuestionDialog gets called just by looking at the source code. Imagine if you end up having 30 implementations of DisplayQuestionDialog in 10 different assemblies or namespaces? What a mess that would be! I suppose as with all new technologies, it's effectiveness depends on its implementation. Any new idea can be abused, and if the examples used above are abused too much, I can certainly see how code can become horribly confusing. But I do think that the benefits of such techniques are enormous, provided they are used properly - particular when you start thinking about debug logging and tracing (how many places in your code do you see the same Utilities.LogMessage(.....) call? Wouldn't it be nicer to have some extension methods such as the ones outlined above?).

You can see some more examples of extension methods that I use in some of my programs here.