Abstract:
How to design easy-to-use interfaces between modules of your program.

Created by Peter Kankowski
Last changed
Filed under Interfaces

Share on social sitesReddit Digg Delicious Buzz Facebook Twitter

Software interface design tips

This article summarizes tips and techniques for creating a good API or a software interface, i.e. the set of public classes and functions that connects the modules of your program. Win32 API, PHP standard library, and C run-time library will be used for examples, because they should be well known to most programmers, not because they are worse than other APIs (they are not).

High-level guidelines

Interface is created for the caller

When creating an interface, you should consider the typical tasks that the calling code will do, and provide useful primitives for these tasks. Often, programmers start from the other end: from a list of the things that their internal implementation can do. This approach leads to verbose code in the caller. Remember that the interface should be expressed in the caller's terms, not in implementation terms.

Compare CreateFile and fopen functions. CreateFile takes 7 arguments, most of which are rarely needed. (Which was the last time you used security attributes, a template file, or an attribute other than FILE_ATTRIBUTE_NORMAL?) It looks like the developer of CreateFile gathered all possible parameters you can have for opening files:

HANDLE hFile = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ, NULL,
    OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL);

The fopen function has just two arguments: a file name and a mode string. The interface is much easier to use than CreateFile:

FILE * f = fopen(filename, "rb");

An ideal interface would use flags instead of the cryptic mode string. Most frequently used flags would be "open for reading" and "open for writing". For the sharing mode and creation distribution, the function should provide reasonable defaults and allow the caller to override them by providing additional flags. The file should be created with default attributes, which can be changed later with a separate function (such as SetFileAttributes).

Keep balance between low-level and high-level interfaces

As you have seen from the previous example, low-level interfaces require writing a lot of code on the caller side. Too high-level interfaces are also bad: they are not flexible enough, so you cannot do some things with them. You need a compromise.

When working with files, a typical task is reading a small file into memory buffer for further parsing/decoding. Win32 API and C run-time library don't provide a function for this task, so you have to synthesize it from low-level primitives: open file, get file size, allocate memory, read file, and close file (or even more steps if you are using memory-mapped files). Python and PHP have a ready-to-use function, which is often helpful.

However, such function cannot fully replace the traditional fopen-fread-fclose interface, because it fails for large files that don't fit into memory. That's why Python and PHP also provide means for chunk-by-chunk reading. This is an example of the balance between high-level and low-level interfaces.

Download a C++ class for reading small files as a whole.

One function for one task, one class for one purpose

Don't try to do everything in one large function or one big class. One function should do one simple task; one class (or group of functions) should have one purpose. Create simple interfaces, which can be combined to solve complex tasks (similar to Unix command-line tools connected with pipes).

This principle is violated in the FormatMessage function, which does three distinct tasks:

  • formatting a message from string (similar to sprintf);
  • formatting a message from message-table resource;
  • obtaining an error message string for the system error code returned by GetLastError.

The buffer can be supplied by user or allocated by the function. The parameters of FormatMessage have different meaning and type depending on the task you want to do, so you have to cast the types and carefully read the manual. Separate functions for each task would be much easier to use.

Be consistent

Develop a convention for the order of arguments, function naming, and error handling, so that your interface could be used without looking into the manual. Some examples of inconsistent interfaces:

  • CreateFile and FindFirstFile functions return INVALID_HANDLE_VALUE in case of failure, while CreateFileMapping returns NULL. All these functions use the same data type (HANDLE), so the error value should be the same.
  • FILE pointer is the first argument of fscanf and the last argument of fread and fgets. It should always be the first one.
  • PHP is notorious for inconsistent function naming and parameter order. There are functions with and without underscore after prefix (str_replace and str_getcsv versus strpos, strlen, and strtr). A regular expression is sometimes the first argument (preg_match) and sometimes the last one (preg_replace); the source string is the first argument of strtr and the last one of str_replace.

In contrast to PHP, string and memory functions in C run-time library (strchr, memcpy, memset, etc.) have a standardized, easy-to-remember order of arguments:

  • an optional output buffer;
  • the input buffer;
  • parameters (such as the character to look for);
  • the input buffer length for mem- functions (see memcpy, memchr, and memcmp).

Keep it simple

Keep your interface simple and terse. If you don't have to save state between calls, use a set of functions instead of a class. The usual mistake is thoughtless usage of OOP, patterns, and other fashionable techniques.

Consider file context menu (appears when you right-click a file in Windows Explorer). Applications typically need:

  • to add an item to the menu (depending on extensions of the selected files);
  • to display the menu for a given file.

Both tasks require writing a tremendous amount of complicated COM code. Raymond Chen published an 11-part series about displaying the context menu. The interface is flexible (for example, you can add custom commands), but too complex. Many developers will miss the subtleties and write a buggy code.

A better solution would be implementing a single function for displaying the menu for the given file name (with callbacks or other means to add custom commands to the menu):

BOOL DisplayFileContentMenu(
    HWND hWndParent,                 // handle of parent window
    LPCSTR lpszFileName,             // file name
    UINT nCustomCommands,            // number of custom commands
    LPCUSTOMCOMMAND lpCustomCommands // array of custom commands
);

Low-level techniques

Use objects to save state

Purely functional interfaces are often impractical, so the program state have to be saved between function calls, usually by storing it in an object. In C language, which does not support OOP, you can use structures for this purpose.

Imagine you are writing a function plotting program: the user enters something like sin(x) or x ^ 2 - x * 5 + 3, and the programs draws a graph for it. The optimal solution here is an expression_evaluator that compiles the function code once, then runs it many times with different values of the argument (x). You need two functions ("compile" and "evaluate") with some storage for the compiled code. A class is usually preferable to storing the program state in global variables (what if you will need to draw several graphs simultaneously?)

class MathFunction {
public:
    MathFunction(const char * formula);
      // Compiles the formula into internal representation,
      // throws an exception on syntax error.

    double EvaluateAtPoint(double x);
      // Evaluates the function at the specified point.

    ~MathFunction();
      // Frees the memory for compiled formula.
};

Replace Boolean arguments with flags

As Raymond Chen notes, using flags or enumerations often improves readability in comparison to Boolean arguments.

For example:

Font* f = new Font("Arial", false, true, false);

// The constructor is defined as:
Font::Font(const char * typeface, bool is_bold, bool is_italic, bool is_underlined) { ... }

should be replaced with

Font* f = new Font("Arial", Font::ITALIC | Font::UNDERLINE);

class Font {
    public:
        Font(const char * typeface, int flags);
        static const int
          BOLD      = 0x01,
          ITALIC    = 0x02,
          UNDERLINE = 0x04,
          STRIKEOUT = 0x08,
          SMALLCAPS = 0x10;
};

In the second interface, new flags may be added without altering the calling code.

Avoid long positional argument lists

Long argument lists are difficult to read and maintain, especially if most arguments have the same type:

HFONT font = CreateFont(-20, 0, 0, 0, FW_NORMAL, TRUE, FALSE, FALSE,
  DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
  DEFAULT_PITCH | FF_ROMAN, "Arial");

A typical error is an accidental shift by one or otherwise wrong order of arguments. In C/C++, you can improve the code by storing parameters in a structure or a class:

LOGFONT lf = {0};
lf.lfHeight = -20;
lf.lfWeight = FW_NORMAL;
lf.lfItalic = TRUE;
lf.lfCharSet = DEFAULT_CHARSET;
lf.lfPitchAndFamily = DEFAULT_PITCH | FF_ROMAN;
strcpy(lf.lfFaceName, "Arial");
HFONT font = CreateFontIndirect(&lf);

The default value for most parameters is zero, so you can use = {0} initializer or memset function, then set the parameters that you need.

Python and VB offer another solution: keyword arguments. It's possible to implement them in C++, though this idiom is not widely used.

Select a way to return multiple values

Python, Perl, and PHP can return several values from the function call:

year, month, day = time.localtime()[:3]

In C/C++, you have to return the values in structure or by using pointers/references:

// Structure
SYSTEMTIME time;
GetLocalTime(&time);
printf("%02d-%02d-%02d", time.wYear, time.wMonth, time.wDay);

// References (C++)
bool my_get_date(OUT int& year, OUT int& month, OUT int& day);

int year, month, day;
my_get_date(OUT year, OUT month, OUT day);

When using references, it's easy to confuse input and output parameters. You should use the OUT macro (from Windows header files) to make it clear that the parameter is returned from function:

// Worse:
my_get_date(year, month, day); //  <--  no visible
my_set_date(year, month, day); //  <--  difference

// Better:
my_get_date(OUT year, OUT month, OUT day);
my_set_date(year, month, day);

In C#, the out keyword is mandatory in such cases.

You should also maintain const correctness in your interface if you use a statically-typed language such as C, C++, or C#.

