Sunday, January 12, 2014

How to design interfaces

My definition of the word interface is point of contact for a piece of functionality. Designing good interfaces to any piece of functionality is hard -- that's at least my experience. I've designed several interfaces, but very few of those make me particularly proud of my programming skills. The problems I frequently see are:
  • too specific (hard-wired to a particular client),
  • too generic (hard to use for simple problems),
  • not extensible (adding functionality requires changing the interface), and
  • simply to darn hard to understand.
I've taken a few month off from hobby hacking, which has let my mind wondering in various direction without being held up by any particular project that I happen to work with. One theme that keeps popping up in my readings and thinkings is modeling. That is, given a solution to a problem (e.g, a python function someone just wrote), how should I think about it such that it makes sense to me (and others) -- in other words, how do I model the solution. Let's take an example to make this a bit clearer.

Consider a piece of code that parses a file where each line holds either a number or a string of text. How should the interface to this code look? Some alternatives are:
  • bool str_to_int_or_str(const char* content, int* int_value, const char** text);
  • OneOf<const char*, int> str_to_int_or_str(const char* content);
  • void read_line(const char* content, void (int_func*)(int), void (txt_func*)(const char*));
  • void parse(const char* content, Visitor* visitor);
  • template <class C, class V> parse(C content, V visitor);
I won't go into details, but it's safe to say that each of these alternatives have its good and its bad aspects. The question here is though, which alternative models the solution most accurately. I would personally go for the second to last or last alternative, as parsers and visitors is familiar to most people (by which I mean programmers). That is, I model the solution/code as if it's a parser -- not like some conversion function (str_to_int_or_str) or a line-based reader (read_line).

With small pieces of code like the example above, it might not be clear why thinking about modeling is important. But consider designing an API for a large and complicated library. What are the concepts that the client need to understand? What are the actions it can perform? What side-effects are expected and when should they be visible?

At work I'm developing an application that just-in-time compiles a proprietary language into native x64/x86 code. We've had some bugs related to when variables are updated. This probably sounds like trivial things to get right, but when you get deep down in the details these things easily slip through because your brain are occupied thinking about other aspects of the problem. These bugs could have been avoided if there was a layer that took care of these details. That is, a layer that models these inherent properties of the language and provides an interface that helps the developers to think in terms of the model.

This brings me to the other aspect of modeling -- as tool for communication.

Given any piece of code -- two programmers will think about it slightly different. Furthermore, the larger the code, the more likely that two programmers will understand it more different. In other words, it will be harder for them to understand each other when talking about the code. However, if the code is partitioned into smaller parts and each part models (not only implements) some logical sub functionality, it will be easier to understand it.

Note that the important word in the above paragraph is not implements, it's models. The implementation of an interface is irrelevant for understanding what a client does if the interface models the solution properly.

Think about opening and reading files. In most languages it's as straight-forward to implement it as it is to say "think about opening and reading files". The interfaces models the way we think about files. Great! That means we don't need to understand what open does in order to understand what the code using open does.

This means that one easy way to get an interface to be easy to use is design it by mimicking something other people already understand (e.g., a parser, a visitor, a file). This is the alternative I've taken the last few times I've designed an interface -- trying to find a suitable analogy and use that as model.

No comments: