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 provides to its clients. Here, a component can be as large as a system, or as small as a class. For example, you use the API of the String class provided by C#/C++/Java library in your programming. You are the client of the String class, and you only know the public operations (i.e., the API) it provides. But you 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 module) 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 (and the teams working on those components). 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, no matter how it is implemented internally. 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(s,t)

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 well-defined 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. E.g., 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 way, you can avoid polluting the API with highly specialized and rarely used operations.

Avoid bloated APIs

If the API is becoming too long, may be the component is doing more than what it should. In this case, consider splitting the component. Or the component may be supporting too many operations to achieve the same thing. In this case, 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. 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 a client violates a precondition). 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 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?)

Evolve, as necessary

While we try to keep APIs stable, it is not unusual for them to change later, based on changing client requirements. 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, 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 ()

}

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 API's 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.8, 3rd Sept 2008, Copyrights: Damith C. Rajapakse |---

free statistics free hit counter javascript