Tuesday, April 19, 2011

Designing Better API



1.Each API Interface should be the concise Cover Story of What / How User can achieve out of the API.

We should Code the Use-Case as API.

Each class/interface/method should clearly specify if the implementor / extender needs to do something additional to ensure Performance, State management and Threading. It should say  how to use it. 

It should spell out clearly – what varargs / enums are being used to serve what purpose.

Here are the guidelines provided in http://wiki.eclipse.org/Evolving_Java-based_APIs  - what I call as Bible for API Architects - Courtecy : Jim des Rivières, IBM

We should think deeply how the external world going to provide the service for a requirement.
When we design the requirement as interface if in doubt about an operation we should leave it !

2.How to mark an existing API as obsolete ?
How to advertise that a new API method is available ?

should use @deprecated

3. How to communicate to users whether to extend or not ?
@noextend  (Eclipse API annotation)

4. What can be exposed ?
Only public static final primitive, string constants, enum values and immutable object references.

5. Document thread-safety of super class methods
@ThreadSafe public class Account {
  @GuardedBy("this") private int balance;
,......
}

Make it very clear if a Task needs to wait or scheduled in future.

ExecutorService service = Executors.newSingleThreadExecutor();
// scheduling is quite different
Future task = service.submit(someRunnable);
// compared to waiting
task.get(1000, TimeUnit.MILLISECONDS);
This builds on the expected knowledge that waiting is something that can cause deadlocks and that
developers should do so with care.

6. An API can be evolved easily if we expose Factory methods instead of Constructors.
Advantages :
(i) we can return a sub-class / interface from the static factory method . so there will not be any tight coupling with a concrete class.
(ii) we can cache instances
(iii) a factory method can be synchronized as a whole, including
potential code before the object creation, the code that creates the instances, as well as the
rest of the code that runs after the object is created. This isn’t possible in a constructor at all.
(iv) Factory methods are not constrained by the type of the enclosing class.

7. In API class / Interface we should not specify setter method.
For example javax.swing.Action has setEnabled(boolean) method - which should be placed in the class AbstractAction .. because this method is specific to implementation not the skeleton api i.e. while creating an Action user need not invoke setEnabled(boolean). In some special cases, if user needs to invoke setEnabled(false) then he should use AbstractAction.

8. API is all about good names ! Thats it !
 if (car.speed() > 1.5*SPEED_LIMIT)
   alertDriver("Watch out for Cops");

Bad Example :
Thread.interrupted() -- just a horrific name !  It should be a present-tense verb like -clearInteruptStatus() !!

Good Example :
public interface List {
List subList(int fromIndex, int toIndex);
}
Very powerful. easy to remember ...without document

8.                 Forbid access to the package containing implementation classes .

 Do this on the class-loading level, - by suffixing pkg name 'internal' when developing bundles inside Eclipse or make them pkg-private for non-osgi environment.

9. Always introduce an intermediate bridge interface to maintain backward compatibility

 If we have  class say - public abstract class HelloVisitor with 2 methods
  - helloAll()
  - helloVisitor(Visitor visitor)
 In next release, you decided to introduce new class- public abstract HelloAll with only one method - helloAll()
Then for backward compatibility make  HelloVisitor extend HelloAll.


10. Use Builder pattern as a method is better than a variable.

docInfo= DocumentInfo.empty().nameProvider(p).urlProvider(p).normalize(p);

Never allow more than 3 parameters in arg list
>> break up into smaller methods .. Builder Pattern .. Named params
>> Create helper class to hold params

11.  Clear seperation between API and SPI

An API is something that Clients call and an SPI is something that Providers
implement.
For example, in case of the FileSystem API, the FileObject is there to be called, while the
LocalFileSystem and JarFileSystem are there to be implemented.

Adding a new method in API delights its client. Whereas adding a new method in SPI breaks the provider's codebase !!

12.            Catch the exceptions early as part of Pre Condition Validation.

Remember to throw an Exception at a Consistent Level of Abstraction :

class Employee {
 public TaxId getTaxId() throws EmployeeDataNotAvailable {....}
}

We should not throw a SQLException inside a BankAccountService API rather throw AccountNotAvailable exception.

13. We should adopt 'Annotation' over 'Configuration'.
Spring 2.5 takes a huge step in this direction by providing you with the option
to annotate implementation classes:
@Service
public class SimpleScramblerAnnotated extends SimpleScrambler {
public SimpleScramblerAnnotated() {
}
}
@Service
public class StaticWordLibraryAnnotated extends StaticWordLibrary {
public StaticWordLibraryAnnotated() {
}
}
@Service("ui")
public class AnagramsAnnotated extends AnagramsWithConstructor {
@Autowired
public AnagramsAnnotated(WordLibrary library, Scrambler scrambler) {
super(library, scrambler);
}
}

You use an indication that identifies the exported beans, their names, and potentially also
their dependencies on other APIs that need to be provided for the implementation to work
correctly. The final assembler no longer needs to write a configuration file with references to
all names of implementation classes, but rather can instruct the Spring Framework to use the
provided annotations.

It’s still necessary to know the name of the packages that contain all the implementations.
However, that is significantly less knowledge than knowing all the classes and their parameters.

14. Public Concrete  Class should be Final and Immutable.
            A Classic example of Non-Immutable Class is Calender which should have been designed a Mutable because its job is to just mutate Date.  Another bad example in Java is Collection Classes which should have been declared Final as they should never be extended rather decorated !

Classical design mistakes :: Stack is not a vector .. it has a vector ...
Similarly Properties is not HashTable, but it still extends HashTable.

clone(), readObject(), constructor should never invoke overridable method.

15. Avoid Overloading !! Yes thats true !!
Avoid overloading .. always use meaningful method names !!

Bad Example :
1. public TreeSet(Collection c) // Ignores order
 >> creates a new comparator
2. public TreeSet(SortedSet s) // respects order
 >> uses the comparator of SortedSet

Imagine client calls ..
SortedSet mySet = ..
TreeSet tset = new TreeSet((Collection)mySet)
#1 will be invoked .. and the order already present in SortedSet will be ignored .. as it will use a new comparator ..

If the behaviors of two methods differ, it's better to give them different names

The above confusion arises due to the fact that – Collection Fwk tried to be Flexible by providing overloaded constructors !!!!

AbstractSet, AbstractList are good examples !

16.  In API, paramerts should be ordered in meaningful fashion :
java.util.concurrent provides good examples : .. long delay, TimeUnit unit

17.            Be careful while making super class methods thread safe.

The suggested way is to use synchronized in the definition of a method. Fine, that can work for me.
I am able to write my code in one class without deadlocks. But what if somebody subclasses my object and
also uses synchronized methods? Well, in that case it uses the same lock as mine, usually without any
knowledge as to how I should use the lock, because my synchronized methods can be private and thus not
of interest to subclasses. The result? Deadlocks everywhere.

Subclassing and using synchronized methods sneaks into the superclass’s own synchronization
model.That can completely invalidate any synchronization assumptions the
superclass has made. Doing so is just plain dangerous.

18. API should provide toString implementation
Provide programmatic access to all data available in string form. Otherwise, programmers will be forced to parse strings, which is painful. Worse, the string forms will turn into de facto APIs.
Good StackTrace[] getStackTrace()

19.  "It’s good to be reminded that the sole
purpose of overridable methods is to replace a table mapping method name with actual code
snippets."

java.util.Arrays has many toString methods, each accepting
different array types, such as byte[], short[], and Object[]. These methods are therefore
overloaded. When called, the compiler chooses the best one depending on the type of the
parameter that is passed for method invocation.

20. API should encpsulate boilerplate code ..
   User need not bear the burden of heavy-lifting the bulk of logic , say implementing a Transformer to transform a Dom model etc.
 There should be a simple method writeDoc(...)  which should hide the details.

21. Return zero-length array or empty collection - not NULL
NULL Patterns ::

class Service {
  private final Log log;

  Service() {
    this.log = new NullLog();
  }
}

public final class NullLog implements Log {
  public void write(String messageToLog) {
    // do nothing
  }
}

22.  Contrary to popular belief – do not throw CheckedException so frequently rather throw it very sparingly !

Bad Enforcement JDK !!!
we know it will succeed .. but still client have to catch it ..
try {
Foo f = (Foo) super.clone();
} catch(CloneNotSupportedExceptio e) {

}

23.            Make everything Final then open up / expose on need basis.

24.            Implemenation Class must be Immutable

Do not provide a getter method like getValues() that exposes private internal mutable object state
without defensively copying the state.

Collections.unmodifiableList(Arrays.asList(items));

25. API should not wait for response rather it should strive to reduce latency

Defer the processing logic in Provider API and return immedialtely to Client

Create a client API that will post a Future task to a Provider API wrapping up a Single Processor Executor. When user will call the client API either return immediately or time-out.

26. While resolving Classes for supporting Multiple APIs always use compliant solution.
"Two classes are the same class (and consequently the same type) if they are loaded by the same
class loader and they have the same fully qualified name" [JVMSpec 1999].
Noncompliant --
>> if (auth.getClass().getName().equals("com.application.auth.DefaultAuthenticationHandler")) {
  // ...
}

Compliant  --
>> if (auth.getClass() == this.getClassLoader().loadClass
("com.application.auth.DefaultAuthenticationHandler")) {
  // ...
}

27.            Design Composition and Aggregation inside API using Decorator and Delegate patterns.

Identify where responsibility can be attached in runtime using Decorator by discouraging the usage of Inheritence.

28.    Handle memory-leak scenarios in Base Implemnetaion Classes

Its important to encapsulate the core logic of managing listeners, indexes, caches, data-binding, thread-executors, validators etc. Inside base framework classes.
Its absolutely essential to identify the scenarios of using WeakHashmap, cleaning up resources, visitor patterns to minimize 'instance-of' checking, static inner classes.

References :


API Design and Evolution

Java Coding Practice Guidelines

No comments: