A friend/colleague of mine, George, made an interesting assertion - that he prefers passing implementations, not representations. As we talked and unpacked what he meant by this, I discovered a deceptively simple idea that can make our code significantly more flexible and adaptable.

We can look at the distinction by comparing two examples from the .NET Framework.

When you want to compare two strings, you can (and should!) be explicit about what kind of string comparison you want. In our example here, we’re checking to see whether the current object has a specified name, and we don’t care about letter case.

public bool HasName(string name)
{
    return string.Equals(Name, name, StringComparison.OrdinalIgnoreCase);
}

Here we’ve passed the enumeration value StringComparison.OrdinalIgnoreCase to represent the kind of comparison we want - internally, the .Equals() method makes a decision based on that value and delivers the comparison we want.

Now consider what happens when we define a dictionary to map from names to people and we don’t care about letter case:

var map = new Dictionary<string, Person>(StringComparer.OrdinalIgnoreCase);

Carefully note that this time we’re using a StringComparer, not a StringComparison. We’re not passing an enumeration value; instead, we’re passing an actual implementation that will do the appropriate comparison for us.

What’s the difference?

With String.Equals(), we can only pass one of the predefined values that have already been made available:

public enum StringComparison
{
    CurrentCulture = 0,
    CurrentCultureIgnoreCase = 1,
    InvarientCulture = 2,
    InvarientCultureIgnoreCase = 3,
    Ordinal = 4,
    OrdinalIgnoreCase = 5
}

Contrast that with the Dictionary constructor where we could pass any implementation at all:

public Dictionary(IEqualityComparer<TKey> comparer)

The class StringComparer provides easy access to a number of standard implementations that we can easily reuse:

public abstract class StringComparer : IEqualityComparer<string>
{
    public static StringComparer InvariantCulture { get; }
    public static StringComparer InvariantCultureIgnoreCase { get; }
    public static StringComparer CurrentCulture { get; }
    public static StringComparer CurrentCultureIgnoreCase { get; }
    public static StringComparer Ordinal { get; }
    public static StringComparer OrdinalIgnoreCase { get; }
}

The key point to note here is that we’re not constrained to only use the choices that are pre-baked.

If we wanted our map to ignore whitespace and punctuation, we could create our own comparison class implementing IEqualityComparer<string> and use that instead.

At the edges of our systems, where we interface with other components or with users, we need to use representations because they serialize to names and other forms that cross those boundaries easily.

But within our systems, passing around the desired implementation instead is a very easy way to make our systems more flexible and more adaptable.

As long as we take the time to ensure that easy things are still easy to achieve - as the .NET Framework does by including the StringComparer class - we can do this with very low cost.

Comments

blog comments powered by Disqus