Google

Jul 4, 2012

Why does good API design matter?

Q. Why is a good API design important?
A.The Application Programming Interfaces (i.e. APIs) are fine grained services/libraries used by other applications, systems and libraries. A good API design improves flexibility, stability, and extendability of your application.

Now a days, applications are built as reusable components or services. A single page rich web page invoking multiple back end services via ajax  calls is very common. Services are used to put information into cloud storage, retrieving data from cloud storage,  validating data, updating a cloud based database, etc. This makes it even more important to carefully design your API.

Q. Give examples of a bad and good API designs in core Java API?
A. Here are some examples from Core Java API.

Bad Designs:
  • The Java Date and Calendar APIs are inconsistent and unintuitive. Firstly, it's full of deprecated methods and secondly, it makes it hard to do simple date operations. The Date and the Calendar classes are very complicated. Use the Joda Time API as it is well designed and easier to use.
  • Some of the legacy classes, which has synchronized methods everywhere are extended by other classes. For example, The Properties class extends the Hashtable, Stack extends the Vector class, etc.
Good Designs:
  • The Joda Time Library. This will be added to Java 7.
  • The Java Collection API. 

Q. What are some of the key considerations for a good API design
A. 

1. Define task oriented APIs

It is easier to add new methods. Deleting methods will break your client code or service consumers and deprecating methods can make your API more complicated to use.  Hence, favor fine-grained APIs that does a particular task. This also means you need to invoke many API methods to get the desired behaviour. When you are making remote calls over web services or cloud services, the performance may be adversely impacted. Good design is more important than just coding for performance. Premature optimization of code is an anti pattern. In rare instances where your profiling indicate performance issues, expose a number of fine grained services as a new coarse grained service. Remember -- it is easier to add new methods/services than deleting or deprecating existing methods/services. For example

When designing APIs, always think of the caller.

  • How will the caller use the API? 
  • Are method names and signatures intuitive enough for the caller? 

For example 

Bad design: A single method does too many things. The method name and signatures are ambiguous

public interface CashService { 
 BigDecimal getBalance (CustomerAccount account, BigDecimal currentBalance, List<TransactionDetail> txnDetails, 
                        boolean includeIncome, boolean includePending, boolean includeUnconfirmed) throws CashBalanceException;
}

Better design: Finegrained APIs that does a particular task. No ambiguous boolean flags or method names.

public interface CashService {

 BigDecimal getAvailableBalance (CustomerAccount account) throws CashBalanceException;

 BigDecimal getAutocashAvailableBalance ( BigDecimal currentBalance, List<TransactionDetail> txnDetails) throws CashBalanceException;
 
 BigDecimal getPortfolioBalance (BigDecimal portfolioBalance, List<TransactionDetail> txnDetails) throws CashBalanceException;;
 
 BigDecimal getRealtimeAvailableBalance (BigDecimal realtimeBalance, List<TransactionDetail> txnDetails) throws CashBalanceException;

}

The public interface methods defined above could internally (i.e. in the implementation) use a number of reusable private methods to perform the calculation. When exposing functionalities via a public interface, take a minimalistic approach to expose only those methods that will be required by the caller. Remember: it is easier to add methods than to remove.




2. Instead of the boolean flags use the type safe enum. The boolean true/false flags are ambiguous.

Bad design: boolean flags make the API difficult to understand

public interface UserService { 
 User getUser (String firstName, String surname, boolean isActive, boolean isMale, boolean ignoreSex) throws UserException;
}


Better design: the enum can take on the values like ACTIVE_MALE, ACTIVE_FEMALE, INACTIVE_MALE, INACTIVE_FEMALE, etc. As the method can take an array of user types with the varargs feature.

public interface UserService { 
 User getUser (String firstName, String surname, UserType...type) throws UserException;
}

3. Instead of passing many individual arguments corrupting the method signature, pass a well defined object that wraps all the required values.


public interface UserService { 
 User getUser (User user) throws UserException;
}
Where the User object looks like

public class User implements Serializable{

  private String firstName;
  private String surname;
  private UserType[] userTypes.
  //other attributes

  //getter and setters

  //toString(...), equals(...), hashCode() methods  
}

4. If you are using other collection types like List, Set, etc, make sure that you use generics as it makes it clear as to what type of object is stored. E.g. List<User>, Set<usertype>, etc.

5. Your API design must include the precondition check. The method implementation must fail fast after validating the relevant input. For example,

public User getUser (User user) throws UserException {

     if(user == null && (StringUtils.isEmpty(user.getFirstName()) || StringUtils.isEmpty(user.getSrurname()) ){
      throw new UserException("Invalid input: " + user);
  }
  
  
  ....

}

6. Providing good exception/error handling is essential to give the developers using your API an opportunity to correct their mistakes. Error messages should be clear and concise. Messages such as “Invalid input” are highly unhelpful. Also, avoid using a single exception object covering a number of very different possible errors. The decision to throw an exception, an error code, or a value like null to indicate "value is not found" must be carefully considered.

public User getUser (User user) throws UserException {

     if(user == null && (StringUtils.isEmpty(user.getFirstName()) || StringUtils.isEmpty(user.getSrurname()) ){
      throw new UserException("User's firstName and surname are required. Invalid input: " + user + "");
  }
  
  
  ....

}

In the above code, if an input validation fails it makes sense to throw an exception to indicate that an invalid input is supplied. It does not make sense to throw an exception if the designer anticipates that looking for a user that isn't there is not a common case and likely to indicate something that the caller would deal with the condition by substituting with a default "guest user" or prompting the user to register. In this case, returning a null value will be appropriate as this is not an exceptional condition. So, knowing the context in which an API is going to be used will allow you to make the right choice as to throw an exception or not. If your API requires a user to be present before it performs a task, then it makes more sense to throw a "UserNotFoundException".

With careful design of types and parameters, errors can often be caught at compile time instead of being delayed until runtime. Having said this, exceptions compared to true/false and error codes have several important advantages:


  • Exceptions cannot be ignored. If your code throws an exception the caller has to catch it to avoid getting an unhanded exception. The error codes or true/false can be ignored.
  • Exceptions can be handled at a higher level than the immediate caller. If you use error codes you may end up in situations where you at all layers of you application have to check for errors and pass them back to the caller. So, error codes are more appropriate at the highest layer. 


What ever technique you use, be consistent in your approach. Decide early on whether to use a checked or unchecked exception. If planning to use both, clearly have a plan as to when to use a checked exception and when to use an unchecked exception.

Labels:

2 Comments:

Blogger Owen Fellows said...

I general I agree with what is written here except for throwing a UserException when preconditions fail. Why not just throw an IllegalArgumentException which is a runtime exception and therefore does not need to be declared.
If you have a real exception then you could throw a UserException but doing it this way saves your caller having to deal with an exception which, in reality, will never be thrown.

10:36 PM, July 04, 2012  
Blogger Unknown said...

Good point Owen, and agree with you. The UserException shown above could be a checked (i.e. compile time) exception or unchecked exception (i.e. runtime)

9:53 AM, July 05, 2012  

Post a Comment

Subscribe to Post Comments [Atom]

<< Home