API design tips

API Design Tips

"It's very easy to create a bad API and rather difficult to create a good one."
- Michi Henning,  API Design Matters, Communications of the ACM Vol. 52 No. 5

First think of the consumer of the API. Think about his likely situation and the problems he's facing. He doesn't want to learn a new abstraction and he doesn't like the fact that your API is going to make him change the design of his program. Design for the benefit of the consumer, not your ease of implementation.

  • Naming is everything
    1. Put your API in a namespace that reflects its function, not your org chart or company brand1
    2. Chose names that describe what each function does and what each type contains or abstracts. Avoid "ProcessData()" disease

  • The point is to reduce complexity
    APIs don't merely implement algorithms that the consumer isn't expected to understand, they also present abstractions that reduce complexity. An FTP library doesn't give the consumer a power they didn't already have, it relieves the consumer of doing the socket programming themselves. If you're wrapping a lower-level API then please don't introduce new problems. Aim for designs that reduce complexity without contributing to it in other ways

  • Eschew unnecessary side effects
    1. Avoid overwriting the arguments
    2. If you're given a DB or socket connection, assume it's open and never close it yourself
    3. Do not open your own connections if they're not the point of the API's purpose

  • Use Inversion of Control (IoC) to handle necessary side effects
    Let the consumer give you instances of resource managers or event handlers to open auxiliary connections, write to log files, fetch lookup values, interact with the user and so-on

  • Use events for the bulk of your IoC
    Don't force the consumer to create instances of a dozen different resource managers like logging classes, connection pools, name resolution classes etc. before they can call the first function. Raise events so that the consumer can handle only what they need in their circumstance

  • Use Interfaces everywhere
    When events aren't sufficient for your IoC component, or your arguments require a strongly-typed value, then use Interface types--even if you expose concrete implementations as well, and even if you're certain that your concrete implementation is the only kind that will do. Assume your consumer knows what he's doing

  • Use Interfaces that cover everything you need, but not more and not less
    Do you need IList, or just IEnumerable? Or do you actually need IBindingList and are trying to be too modest? If you specify too much you could force the consumer to work harder than necessary, and if you specify too little you risk being passed an insufficient implementation, or make it harder for the consumer to extend your API with a compatible wrapper

  • Don't make your own gravity
    If your target platform has its own crypto primitives, graphics library, transactional memory, XML parser, or whatever--and the point of your API isn't to replace them--then don't reinvent and bind your API to your own implementations, even if you think they're better

  • return all of your results from the function call
    Avoid returning data by setting global variables or writing to files, tables, sockets, etc. Let the consumer decide where the data goes and use compound data structures if it's necessary to return a success code along with the results

  • The consumer isn't Sherlock Holmes
    If an operation didn't work, or had an unavoidable (and un-IoC-able) side effect, then don't force the consumer to discover this by groping around referenced data structures or other environmental features. Return something other than null or void. Make sure that success is also clearly defined--either return an explicit success code or make sure that the absence of any error can be reasonably construed to mean success

  • Distinguish types of failures
    Did a connection fail because of a timeout, or because the remote host refused the connection, or because the address was invalid, or the login credentials invalid, or because a bird pooped on the switch, or what? Consider exposing and returning an Enum with the types of failures your API can detect

  • Support optional parameters
    Whether by a built-in language feature or via overloads, chose sensible default values or behaviors (timeout values, for example) and give the consumer the option of providing the least you need

  • true and false are lousy arguments
    If you spot the line Filter(widgets, true) somewhere in your code, tell me quick, what does true refer to? Does it mean be case insensitive, or to ignore errors, or to strip the results of blank lines? It's better to create Enums or other data structures that let the consumer's code be expressive and clear, so next time you see it, the line reads Filter(widgets, FilterOptions.IgnoreCase) and everything is clear

  • Use consistent calling conventions
    If you ask for a filename as the first argument in one of your functions, then stick to that as a convention for any other function that needs a filename. If you arrange argument order randomly you'll force the consumer to memorize the signature for all of your functions, instead of being able to intuit them

  • Nouns should be value types, verbs should be static functions. Or: Don't Make "Manager" Classes
    Avoid creating black-boxes that the consumer has to poke and prod to life

  • Fail early
    Check what the consumer is passing to you and aim to fail before you begin using and modifying resources, especially if those resources don't support rollbacks

  • Don't be the weak link in the chain
    If you use cryptography or handle sensitive data, then have your product audited by a security professional. Use well tested, standardized crypto primitives that have been implemented by security professionals. Be careful to avoid buffer overflow vulnerabulities. Perform strict input validation. Do not invent your own ciphers, and do not implement crypto primitives on your own (unless that's the point), because even the implementation of trusted ciphers can be easily screwed up. Your consumer doesn't want an invisible hole ripped open in their software after they did everything right on their end

  • One API at a time
    Identify a single concern for your API and stick to its scope. This concern could cover a broad category (like graphics, compression, networking), and if it does be sure to break it up into focused namespaces. You probably shouldn't bundle your compression library in the same binary and namespace as your FTP library

  • One level of abstraction at a time
    You may need to build more than one layer of abstraction to get up to the interface you'd like to present to the consumer. Networking now involves stacks 7 levels tall, for example. But keep each level separate, in the sense that each level could be re-used with a different "top" or a different "bottom" each coded to the right Interface

  • Expose wrappers for value types, but accept strings anyway
    Some consumers will appreciate a wrapper that strongly types your WidgetSerialNumber, but provide overloads that will also take a string and perform implicit conversion, too. Also favor data types already available on the platform, such as TimeSpans for timeout values instead of integers

  • Always return the richest type
    Following from above: even if the consumer passed an implicitly convertible string, always return the richest type that befits your output. If it's a date or time, return a DateTime value. If it's a number, return an Int, or a Decimal, or your own wrapper type, or whatever fits precisely. Let the consumer convert it back to a string if that's what they want

  • Implement IClonable, INotifyPropertyChanged, ISerializable and other handy interfaces for your value types
    Give your types more utility for the consumer so they don't have to marshal values back and forth, just because they want a nice reactive UI or an easy way to save and restore data to disk

  • Provide flexibility and isolation for configuration
    Libraries that link to the application's binary could be configured many ways: in code, as blocks in the application's own config file, or in a separate config file. The consumer will appreciate it if you offered at least the first two. When providing the option to configure the library through a shared config file, make sure to use namespaces or similar mechanism--such as .Net's ConfigurationSection class--to keep yours separate from the rest

  • Keep your knickers to yourself
    Consumers don't need their Intellisense or other inspection tools cluttered up with all your helper classes and utility methods. Makepublic only what the consumer needs to use your API, and mark everything else as private or internal

  • Only throw exceptions for the exceptional
    Failures in I/O operations are expected for an API that deals with I/O--an attempt to read or write can fail for a variety of reasons. You should use return values to indicate failure here and not exceptions, since throwing exceptions will add unnecessary overhead for what could be a completely normal outcome from the consumer's point of view (they may be writing a dead-link checker, and be expecting lots of FileNotFound conditions). Only throw exceptions if the fault occurs in supporting code (like when your graphics library tries to load an image from disk), or something is wrong with the consumer's code (invalid arguments, unopened connections, out-of-bounds conditions, etc.)

  • Document the exceptions you throw
    Give the consumer the chance to factor exception handling into the design of their program before it's too late to change it

  • Document how to do what your consumers tend to do
    Release 1.0 of your documentation might not contain tutorials and How-Tos for everything your consumers use your API for, but listen to feedback and have a plan to improve the How-Tos chapter of your manual for subsequent releases

  • Flag deviations from the norm
    If your API is a wrapper for a low-level service, and you change the behavior (eg: setting a timeout to zero no longer means wait indefinitely), then document this clear and in bold for those consumers who already know or reference the documentation for the underlying service

  • Consider shipping your unit tests
    They will serve both as a guide to the use of your API, and a framework to reproduce and report bugs that your consumers find in the field

  • Don't ship an API you haven't used in a useful, working program
    For everything that can't be covered here, you will learn whether your API is actually practical and usable by employing it in a program that actually does something useful--not just a unit test or a proof-of-concept, but something that has a UI or interfaces with a real system. That program doesn't necessarily have to be a product that you sell and support, it can be an internal utility, too. Bonus points if the programmer doesn't even work on the API team and therefore provides a fresh perspective. 

    If you develop the API at the same time you develop the program, you will also have the chance to see and correct mistakes that add unnecessary complexity to the consumer's program, conflicts with the naming scheme of an existing API, or exposes an interface that's unintuitive and confusing. That may be better than all of the above tips combined.
1 - It's okay to use a product name as the root of the namespace. MegaWidgets.Cryptography.DecoderRing.DRCryptoServiceProvider is acceptable, for example.

Other words of wisdom

  Michi Henning's essay, referenced at the top, was the inspiration for this collection of tips. He himself was inspired to write it after experiencing the difficulties of using the socket Select() function in the .Net framework, and he passes on some excellent admonitions that bear repeating, or to whet one's appetite for more:
  • General-purpose APIs should be "policy-free;" special-purpose APIs should be "policy-rich."

  • APIs should be designed from the perspective of the caller

  • Good APIs don't pass the buck

  • A big problem with API documentation is that it is usually written after the API is implemented, and often written by the implementor
And since publishing, someone on Hacker News pointed out this article written by Trolltech, who designed the Qt GUI framework, and someone on Reddit pointed out the book Framework Design Guidelines, written by members of the .Net team at Microsoft.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值