Develop a consistent naming convention

  • If your programming language supports namespaces, create a namespace for your classes and functions. If not, consider using a prefix for names (such as wp_ in Wordpress functions).
  • Use clear and precise names for functions (methods, classes). The function name should include a concrete verb (create, destroy, draw, print, insert, remove, enumerate, get, set, etc.) or a name of the thing that is returned or displayed by the function (MessageBox, Polygon, CharNext, MAX). Another common pattern is symmetric conversion functions (ScreenToClient - ClientToScreen, CharToOem - OemToChar).
  • Systems Hungarian notation for arguments is often redundant, but Apps Hungarian may be useful.

Choose between raising exceptions and returning an error code

The problem with exceptions is the need to analyze all called functions to understand where they can fail. Without this knowledge, it's impossible to free the resources correctly. Writing a non-trivial code in exception model is often harder than in error-code model.

OTOH, exceptions allow you to return an error from constructor easily.

Error codes lead to a verbose program, because the caller has to check each function for failure explicitly.

If your function returns a value, you have to return the error code via pointer/reference, as described above:

enum EXPR_EVAL_ERR {
    EEE_NO_ERROR = 0,
    EEE_PARENTHESIS = 1,
    EEE_WRONG_CHAR = 2,
    EEE_DIVIDE_BY_ZERO = 3,
    EEE_UNDEF_VAR = 4,
    EEE_TOO_MUCH_VARS = 5
};
double EvaluateExpression(const char * expression, OUT EXPR_EVAL_ERR& error_code);

EXPR_EVAL_ERR error_code;
double res = EvaluateExpression("2 + 2 * 2", OUT error_code);
if(error_code != EEE_NO_ERROR) {
    // Handle the error
}

Summary

  • Start from a list of the things that your caller needs to do. These will be your methods/functions.
  • Add only those arguments that are useful for the caller.
  • Make your interface high-level enough to shorten the calling code, but low-level enough to solve all necessary tasks.
  • Don't mix different things in one function or one class.
  • Be consistent in function naming and order of arguments.
  • Use objects for saving the state of program between calls.
  • Implement yes/no parameters with flags.
  • Find a replacement for long parameter lists (use keyword arguments or a structure).
  • If your language does not allow returning multiple values, put them in structure or use pointers/references.
  • Choose between exception model and error-code model for handling errors.

