Here’s a quick poll: How many of you have figured out by now that 16,777,217 = 2^24+1, and that the "24" refers to the 3 RGB bytes in RGBA32?
An alternate title I considered for this post was "NoColor is a color", which would have been an inside joke for people who go to science fiction conventions. Considering the audience, I decided to stick with the computer science reference :)
So, what am I talking about? The fact that, for 24-bit RGB colors, there are actually 2^24+1 possible states, since the absence of color is itself a state. This in turn causes problems because it’s very difficult to jam 2^24+1 states into 24 bits. The more subtle problem here is that people often aren’t thinking about the extra state when they write their code, which can lead to "Happy Path" algorithms. So the question is, "How do we provide an interface to customers that manages the NoColor issue in the best way possible".
Let me give a concrete example. In our CGM product, we provide the ability to attach "properties" (such as color) to geometric objects – basically we provide SetColor and GetColor methods. If no color is present, then the object takes on the color of its parent object (or the default color if it is a root object). The problem we had to deal with was how GetColor should indicate the absence of color. More precisely, what should the signature of GetRed be, and what should it do when color is absent?
First, the evil answer: int GetRed() returns a “magic (invalid) value” such as -1 when no color is set:
The reason this is evil is that the server (the Color1 class) is intentionally returning an invalid result, and relying on the person writing the client code to remember he needs to check for a special state, then go down a different code branch in that case. I remember reading somewhere (but can’t find the reference) the "Tiger Trap" principle of interface design: that using the interface correctly should be like a tiger trap – consumers of the interface naturally fall into it. With this interface choice, the tiger trap is in the direction of using the interface incorrectly. An insidious variation on this design which might even be worse is to return a valid value for red in the NoColor case. The reason this might be worse is that an invalid value can be detected by a contract check (in DisplayRedValue() in the example). A valid (but arbitrary) value is undetectable, and can lead to weird results that are difficult to track down.
Next, the answer from my last post: create a Nullable<T> template class and have the GetColor query on an object return a Nullable<Color>:
There are several nice things about this approach:
- The color class is now responsible for only 2^24 states – the NoColor state is managed by the Nullable class.
- The Nullable class is designed for exactly this situation, so the approach is intuitive.
- The tiger trap is now in the correct direction –the compiler won’t let the customer call GetRed() on a Nullable<Color>.
Although the Nullable approach is appropriate in certain circumstances, it still has a drawback: the client still needs to write an if statement when actually getting the RGB values. This problem is solved by the last approach: pass the RGB values by reference, and don’t change them if the color is unset:
The nice thing about this interface is that it caters to the client’s workflow. Basically, the client calculates the RGB values that will be used if the color is unset (typically by looking at parent objects and/or the default color). The client then calls GetRGB – if the color is unset, then the previously calculated colors are left alone, otherwise they’re overridden. This is the interface that we chose to use for color (and other, similar) properties in our CGM product.
We have found this signature idiom to be really useful in a lot of places. For example, it’s a natural fit for a chain-of-responsibility pattern:
(Yes, I know I could reduce this to a single line, but I’m conservative about these things J ) I’m sure many of you are already using this one in your code, but for those of you that aren’t, I hope you find it useful as well.
One last thing: if anyone out there knows the correct tiger-trap reference could you please post it in a reply? I’d really like to track it down for proper attribution.