A while back, I found myself plugged into working on a rather large .NET project. The project had been going on for a while, and I was helping out only in terms of implementation manpower; architectural decisions had long since been made. And, in fact, some of the architectural decisions had pre-dated the project altogether. There was a “framework” that these guys were using for the purpose of rapid implementations. The idea was that developers could sort of slap this framework into place and make a few tweaks to have new applications rather than having to take more time to write the code. This wasn’t especially effective, but that was the goal.
As I used (and kind of battled with) this framework, something bothered me about it that I couldn’t put my finger on at first. But as I worked with it, I started to figure it out. The framework wasn’t discoverable in the slightest and thus fought against its own goal of rapid ramp-up and development. I actually put a placeholder in my drafts folder all of those moons ago wherein I made notes about how to make a framework discoverable. I never wound up bothering to make suggestions for improvements to that framework since, luckily, it was the last time I had to use it. But I’m trying to reduce the number of unpublished drafts in my folder, so I figured I’d offer up the suggestions to the world at large so that maybe some of you reading can get some value out of them.
What Do You Mean, Discoverable?
In the last paragraph I used a word without defining it, so I’ll correct that here. To understand what I mean, ask yourself the question, “how do developers discover how to use code?” 10 years ago, the most common answer to that question would have been “read the documentation.” Ah, the bad old days. I remember doing some Linux kernel programming using a library called RTAI, and I had these binders full of documentation on the API. They described in detail what each of the hundreds of parameters for dozens of functions were and what they meant. You certainly couldn’t tell from the names of these things — they were abbreviated, cryptic, and unclear.
The API itself lent few obvious clues about how to use it, and documentation was necessary for understanding. This is an API that is not discoverable. Discoverability is the degree to which you can figure out how to use something simply by playing with and examining it. For instance, if some API had a method called “Connect_To_The_SQL_Server_Database_Using_The_Following_Connection_String(string connection_string)”, you’d be dealing with something highly discoverable; you know how to use this without doing anything but looking at the code.
Developers have a variety of strategies for discovering how to use code. Reading documentation is probably the most traditional and established method, and one with which most experienced developers are familiar. However, let’s be frank — it’s also kind of a bummer. When you’re coding, you just want to write code via experimentation, trial and error. Having to lug out a big binder and pore through it, parsing jargon-heavy text is definitely a fly in the ointment. And while I’ve seen a lot of people express the sentiment, “you have to fight through the burn or you’re not a real programmer,” the fact of the matter is that when something is drudgery, attention spans wander and productivity decreases. So while reading documentation is the most established way of discovering code, it’s also the least effective, typically.
By contrast, the most effective manner of code discovery is use. Developers start using an API in whatever way seems most intuitive to them, typically using tools like Intellisense or word completion. They have some class they’ve declared an instance of in their IDE and they type the variable name and then a period and then they see what the auto-completion tool gives them by way of options for methods. There are other ways for developers to discover APIs as well, such as reading the code when it’s available, pairing with the code’s author, discussing the code with others, etc. But by and large, discovery through experimentation and clarity of purpose is most effective.
So the important question for architects, API authors, and framework providers thus becomes “how do I make my stuff discoverable for the developers that will use it?” Here are some suggestions.
Favor composition over inheritance
Inheritance is one of the main facets of polymorphism and boy oh boy was it popular in the late 1990’s and early 2000’s, especially in Java-land. I recall working on and with inheritance hierarchies that were 10 deep before any useful functionality emerged. Inheritance was one of the best ways at the time to avoid code duplication, but we, as an industry, learned the ugly downside of this approach: confusing non-discoverability.
There is nothing quite like getting a bug report that your code isn’t working and then taking a look to see what went wrong. You look at the source control history and see no record of any change that would explain the bug. None of the classes that your class is using have changed. Is it a bug in the GUI technology or database driver or something? Nope. Turns out, someone checked in a change 3 levels above you in the inheritance hierarchy and suddenly your class behaves differently. /Sigh.
When you use composition (the practice where your class uses other classes or implements interfaces), this sort of mysterious behavior in your classes is eliminated. If your class’s behavior has changed and you haven’t changed it, you know that one of the things that you’re using must have been changed. Eliminating or minimizing inheritance gets rid of a source of confusing possible changes.
Favor few parameters over many
This is relatively straightforward, but avoid using a lot of parameters with your methods. The fewer the parameters, the fewer the amount of things that callers of your methods have to understand and remember. This is a “Golden Rule” kind of situation. When you find the method you want to use by name, but it takes 12 arguments, do you think to yourself, “perfect — just what I want!” or do you think to yourself, “ugh, why me?” Well, users of your API/framework are no different. Consolidate the parameters you require into objects if need be — this will still be easier to understand for clients of your work.
Be judicious with overload methods
Method overloads are a powerful tool, but use them sparingly. It’s nice to offer your clients options, but it’s easy to offer them too many options. Applying the “golden rule” concept from the previous section, imagine if your code autocomplete tool shows you the method that you want, but there are 20 different parameter lists. Ugh. Which one to use? This isn’t discoverable anymore because you’re going to start googling or reading documentation or something. If there’s only one option, you’re simply going to use it and move on.
Favor early binding over late binding
One of the nastiest facets of the framework that I mentioned in the introductory section of the post was rampant late binding. Late binding is a scheme where relationships are resolved at run time rather than compile time. A common example of late binding would be a scheme that uses reflection to resolve names of fields in code to strings and to sync them with values on a form. So, if you have a Customer object with a “Name” property, you use reflection to get the string “Name” and look for a matching string on the form to which to write that value. Contrast this with a scheme where you simply have a strongly typed form object and you assign a value to its “Name” property. In the former scheme, mis-typing the property name results in a confusing runtime exception whereas in the latter scenario, the code won’t compile.
Programmers understand non-compiling. It’s one of the first feedback schemes you learn with a new language and you’ll very quickly understand why the compiler complains at you. So when something doesn’t compile, it’s quick and easy to figure this out and fix it. But if something fails weirdly at runtime as with a late binding scheme, it becomes hard to figure out what’s wrong. And, more importantly, it’s very frustrating. Part of discoverability is that it’s easy to understand why the right things work and the wrong things don’t. Late binding makes this understanding far more difficult for people working with the code.
Fail as early as possible
Very closely related is the concept of failing fast and early. An important concept for programmers is a tight feedback loop — a minimum of time between when you take an action and see the result of that action. A framework that creates negligible lapses between action and reaction is a discoverable one because users can quickly understand what works and what doesn’t. If there’s a lag between action and reaction, things are harder to figure out. This is a big problem with late binding, but it would also apply to a framework that’s slow running or slow to build as well.
Make screwing up impossible
Perhaps most important to discoverability is to make screwing up as hard as possible. Have clients of your code “fall into the pit of success.” This means taking whatever steps are possible to disallow doing the wrong thing. If a class Car in your framework requires an Engine in order to work, force people using your framework to pass Engine to Car’s constructor so that if they don’t, the code won’t compile. If they pass null, throw an exception. Contrast this with a behavior where the code makes passing in an engine optional and simply limps along and maybe throws a null reference exception somewhere if it’s missing. The latter approach is confusing — you’re communicating through your API that the Engine isn’t needed, and yet you’re failing everywhere it’s not present. You’re creating a minefield for your users rather than a pit of success, and consequently making your stuff really non-discoverable.