Exercises

  • What are the problems of Win32 memory-mapped files interface? See CreateFileMapping and MapViewOfFile functions.
  • How would you return several values from function in your favorite language?
  • Design an interface for advanced expression_evaluator in C or C++. The caller should be able to add custom constants and functions that are implemented in C/C++ and can be used in the expressions. For example, the caller could define the PI constant to be equal to 3.14 and the abs function for calculating the absolute value. The functions can have any number of arguments, which are always double. (Don't implement the evaluator; just describe a possible interface.)

Further reading

Books
Web pages
Peter Kankowski
Peter Kankowski

About the author

Peter is the developer of Aba Search and Replace, a tool for replacing text in multiple files. He likes to program in C with a bit of C++, also in x86 assembly language, Python, and PHP.

Created by Peter Kankowski
Last changed

9 comments

ace,

For me it's misleading to have "an expression evaluator that compiles the function code once, then runs it many times with different values" named "Expression." I'd call it Evaluator or ExpressionEvaluator.

An ideal interface would use flags instead of the cryptic mode string

I actually like the concept, and I think it's too infrequently used, that is, I often see too much code too tightly dependent. For example, for modules that are supposed to work even with existence of different versions of client code and the module at the same time, e.g. when you have to maintain DLL's which are changed independently from the programs which use it, there are advantages at having a string as something that carries all the options. Higher chance to implement good decoupling, so different versions can exist without the need to recompile the clients and you can still introduce of new options and still keep the binary compatibility.

Additionally, I really like the principle used by CreateFontIndirect the best: as soon as you have more than 2 or three parameters to the function, introducing the structure to be passed there pays off very fast. There's high chance that that one function will not be the only one that need the access to the parameters. With the structure, you just pass one pointer around. Without, you spend unnecessary lines of code and cycles copying things from the place where they are not needed to the places where they are even less needed.

CreateFontIndirect is a good example to point to another common error: "if you hold a hammer, everything looks like a nail." People learn "classes" and then think that structures are something not worthy of C++. Of course, language lawyers never preach about the structures, it's not new enough and modern enough. In fact, structures are most of the times easier to handle.

When we're by LOGFONT: Instead of

LOGFONT lf;
memset(&lf, 0, sizeof(lf));

I use just

LOGFONT lf = {0};

when the structure is on the stack.

Using structures gives you both "explicitly named" and "implicit" passing, and even "filling all that are left with 0" in implicit case. E.g.

LOGBRUSH b = { BS_DIBPATTERN, DIB_RGB_COLORS };

and

LOGBRUSH b;
b.lbStyle = BS_DIBPATTERN;
b.lbColor = DIB_RGB_COLORS;
b.lbHatch = 0;

mean the same. And both are valid C and C++ code since long ago.

Moreover, the latest C standard also has "designated initializers" like:

div_t answer = { .quot = 2, .rem = -1 };

Almost unsurprisingly for the current state of affairs, C++ still doesn't have designated initializers.

Peter Kankowski,

Thank you for your comments.

I'd call it Evaluator or ExpressionEvaluator.

In this case, the method call will sound like "Evaluator.Evaluate", which is a tautology. The Expression class can do other things with a parsed expression (e.g., syntax highlighting), not only evaluating.

there are advantages at having a string as something that carries all the options

Do you mean keeping in a string something more complex than yes/no flags? If you need only yes/no flags, they can be added easily in new versions of DLL (Microsoft often adds more flags to Win32 API functions in new versions of Windows). A string is useful if you want to create a little language (such as Media Control Interface) as an alternative to passing a lot of parameters.

Thanks for the examples with LOGFONT, LOGBRUSH, and div_t. I forgot about

LOGFONT lf = {0};

(The article is changed to use it now.)

Why don't they add "designated initializers" to C++?

ace,
In this case, the method call will sound like "Evaluator.Evaluate", which is a tautology.

For now I'd have:

Evaluator.RecalculateForX( 33 );

"Expression" is anything. "Evaluate" can also mean anything.

The Expression class can do other things with a parsed expression (e.g., syntax highlighting), not only evaluating.

Once it has more functions then it can be that it's better described by what's inside than what it does. In that case I'd probably go with "ParsedExpression" since it immediately gives the information about the contents (you can conclude that there are some structures which have to be regenerated when the expression changes, and the expression is stored as some appropriate internal representation). I can imagine that in very small programs any name can fit, I admit I'm certainly biased as I daily work with millions of lines of code. My favourite example of a wrong name: "Data" – to the guy who writes some small piece of code, the data he handles there is all the data in the world :) In my (biased) world Expression is not much more descriptive.

The article is changed

Still:

so you can call memset to initialize the structure
Why don't they add "designated initializers" to C++?

In shortest, C++ grew "unbalanced" and it seems that C++ standard community is often "too much in the clouds" and bureaucratic at the same time. The probably feel too superior and also with "not enough time" to care about C. C guys probably see C++ as "too inflated" and they are quite right. Stroustrup actually recognized that it's not a good idea (I remember he wrote an article about that, I can't find it now) to have C and C++ out of sync, but that didn't change the results.

a little language (such as Media Control Interface) as an alternative to passing a lot of parameters.

Good example: If we look at Media Control Interface, we can see that there is really small set of commands and the number of interface functions to cover them all would still be small. Number of commands is around 10 – one int for them. Parameters are different but it would be easy to have one more int for options. So you could easily have

mciDo( int command, int flags, … )

and then enums for commands and for flags. But still passing only string as an interface gives even more decoupling and more freedom to modify it during the life of the program.

Most of the people don't understand what interface really is, where it should actually be and how desirable decoupling is. I had so many examples when programmers want all the classes they wrote to be an interface! I think C++ and OOP lawyers made disservice to good programming by teaching people to just always think about the classes. C++ classes are for most of uses too small an entity to represent an interface. But people overwrite every internal class they make treating it as it would be an interface (getters and setters for every member when it's so wrong!), and on another side are not able to even imagine what a real interface to a module or their component should be.

If you need only yes/no flags, they can be added easily in new versions of DLL

Imagine you had 20 flags and you need to add 13 more. Now you have to change the function – one 32-bit int is not enough anymore. Passing string you can keep the function definition unchanged much, much longer.

Regarding fopen, my guess is that the designer didn't really expect too many flags, but he wanted to be able to pass the flags for the file open directly from the command line. Like fopen( argv[1], argv[2] )

ace,

When we're by fopens and opens with strings, I think some open variants in Perl are really both concise, obvious and convenient, like:

