CodeProjectI built up products from scratch several time in my professional life. Usually it starts with a very small engineering team –sometimes I was the very first member of the team. This is a great opportunity to lay strong foundations for the subsequent software development, because one is in charge of the whole process. But one does not always have the chance to start from scratch.
I also worked with already established products, with larger team and millions of lines of already existing code. The typical software management and development project always offers some cumbersome legacy code and API that survive years after years. The reason is not so much that people do not want to fix the problem, but that fixing the problem requires a major product architecture overhaul, which comes to a prohibitive cost. There are striking lessons in failed software architectures, and it all start with API design. I am sharing here my practical experience with C++ projects, but most of these advices also apply to Java.
Why is API so important?
An API can be a company’s greatest asset: it captures communication and exchange of services in an application. A good API will naturally lead to more reuse, simpler code, and lower maintenance cost. If the API is public, a good API will also capture customers. There are examples of Java libraries that failed to be accepted not because they were inefficient, but because they very poorly designed.
An API can also be a company’s greatest liability: once the service has clients, one can no longer change the API! Suspending or rewriting an API is very pricey in terms of time and money. In the case of a public API, cost also comes in terms of reputation. A public API is forever: there is only one chance to get it right.
What is a good API?
In today’s object-oriented software, writing an API is providing a service. Thus instead of thinking in terms of implementation and efficiency, one must first think in terms of modules and services: determine the usage model; establish the clients’ needs; and anticipate tomorrow’s needs.
Besides being powerful enough to satisfy the requirements, an API should be designed with two principles in mind:
- Keep it simple! An API must be easy to learn and use, even without documentation. The API must be hard to misuse. Functionality should be easy to explain –if it is hard to name, it is likely a bad function. Use simple, consistent naming, and the code will read like a prose –Java libraries and STL are good inspirations for naming conventions. The API should be as small as possible: you can always add to an API, but you can never remove. A method should not take more than 3-4 parameters –else wrap the parameters in a class that can be augmented later.
- Keep it abstract! An API must allow extension for future needs. For example, it should not assume anything about the implementation. It should minimize accessibility to implementation-specific details –an API, once public, will be used, and you do not want to expose the ugly details of a database.
In theory, an API should be written before going into some implementation. Gathering requirements is the first step. Requirements must be case-driven, specific, and should be questioned relentlessly until proven to be must-have. The API should then be written in the target language (C++, Java, etc): this will force the development team to make choices, and to keep the API simple and abstract enough –nobody wants to have too much to discuss! Then the API should be reviewed and made final in a public forum with the two principles above in mind: keep it simple (so that it is easy to support) and abstract (so that it is easy to extend).
An API should be documented, but well-designed APIs are sometimes self-explanatory. An API should answer the following questions about its components.
- Class: what does an instance of a class represent? Is that a singleton class? Is there a factory? Who owns the memory?
- Method: what does it do? What is the contract between the client and the instance? Is there any precondition and post-condition? Is there any side effect?
- Parameters: what do they represent? Which information do they carry? Who own them?
- Exceptions: who throw exceptions? What do they mean? What to do when catching one?
API and performances
Bad API decision can limit performances. When designing an API, it is good to consider the following rules.
- Avoid mutability. If a method returns a mutable instance, that instance needs to be created somewhere, which raises the question of memory ownership. Also mutable classes limit thread-safeness. Use ‘const’ whenever possible.
- Avoid implicit call to copy and assignment operators. This is a waste of resources if you can use references. Declare these operators ‘explicit’ or ‘private’ to catch any misuse at compile time.
- A factory is often better than constructors. A factory has full control on how instances are created and when they should be released (shared model, garbage collection, save/restore, caching and disk mirroring, etc). A factory can return an instance of a sub-class.
- Avoid exposing implementation details. It may prevent later improvements of a database. Never expose data members of a class, always use get/set accessors.
- Question the thread-safeness of computational-intensive methods. One day the software may run on a grid or in a cloud.
- Never compromise the rules above for a small runtime or memory improvement. For the vast majority of the applications, going a few percents faster is not worth the maintenance nightmare it can imply.
A good API is a key to produce smaller and simpler code, which makes the product more stable and easier to maintain. Designing a good API is a collaborative effort, and a formal decision process is needed to freeze an API. A good API is hard to write, get your best people on it. And finally, a public API is forever. May these simple rules guide your next project.