API design principles in cpgf library

From the first day when I designing the API in cpgf library, I had some design principles in mind and tried to stick with them. In this blog I will show the principles and give some examples.

Please note, none of any principles are found or invented by me. I was inspired by the great book “API Design for C++” and the blog Designing Qt-Style C++ APIs

Design principles in cpgf

Minimal

A class should have smallest number of public functions. A public function is added to a class when and only when,

  • The function can't be done without accessing non-public members,
  • Or, there is big performance penalty if the function is composed with other public functions

Complete

A minimal API is not complete if the user needs awkward extra steps to use the API. If a function can be done with several existing functions, and the function is frequently used, we should add a utility function to make the API complete. This kind of utility function is not only a shortcut, but also an encapsulation at the function level to avoid misuse or duplicated code.

Self explained

Good code should be self explained and self documented, there is no exception. I don't mean we don't need documentation. We need documentation. But if the user needs to look up the documentation to see what a function does, I would prefer giving the function a better name, better parameters name, rather than document it.

Consistent

This is one of the most important principle in my opinion, and the book “API Design for C++” gives some very good inconsistent examples in C API.

Easy to use and difficult to misuse

An API should be easy to use. Indeed all above principles are about easy to use. Also, a good API should be difficult to misuse. A function may be misused if it returns more than one result via parameters, or mixed const and non-const reference, etc.

Examples in cpgf

All interfaces returned by a function needs to be released

This example is about consistence.

Lots of functions return an interface, such as IMetaClass. It's required releaseReference is called on the interface to avoid memory leak. Sometimes this may be cumbersome because we can't call the functions in a chain, such as, getInterfaceA().doSomething(); We must store the return value of getInterfaceA to a local variable and release it after doSomething(). However, since all functions accept the same rule, we don't need to reference to any document to see when we should and when we should not release the interface.

When I was working on Python script binding, I was scared with the concept of “steal” and “borrowed” reference count in Python C API and I had to always check the document to see which return value I should release the reference count. That's not happening in cpgf.

Changed meta utility API functions from template to non-template

This example is about minimal and complete API, and self explained code.

There are some utility functions to ease the use of interface APIs. For example,

// Utility API
GMetaType metaGetItemType(IMetaItem * item);
 
// Implementation
GMetaType metaGetItemType(IMetaItem * item)
{
    GMetaType type;
 
    item->getItemType(&type.refData());
    metaCheckError(item);
 
    return type;
}

If we don't add this utility function, we have to repeat the implementation code everywhere. We add this utility function to make the API complete.

However, the original function was a template function,

template <typename Meta>
GMetaType metaGetItemType(const Meta & item)
{
    GMetaType type;
 
    const_cast<Meta &>(item)->getItemType(&type.refData());
    metaCheckError(item);
 
    return type;
}

My original opinion is that the function can accept both raw pointer and smart pointer. So no matter item is a IMetaItem *, or a GScopedInterface<IMetaItem>, the function can work. Handy? Yes. Good? No. The problems are,

  1. The API is not minimal any more. Though there is only one function metaGetItemType, since it's a template, it has two instantiations, one for IMetaItem *, and one for GScopedInterface<IMetaItem>. However, we can always call scopedinterface.get() to get the raw pointer, so the smart pointer version is not necessary.
  2. More serious problem, the function is not self explained. Without reading document or the function body, do we know what's type “Meta”? Of course not.
  3. Overuse of C++ template. C++ template is one of the most powerful feature, but I would not use it when I can. C++ template should not be used as shortcut method.

So since version 1.5.2, I changed all those utility functions to non-template functions.

Caveats

The principles we discussed are only for the public API in cpgf. The internal API doesn't accept the principles. For example, in script binding all interfaces don't require a releaseReference.

Also, even in the public API, it's not guaranteed that all APIs accept the principles well, this is same as we can't guarantee there is no bugs in a software.

Discussion

Enter your comment:
WMWUM