"filename" opens to read ">filename" opens to write "acommand|" opens output of the executed command as file "|command" starts the command and passes what's written to pseudofile to it.

And all they are actually directly taken from Unix command line. Some are confusing: what does "+<" and what does "+>"? And fopen – "r+", "a+", "w+"?

Most of the times not compressing too much is much better. So if I'd make fopen now I'd prefer "r" for read, "rc" for read and create if doesn't exist (it would fail but the flags would at least be explicit!), "w" for write, "wc" write and create if it doesn't exist, "rw" for read write, "rwc" for read, write and create if it doesn't exist, "arw" for append and read and write, "a" for append, "ac" for append or create if it doesn't exist etc, "ar" for append and read etc.

Peter Kankowski,
In my (biased) world Expression is not much more descriptive.

Thanks, I agree that MathExpression or ParsedExpression is better for large projects. What do you think about MathFunction? The example in article is about a function with one argument ("sin(x)"), not about an arbitrary expression ("2 + 2").

So you could easily have mciDo( int command, int flags, … )

MCI actually has this kind of interface (mciSendCommand) in addition to the string interface.

I'd prefer fopen with integer flags:

FileOpen("file.txt", OPN_WRITE | OPN_CREATE_IF_NOT_EXIST);

Even though it takes more time to type than "wc", it's easier to read and shorter in the compiled code.

Thank you for the suggestions and comments.

ace,
I'd prefer fopen with integer flags:
FileOpen("file.txt", OPN_WRITE | OPN_CREATE_IF_NOT_EXIST);

See unix man page for open system call. You have flags which are to be ORed there. But see also how many flags are defined. The library fopen was intentionally made to have much less flags than that, and even to be used as one call instead of the combinations of the open and creat system calls. But see also that the "modern" Linux man page lists two functions with the same name but different parameters.

      int open(const char *pathname, int flags);
      int open(const char *pathname, int flags, mode_t mode);

It seems "int is not enough" happened there. It can also be that it can properly work for ints which are only two bytes. Now imagine that fopen is not something fixed through the decades, but something you made and where you must expose some new flags. fopen signature doesn't have to change, and that is also not dependent from the questions "what can fit in int, is int maybe 2 bytes" etc.

Peter Kankowski,

Thanks for the example. I understand your point now: the string interface is more extendible without the need to add new arguments.

ace,
the string interface is more extendible without the need to add new arguments.

There's also something often not recognized: the string interface is actually from the point of the view of the interface user's interaction with the compiler not inferior to the one with flags combined with or.

It's true that with string interface you can write some letters that are not valid, and that some combinations of valid letters will not behave as expected. Language lawyers would say "compiler should check more." Still compiler can't tell you "you made an or combination of the flags which won't work." You still have to actually perform a run-time test, the same you have to do with string. The compiler can't even tell you "you used wrong flag definitions for the parameters" just like you can write wrong letters. So your program will not be magically "less error prone" when you use flags and ors!

The only thing you get on the "safeness" side is that when you as human read the code and when you see OPN_WRITE it can mean you more than when you read "w". Good programming is targeted to humans, not computers. Computers would be happy enough with "8B85C9FEFF…" Note that in some explicit cases even humans can find a single letter easier to grasp.


When we're at "good written for humans": as an example of extremely clean implementation of library fopen the sources of Plan9 are especially educative: fopen.c and freopen.c. See how fopen must map to more system calls and flags. I also like the balanced minimalism in the code which (at least for me) makes the code more and not less readable.

To contrast that you can try to follow an implementation of fopen used in very widely used GNU glibc library iofopen.c.


(BTW The elegance of Plan 9 code is not an accident. Ken Thompson, the creator of Unix, made the compiler for Plan 9 with the following idea: "This compiler does not support #if, though it does handle #ifdef. In practice, #if is almost always followed by a variable like ‘‘pdp11.’’ What it means is that the programmer has buried some old code that will no longer compile. Another common usage is to write ‘‘portable’’ code by expanding all possibilities in a jumble of left-justified chicken scratches. As an alternate, the compiler will compile very efficient normal if statements with constant expressions. This is usually enough to rewrite old #if-laden code." (see A New C Compiler, by Ken Thompson) There's one more quote there I really like: "The keywords register, volatile, and const, are recognised syntactically but are semantically ignored. (…) Const only confuses library interfaces with the hope of catching some rare errors.")

Peter Kankowski,

Thank you very much for the links!

Your name:


Comment: