2013 m. lapkričio 30 d., šeštadienis

How to make good API

Good API is a large part of success. Sometimes it's the nearly only factor, why library/solution was chosen. It's not easy to create a good API and there is no single recipe for that, since different cases have different requirements. Everyone who has used many different libraries across several different programming languages should already have a feeling for what is a good API. I'll try to summarize main points here.

  1. Flexible but convenient
  2. Flexibility is something everyone understands, unfortunately convenience quite often forgotten. Some use cases are frequent, others are not. One key to success is to have dedicated API for most common use cases along side the general more flexible API.
    For example, consider a very simple Person class:
    • You need a no-argument constructor to create empty object, that will be filled later
    • You need methods to get/set first and last name
    • You need methods to get/set a list of middle names (because some people have more than one)
    The list above makes a flexible API. Now let's go to the convenience part:
    • A constructor, that takes first and last names as an argument, because that's what most people have
    • A method to set/get middle name, because very little people have more than one
    In some cultures most people have middle name, so if application is specific for such culture, replace two argument constructor with the one with three arguments.
    The point here is not to limit API to the basic all-cases set, but add additional APIs to make shortcuts for common use-cases.
  3. Flexible but not bloated
  4. This one is part of first one, but it's so frequent, that it deserves to be a separate point.
    Most APIs are designed in a "what-if" way, however you have to stop in time, because there are no limits to "what-if". There's no clear recipe here, since it all depends on exact domain. But there are few guidelines:
    • Something that very high impact and/or hasn't changed for a long time is unlikely to change without early warning
      • i.e. IPv6. We're moving in this direction so long, because too many software were "hardcoded" for IPv4. Were they wrong? No, they saved a lot by doing so. Right now you can chose to either support these two or to make your system flexible to support a growing list of different formats. Is the later worth the additional time? It's up to you to decide.
    • Convenience classes and method are only convenient as long as it's clear what they do and what's the difference between any two of them; here are couple of bad examples:
    • String is simple and very flexible data type
  5. Names should be meaningful, guessable, structured, consistent and short
  6. Naming is one the most important things in API. While meaningful is the most emphasized one, it's far from being the only one. Although most of the time programmer reads code, he also writes it, so being able to guess a name improves his productivity quite a lot, especially accompanied with code-completion.
    Structuring API is very important, when API is large. A good example of how not to structure your API is Window API, while GTK+ is an example of good structuring. In short: your API should have something that separates it from the rest of the world (namespace, package, ... or simple prefix). Large API should be divided into submodules etc.
    Another things that make names guessable is consistency. Naming conventions should be consistent across the API, preferably consistent with other APIs in the same field, domain, language etc.
    Finally, names should not be longer than needed. While longer usually means clearer, there's always certain point beyond which length no longer adds clarity. I.e. MAX_INT is perfectly fine name and making it MAXIMUM_INTEGER_VALUE adds no additional value.
  7. Performance oriented, but convenient
  8. Some APIs are not meant to be called often, others should think about performance. However, convenience should be maintained. Windows API is an example of API, that sacrificed convenience for performance. What it lacks is functions to fill various structures with some default values. It's really annoying to set every struct member. At the same time it is bloated in amount of functions, there are often several functions instead of one. I.e. FindFirstFile() and FindNextFile() could easily be just one functions and who really needs ZeroMemory() when you can use much more powerful memset().
  9. Convenient defaults
  10. The less user has to specify explicitly, the better. Most of the time...
    Default values should be intuitive and meaningful. Otherwise it's better to require to explicitly specify the value.
  11. Carefully chose data formats
  12. In short - don't just blindly use XML.
    If you're making Web Service, think which format for data is most appropriate. It might be XML, JSON or anything else. Remember, that someone will have to use it and it's for their sake.
    It's also important for configuration files. Forcing people to write XML by hand is... well don't use XML when something simpler works just fine.
  13. Configurable, but not over-configurable
  14. This one applies to frameworks. Many of them allow user to change the behavior via some setting in configuration. This is good, but there are limits. First of all, it should be clear which configuration is valid and which is not. The more settings there are, the harder it is to list all allowed combination, not to mention that they should work! "Everything is pluggable, extensible and configurable" is never the answer as it will result in huge and buggy mess. No setting is better then a non-working one.
    For complicated cases white-box can be used: instead of super-configurable black box component have a component composed of smaller components - when limits are reached, user can compose his own component reusing parts of original one.
  15. Synchronous vs. Asynchronous
  16. Asynchronous API is good for operation that might take long to complete. Ideally all long taking operations should be asynchronous.
    At the same time, every asynchronous API is only good if it provides synchronous alternatives. Strange? Asynchronous is good for UI as it makes it responsive rather than hanging it. But when you're already on on non-UI thread, you want to perform sequential actions synchronously, rather then messing up with asynchronous continuations. Since you can't predict where which API will be used, it's better to provide both to the user and let him decide, rather than pushing one or another down his throat.
  17. The are non-English speaking people out there
  18. If you have UI, it should be localizable. If you throw exceptions or otherwise report errors, error messages should be somehow localizable too, either provide error codes along messages, or localize messages themselves.
    Finally, if you have no idea about localization, don't implement it without consulting someone who does. A good start is GNU gettext manual, especially the section about plural form, to get some grasp what you're dealing with.
  19. Backward/forward compatibility
  20. Ideally every new version should be backward compatible with all previous ones, but in practice it's almost impossible. The guidelines here are:
    • Design API from beginning to be extended in future. Be especially carefully with boolean arguments (enum is a good replacement). In C using opaque structures is a good way to provide extensibility in the future, avoid reserved members/arguments, because you can end-up with something like this
    • If future features are known or very apparent, prepare API for them in advance (forward compatibility)
    • Avoid breaking backward compatibility of API, but don't add workarounds to API itself, because later you'll have to be backward compatible with those too
    • Prefer big breaks to often ones: no one likes API breaks, but often breaks seems to be hated more; when you break API, use the chance to fix all known shortcomings
    • Be clear about your API stability, so that users know, when to expect breakages; major releases are good candidates for breaks, subreleases should be backward compatible
    • It is also good to have alpha/beta testing, where new APIs are introduced for users to try, but are not yet stable and might change in final release, user feedback is best way to determine shortcomings
    • Be realistic, if your thing will survive long enough, you will, eventually, have to break it's API
  21. Convention over configuration is dangerous
  22. The point is to save developers time. In practice, not in theory! Just because developer writes less code/configuration does not mean he actually spends less time on it.
    When things work auto-magically, they sometimes don't work in the same magic way and finding out why can be very time consuming.
    Besides, changing convention is very painful, as it's less apparent (everything compiles as it did, but it doesn't work as it did, happy debugging).
Bottom line

Great APIs are not designed behind closed door by few system architects. Constructive communication and involvement from many parties from the beginning is the way to understand the problem.

Komentarų nėra:

Rašyti komentarą