Plenary session on 3/31 - how to design a good API - and why it matters. Given by Joshua Bloch, senior staff engineer at Sun. The need for good API's remain as languages and platforms change - what do we mean by APIs here - the public interface to a component or sub-system. There are going to be examples (good and bad) given.
Why is API design important?
- APIs can be amongst a company's greates assets
- Customers get attuned to the apis that they have studied, and write (and buy) code that is aligned with those APIs
- APIs capture and retain customers
- A badly designed API is an unending source of support calls - and public APIs are forever. Once it's loose, you can't change it - you can only add to it
If ypu program, you are an API designer. Each module you create ends up with an API - either real or ad-hoc. Useful modules tend to get reused. Once you have users, you are not at liberty to change it - people rely on it. These "private" apis can be an asset or a liability.
- Easy to learn
- Easy to use, even w/o doc
- Hard to misuse
- Bad ones will be easy to misuse
- Easy to read and maintain code that uses it
- Sufficiently powerfil to satisfy requirements
- Easy to extend
- Appropriate to audience - no such thing as a "perfect" API for everyone
Methodology for API design - there are a few things that tend to lead to good API design. Gather requirements, and be skeptical. The audience will tell you what they want (may be different from what they need). Extract the real requirements from what you get told and what you observe. Use Cases are the best way to do this - what are the easiest things to accomplish with that API? Do the Use Cases look good when using the API? If not you have a problem. It can be easier to build something more general than what you are asked for - but don't go to "framework-itis
Start with a small (1 page) spec. Agility is good at this point. Bounce the spec off of as many people as possible. Keep the spec to a short description and method names - flesh it out only as you gain confidence. Shop it around while it's simple. When you get a large spec, it's hard to change (and involves politics).
Write the API before you implement it. Saves an implementation that may be useless. Start coding to it when it's a 1 page spec to test (TDD driven API design?). Continue to write code against the API as you develop as a constant check against it.
Writing to an API (Service Provider Interface) is even more important.
- Interface for supporting multiple implementations
- Example - Java Cryptography
Write implementations against it before you release it to avoid large risk of failure - more than one (3 or more is best).
Keep expectations realistic. Won't be all things to all people. "Aim to displease everyone equally". Say you have 6 customers - 5 happy, one angry - not good. 6 grumbling "can work with it, but..." - likely ok. Expect to make mistakes - you'll make them, and you'll have to evolve (NOT change!) the API.
General Principles - should do one thing, and one thing well. Use good naming. If it's hard to name, it's a bad sign of code smell. Bad names mean poor understanding. Be open to refactoring (splitting and merging modules). The API should be as small as possible but no smaller (Occam's razor). When in doubt, leave it out - in terms of classes, methods, functionality - they can be added later, but once it's there, it's there. Conceptual weight is more important - how long will it take to learn the API? A Java example - if you implement an interface that is already there, people have a guide.
Implementation should not impact the API. Implementation details constrain you and confuse users. Don't over specify (e.g., don't specify the details of a hash function...). Don't let implementation details leak into the API. Exceptions, on the wire, or on disk formats are good examples where leakage can hurt. The initial Java String hash function (which they almost had to live with) was very bad. Actual is better, but still specified (bad).
Minimize accessibilty - maximize information hiding. Make instance variables private (i.e., in ST, no accessors). APIs are a "little language" - be consistent - parts of speech and basic names. Intention revealing names important.
Documentation matters, a lot. Reuse won't happen without good design and good documentation. Document every class, every interface, every method, constructor, parameter, exception (ed. - most methods should not need doc - especially internal, non API ones. API methods, likely should. IMHO, intention revealing names should do most of this). If there are state constraints, doc that, or risk unusability.
Consider performance implications of an API.
- Constructor instead of static factories
- Implementation type instead of interface
ed. - These are issues that, IMHO, most people should never have to worry about....
Bad API designs are permanent. Example: In AWT, Component.getSize returns (mutable) Dimension object. Each getSize call allocates a Dimension (ed. - that's a GC issue. VW has never had an issue here....)
API's must co-exist with the platform. Obey naming conventions, avoid obsolete stuff, etc. Take advantage of any language features. Avoid any language/platform specific traps
- Minimize mutability - classes should be immutable unless there's a good reason... not sure I agree with that...
- If mutable, keep the state space small and well defined
- Date and Calendar are bad examples
- Timer is good
Seriously, I don't know that - other than (previously mutable) Strings in Smalltalk, this is ever something I've had to worry about...
Subclass only when makes sense
- Liskov substitutability principle
- Inheritance violates encapsulation - he says you should use final - I disagree violently. To me, this is a classic engineer's mistake of assuming all possible future uses of a class or library. You can't see that. Now, admittedly, making classes interface compatible rather than inheritance compatible is safer - but "fragile base class" is more of a Java/C issue. Final is a bug.
- Don't make the client do anything the module should do - reduce need for boilerplate. There's an example of this from the DOM code in Java. If you require boilerplate, then your library is missing a few things. Think as a user of the API and encapsulate that sort of thing.
- Don't violate the "principle of least astonishment" (old Smalltalk adage :) ). Spending time on the API to make it easier is more important than performance. Example - method called "interrupted" in Thread clears state as well as reporting it.
- Fail fast - he likes strong static typing, but he's wrong :) In general, good principle goes with "least astonishment" - fail early when it makes most sense. Ironically, his properties example shows the limits of declarative typing. put() in HashTable accepts any objects, but actually enforces Strings in code.
- Don't make the client play "20 questions" when asking for state.
- Provide programmatic access to all data available in String form - otherwise people will parse them. This ends up turning those strings into a de-facto API.
- Overload with care - no two with same number of args (ed. - not a Smalltalk problem due to keyword messages :) )
- Use appropriate parameters and return types - "least astonishment"
- Favor interfaces
- Don't use Floats for money (amazing that this still comes up!)
- Use consistent parameter ordering - note again that keyword messages solve this whole problem :)
- Avoid long parameter lists - long ones cause problems (going to doc) - especially bad - long lists of identical types (again, solved by keywords...)
- Avoid return values that demand processing - he says not to return null (Wrong!) - In Smalltalk, answering nil is a well understood and widely used pattern.
- Don't use exceptions for control flow, only for errors. Don't fail silently!
- Favor unchecked exceptions. Checked exceptions cause boilerplate. See Parcel loading in Smalltalk for examples of this and the previous...
- Include failure/capture information with the exception
The examples are showing the Collection hierarchy in Java as a replacement for the older Vector stuff. Another example shows a cleanup of some Thread code (replace Strings and keys with key objects).
Can be rewarding and valuable for companies and teams. Suggests that these rules are guidelines, not hard and fast rules. API design is not simple. "Perfection is unachievable"