Questions to consider when designing APIs: Part I
A big part of an agile programmer’s job is API design. In an agile project, the architecture is defined from on high only in broad strokes, leaving the fine details of component design up to the implementer. Even in projects that are specified in much more detail, implementers will still find themselves in situations where they have to design something.
This means that programmers in an agile team have to be capable of weighing the pros and cons of various approaches in order to avoid causing performance, scalability, maintenance or other problems as the API is used and evolves.
When designing an API, we consider some of the following aspects. This is not meant to be a comprehensive list, but should get you thinking about how to think about the code you’re about to write.
- Will this code be re-used inside the project?
- How about outside of the project?
- If the code might be used elsewhere, where does that need lie on the time axis?
- Do other projects already exist that could use this code?
- Are there already other implementations that could be used?
- If there are implementations, then are they insufficient?
- Or perhaps not sufficiently encapsulated for reuse as written?
- How likely is it that there will be other projects that need to do the same thing?
- If another use is likely, when would the other project or projects need your API?
- Where should the API live in the code?
- Is your API local to this class?
- Is it private?
- Are you making it public in an extension method?
- Or internal?
- Which namespace should it belong to?
- Which assembly?
- What about testability?
- How can the functionality be tested?
Even if you don’t have time to write tests right now, you should still build your code so that it can be tested. It’s possible that you won’t be writing the tests. Instead, you should prepare the code so that others can use it.
It’s also possible that a future you will be writing the tests and will hate you for having made it so hard to automate testing.
- Is multi-threading a consideration?
- Does the API manage state?
- What kind of dependencies does the API have?
- Which dependencies does it really need?
- Is the API perhaps composed of several aspects?
- With a core aspect that is extended by others?
- Can core functionality be extracted to avoid making an API that is too specific?
- How do callers use the API?
- What are the expected values?
- Are these expectations enforced?
- What is the error mechanism?
- What guarantees does the API make?
- Is the behavior of the API enforced?
- Is it at least documented?
- Are known drawbacks documented?
This is a very important one and involves how your application handles situations outside of the design.
- If you handle externally provided data, then you have to handle extant cases
- Are you going to log errors?
- In which format?
- Is there a standard logging mechanism?
- How are you going to handle and fix persistent errors?
- Are you even going to handle weird cases?
- Or are you going to fail early and fail often?
- For which errors should your code even responsible?
- How does your chosen philosophy (and you should be enforcing contracts) fit with the other code in the project?
Fail fast; enforce contracts
While we’re on the subject of error-handling, I want to emphasize that this is one of the most important parts of API design, regardless of which language or environment you use.
You haven’t tested the code, so you don’t know what kind of errors you’re going to get. If you ignore everything, then you’ll also ignore assertions, contract violations, null-reference exceptions and so on. The code will never be improved if it never makes a noise. It will just stay silently crappy until someone notices a subtle logical error somewhere and must painstakingly track it down to your untested code.
You might say that production code shouldn’t throw exceptions. This is true, but we’re explicitly not talking about production code here. We’re talking about code that has few to no tests and is acknowledged to be incomplete. If you move code like this into production, then it’s better to crash than to silently corrupt data or impinge the user experience.
A crash will get attention and the code may even be fixed or improved. If you write code that will crash on all but the “happy path” and it never crashes? That’s great. Do not program preemptively defensively in fresh code. If you have established code that interfaces with other (possibly external) components and you sometimes get errors that you can’t work around in any other way, then it’s OK to catch and log those exceptions rather than propagating them. At least you tried.
In the next article, we’ll take a look at how all of these questions and considerations can at all be reconciled with YAGNI. Spoiler alert: we think that they can.