Tuesday, May 01, 2007

API Design Guidelines

I keep getting back to some references on the best practices for C++/Java API design so I decided to summarize them all here for future reference.

General design guidelines

A somewhat better API might have been

  • General naming rules: Do not abbreviate. Even obvious abbreviations such as "prev" for "previous" don't pay off in the long run, because the user must remember which words are abbreviated.
  • Naming classes: Identify groups of classes instead of finding the perfect name for each individual class.
  • Naming functions and parameters: The number one rule of function naming is that it should be clear from the name whether the function has side-effects or not. Parameter names are an important source of information to the programmer, even though they don't show up in the code that uses the API. Since modern IDEs show them while the programmer is writing code, it's worthwhile to give decent names to parameters in the header files and to use the same names in the documentation.
  • Naming boolean getters, setters, and properties: Finding good names for the getter and setter of a bool property is always a special pain. Should the getter be called checked() or isChecked()? scrollBarsEnabled() or areScrollBarEnabled()?
    • Adjectives are prefixed with is-. And be consistent: Adjectives applying to a plural noun have no prefix (like are).
    • Verbs have no prefix and don't use the third person (-s):
    • Nouns generally have no prefix. (Generally because sometimes not having an is prefix can be misleading)
  • Write to your API early and often: Code lives on and is understood as examples, unit tests
  • Implementation should not impact API: Implementation details confuse users and inhibit freedom to change implementation. Shield your users from them
  • Minimize visibility of everything
  • Documentation matters: An API is supposed to be read (several times) and understood by others and this kind of people normally don't read all words you write (I know I don't) so be succinct, precise and cover every single class/function.
  • Consider performance consequences of API design decisions: Bad decisions can limit performance (Making type mutable; Providing constructor instead of static factory; Using implementation type instead of interface)
  • Fail fast: Report errors as soon as possible after they occur
  • Avoid long parameter lists
  • Avoid return values that Demand Exceptional Processing: return zero-length array or empty collection, not null
  • Provide programmatic access to all data available in string form, Otherwise, clients will parse strings
  • Overload with care: Avoid ambiguous overloadings. Just because you can doesn't mean you should and it's often better to use a different name
  • Use appropriate parameter and return types:
    • Favor interface types over classes for input: Provides flexibility, performance
    • Use most specific possible input parameter type: Moves error from runtime to compile time
    • Don't use string if a better type exists: Strings are cumbersome, error-prone, and slow
    • Don't use floating point for monetary values: Binary floating point causes inexact results.
    • Use double (64 bits) rather than float (32 bits): Precision loss is real, performance loss negligible
Java specific
  • Supply Interfaces (Good reasons: Callbacks, Multiple inheritance, Dynamic proxies), but keep in mind that interfaces:
    • Can be implemented by anybody.
    • Cannot have constructors or static methods.
    • Cannot evolve.
    • Cannot be serialized.
  • Be careful with packages: The Java language has fairly limited ways of controlling the visibility of classes and methods. In particular, if a class or method is visible outside its package, then it is visible to all code in all packages. This means that if you define your API in several packages, you have to be careful to avoid being forced to make things public just so that code in other packages in the API can access them. The simplest solution to avoid this is to put your whole API in one package. For an API with fewer than about 30 public classes this is usually the best approach. If your API is too big for a single package to be appropriate, then you should plan to have private implementation packages.
  • Avoid Static Methods
  • Avoid making classes and methods sealed or final or non-virtual
  • Immutable classes are good: If a class can be immutable, then it should be.
  • The only visible fields should be static and final.
  • Avoid eccentricity. There are many well-established conventions for Java code, with regard to identifier case, getters and setters, standard exception classes, and so on.
  • Don't implement Cloneable: It is usually less useful than you might think to create a copy of an object. If you do need this functionality, rather than having a clone() method it's generally a better idea to define a "copy constructor" or static factory method.
  • Exceptions should usually be unchecked: Use a checked exception "if the exceptional condition cannot be prevented by proper use of the API and the programmer using the API can take some useful action once confronted with the exception." In practice this usually means that a checked exception reflects a problem in interaction with the outside world, such as the network, filesystem, or windowing system. If the exception signals that parameters are incorrect or than an object is in the wrong state for the operation you're trying to do, then an unchecked exception (subclass of RuntimeException) is appropriate.
  • Design for inheritance or don't allow it: Every method should be final by default (perhaps by virtue of being in a final class). Only if you can clearly document what happens if you override the method should it be possible to do so. And you should only do that if you have coded useful examples that do override the method.

C++ specific

  • Static Polymorphism: Similar classes should have a similar API. This can be done using inheritance where it makes sense -- that is, when run-time polymorphism is used. But polymorphism also happens at design time. Static polymorphism also makes it easier to memorize APIs and programming patterns. As a consequence, a similar API for a set of related classes is sometimes better than perfect individual APIs for each class.
  • Naming Enum Types and Values: When declaring enums, we must keep in mind that in C++ (unlike in Java or C#), the enum values are used without the type. So one guideline for naming enum types is to repeat at least one element of the enum type name in each of the enum values.
  • Pointers or References: Most C++ books recommend references whenever possible, according to the general perception that references are "safer and nicer" than pointers. However, sometimes it's best to use pointers because they make the user code more readable by making it clear that there's a high probability that its parameters will be modified by the function call. By accepting references one could infer from a function call that looks by copy is actually by reference. You can't have this doubt with pointers.


Anonymous said...

Good material, but you should reference where some of this information came from:


Ricardo N. Cabral said...

Thanks, but it is referenced. Look at the links on the first sentence of this post.

Steve said...

I'm about to develop some APIs and do appreciate your pragmatic view.