Tips for defining API's

An API that isn't comprehensible isn't usable. --James Gosling [source]

Overview

An API (Application Programming Interface) is a set of operations a software component (i.e., a system, a sub-system, class, or a function) provides to its clients. For example, we use the API of the String class provided by C#/C++/Java library in our programming. We are the client of the String class, and we only know the public operations (i.e., the API) it provides. We can still use the String class without knowing the details of its implementation. A well-defined API is not only easy to use, it gives the implementers (of that component) flexibility in implementation. As clients of the component write code against the API (i.e., they only know about the API), implementers have the freedom to change internal implementation details. As long as the API remains stable, the client code will not have to change.

When we design a system, we should use APIs to achieve a similar degree of freedom between components which allows developers of those components to work independently of each other. Let us assume our system has two sub-systems: front-end and back-end. Let us also assume that the front-end uses the back-end. In this case we must first define the back-end API. Once that is defined, both the front-end team and the back-end team can start work in parallel instead of the front-end team waiting until the completed back-end is available. That is because the front-end team is assured that the back-end will support the agreed API in time to come irrespective of how it will be implemented. Therefore, a vital part of designing software is defining APIs.

Tips

Talk to the client (of the API)

To define the API of a component , we must clearly understand the needs of component’s clients. The best way to discover an API of a component, therefore, is to talk to the clients of the component. For example, the client for the back-end is the front-end. Therefore, the back-end API should be defined based on a discussion between front-end team and the back-end team. Similarly, if you are writing a utility class to be used by many other classes in the system, you must define the API of your class based on the requirement of those client classes.

Use descriptive names

API operation names and parameter names should have intuitive meanings. E.g., setParent(parent, child) is more meaningful than setPrt(p,c)

Choose standards, follow standards

If you have two operations addVariable() and removeVar(), you are not following a standard way of naming. Name the second operation removeVariable(), or the first operation addVar(). When using a consistently named API, users can simply deduce the correct operation to call without actually checking the API documentation.

Don't give away the internal details

The API should not reveal clues about the internal structures. For example, addToVarHashtable() reveals that we are using a Hashtable to store variables; what if later we want to change it to a Vector?

Avoid Swiss army knife operations

Having multi-purpose methods causes unnecessary complexities, performance penalties, and misunderstandings. Make each operation do one thing and one thing only. Rather than have a writeToFile(string text, boolean append) where the second parameter indicates whether to overwrite or append, have two methods appendToFile(string text) and overwriteFile(string text).

Caveat: An over-enthusiastic application of this guideline could lead to a bloated APIs

Have a complete set of primitive operations

Make sure the component provides a complete set of primitive operations. You can add more specialized operations as necessary and implement them using the primitive operations. Even if you don't supply those specialized operations, clients will be able to accomplish such specialized tasks using the primitive operations. This avoids polluting the API with highly specialized and rarely used operations.

Avoid API bloat

If the API is becoming too long, may be the component is doing more than what it should; consider splitting the component. Or else, the component may be supporting too many operations to achieve the same thing; strive to achieve a minimal yet complete API (refer the previous point about primitive operations).

Make common things easy, rare things possible

This is another way to look at the previous two points. On the one hand the API should provide direct and easy-to-use ways to accomplish common tasks. On the other hand, rarely-used highly-specialized tasks need not be directly supported; instead, we can let clients implement these operations themselves, using a combination of existing primitive operations.

Promise less

An API is a contract (between the implementer of the component, and the user of the component); just like any contract, promising less makes it easier to deliver. E.g., If an operation is for calculating the root of positive integers, it should be findRoot(positiveInt i), not findRoot(int i)

Be clear about abnormal behaviors

An operation should throw exceptions to indicate exceptional situations (e.g., when an input file was not found). These should be clearly specified in the API. A common mistake is to specify only  the typical behavior.

Choose atomic operations over multi-step operations

When a set of steps are always done together in the same sequence, capture that as one atomic operation instead of having separate operations for each step. For example, imagine that your component has to parse a file that contains some program code and create the corresponding abstract syntax tree (AST).
Option 1: have one operation that takes the file name as the parameter and returns the resultant AST  parseFile(String fileName): AST
Option 2: have three operations setFileName(String fileName), parse(), getAST(): AST
Clearly the first option is better; it makes the API shorter and reduces chances for error (what if we call parse method without calling the setFileName method first?)

Make the API match the abstraction

The operations in the API should be in tune with the abstraction the API represents. For example, a Parser API should use terms related to Parsing. This is also applicable to the exceptions thrown by the API. For example, a Parser API should throw exceptions that are related to a Parser.

Keep the API on the same level of abstraction

Try to keep all operations of an API on the same level of abstraction. An API that has a mix of high-level and low-level operations is harder to understand.

Evolve, as necessary

While we try to keep APIs stable, it is not uncommon for changing client requirements to force changes in existing APIs. If you have multiple clients, make sure that a change requested by one client does not adversely affect the other clients.

Also, we sometimes evolve the API as our understanding of the client requirements improves.

It is ok to specify the data types in the operation header using symbolic names, and later refine them into concrete types. For example, the return type can be first specified as list-of-TreeNodes and later refined TreeNode[] or ArrayList<TreeNode>

Document the API

The API documentation should be precise, unambiguous, concise, and complete. Do not leave room for the client to make assumptions. Instead, explicitly specify what the method does. Here is an example standard way of documenting APIs.

Component_name {

Overview: explain the rationale and the responsibility of a component

Public interface: here you will list interface operations documented as follows

Operation header: returned-value operation-name (list of parameters with names and types)

*Parameters (optional): unless it is not clear from the header, describe parameters

Description: here you describe the effects of the operation (what the operation does) in terms of parameters, returned value and whatever else you need to explain the meaning of the operation; it is most important that you describe both normal and abnormal behavior (handled by assertions and exceptions)

}

Here is an example API description, based on the above format:

AST {

Overview: AST represents the abstract syntax tree for a program, and it takes the form of a tree of Tnode objects

Public interface:

Tnode getParent (Tnode p);
Parameters: ‘p’ refers to any AST node
Description: returns the parent of AST node referred to by ‘p’; returns null if ‘p’ is the AST root. Throws an exception if p is null.

void setParent (Tnode parent, Tnode child);
Parameters: parent and child refer to AST nodes
Description: creates a Parent relationship among nodes parent and child. Throws an exception if child already has a parent, or any of the parameters is null.
post condition: parent == child.getParent ()

}

Generate API documentation from the code

Find a way to generate the API documentation from the code (and comments therein) itself. An example of this strategy in action is the Java API documentation generated by Javadoc comments embedded in the code.

Grading tips

Try to follow the above guidelines in all the classes you write (even when only you use a certain class), not just for the APIs between sub-systems developed by different sub-teams.

Giving feedback

Any suggestions to improve this book? Any tips you would like to add? Any aspect of your project not covered by the book? Anything in the book that you don't agree with? Noticed any errors/omissions? Please use the link below to provide feedback, or send an email to damith[at]comp.nus.edu.sg

Sharing this book helps too!

 

---| This page is a part of the online book Tips to Succeed in Software Engineering Student Projects V1.9, Jan 2009, Copyrights: Damith C. Rajapakse |---

free statistics free hit counter javascript