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
A class should have smallest number of public functions. A public function is added to a class when and only when,
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.
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.
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.
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.
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.
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,
So since version 1.5.2, I changed all those utility functions to non-template functions.
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.