Thursday, 19 March 2015

Why API design matters

Disclaimer: This post is not all my own work.

I recently had to take a look at API exposure in user-created Powershell modules and in a new internal programming language that uses XML. I found that a lot of my conventional knowledge around what makes programming easy and bugfree was not being followed. Why is not the concern, but how was. So I have copied and pasted and referenced and linked and pasted more and re-organized and added some of my own stories to other peoples work below.

So why does API design matter? [R1] 

As someone who has to consume APIs from lots of places, the APIs I’ve been most productive with, follow a few rules. Here are some I’ve picked up over about 20 years.

Please write your API in a consistent manner:

  1. Consistent naming within the API itself. Use verbs, nouns, keywords in EXACTLY the same style or way all of the time. 
  2. Consistent with the target environment it will be used in. Take the programming language into consideration, python, Powershell and so on. If it’s on .NET, then consult Microsoft's naming guidelines.
  3. Consistent concepts. Factory pattern? Builder pattern? Static methods? Interfaces? Just pick one, and stick with it. REALLY. There is no such thing as a small exception to the rule. It will stick out as a big sore thumb. More than 1 exception? Your API is more and more amateur.
  4. Make it easy to figure out how your API will work. I use APIs from lots of places try not to assume I remembered that your personal preference for naming widgets changed at some point from always having an ‘s’ on the end, to dropping the ‘s’ later on when the last few functions got added. This goes for named arguments too naturally.
  5. Save yourself on documentation and the iterative code->debug->refactor and repeat steps. There are so many great pointers on the actual API development steps in this slideshow, that I'm going to just link to it instead [R3]

Here's another one: Specificity.

  1. Return types. Functions often return true false or an integer that indicates a code, or they return data. I have a pet-hate for the function that falls into the second category, but lets move to the last category.
    For example. When a function will return some data, don’t just say it returns Objects, and then leave me to guess what form those objects might take. Example: can I expect a, string, array or an object that has specific properties. I cannot know if your API is useful to me if you don’t tell me what properties it is going to return to me until I actually call it. Then going further, nothing guarantees that depending on the inputs the output may have varying sets of properties. Say for example I use your Get-widget(‘patrick’) to return a record about Patrick the starfish

    But how useful would it be to know that for example if I said Get-widget(‘spongebob’) would I expect this for example to get returned?

    Now "I" expected that to come back, because Spongebob is a sponge, but you didn’t expect that. So what a function returns is really really important.
  2. Naming
    Please don't get silly-long with names: StartProcesswithRedirection. You can often refactor super-specific things into a more generic name + parameters.
  3. String parameters for constants are often evil! In Powershell its often convenient, but when it is possible use enumerations.  I'd much rather like an enum I can investigate: or a Get-RedirectionEnums() ‘none’,‘stdout’,’stderr’,’both’. Now documenting this is all fine and well, but if in a future API you decide to deprecate ‘both’, and require people to use something else, all they have to do is call the RedirectionEnums() command, they don’t have to read the docs, and you don’t have to update your documentation in very single damn place where you copy/pasted the above values. Smart?

Be complete and verifiable

  1. Verify: There’s nothing worse than pulling a trigger and then thinking, hey! Did that work right or not? I believe that for meaningful software to be written using any language (or API) every operation to modify the system needs to have a similar function to restore the system state or at very least to probe the state. This means New must always have Delete or Dispose. Add must have Remove, and so on and so on. To not have this causes API orthogonality issues and blocks many use-cases. What I’m talking about is not new, See chapter 4, The Art of Linux Programming 2004 [R2] .
  2. Orthogonal: In general to myself it makes more sense to design for and create orthogonal APIs so that not only can all future workflows be done with your API, but also the API be fully testable and stable. That way I never have anyone coming back and saying things like:
“Hey John, I need a feature to be able to move a widget from one folder to another. Can your API do that?” My response must always be “Yes Spongebob!”, because it's only 3 lines of code:
1: create blank object in the new location
2: copy the old object into the new blank location
3: delete the old object
I don’t have to change my API, and the user does not have to write more than 3 lines of code.
 3. Symmetry and New/Delete: It also makes sense to me to always implement an undo for every action to allow for cases where an operation fails for any reason. Depending on the API or service, and the point that it fails, it’s otherwise impossible to know if the operation is half-complete, or got rolled back or just never modified the system at all before failing. Pairing up getters with every setter and New() with Delete()/Dispose() makes this question go away. It makes automation and remote control under failure scenarios really easy. If an API does not support rollback/undo operations, you want to often be clear why an undo function is not necessary. For example there is no function to uninstall Windows 7, why? Well because not only is it not functionally useful, and format.exe would have the same effect, but the workflow and user experience is that you only ever want to uninstall in extreme corner cases. Microsoft want you to pay the license money and stay. If your API does not need undo(), either explain why it does not, or we make it clear in the API design. Symmetry is everywhere.
Works in progress:
In cases where implementing all api methods to complete a pattern, is not realistic due to time constraints un-implemented function stubs can be used to allow static analysis of the API surface. This process of stubbing in time-bound or agile development is typical TDD process. Not closing or making an API encapsulate it’s purpose fully, conversely leads to accidental functionality leakage into other pieces of software which either introduces duplication or interface sanity bugs. 
5. Easy to learn: 
Compactness or completeness of anything programming or real-world,  as with APIs is about the sweet spot that allows you to not have to worry about external systems that you know nothing about that lie beyond the API or interface that controls them. For example you don’t have to know how an AM/FM radio works, to get sound on either an AM or an FM frequency. Good APIs meant you only need to worry about inputs that have meaning to you the user of that API. If a specific implementation of the radio class happens to have an API that tells you the signal strength or some other technical details, well that’s totally optional and does not interfere with using a radio in general at any point.

My Creds

So you may ask why Conrad are you qualified to sprout all of this? Well, that's because I once wrote all of these drivers against an interface that never ever broke once over a period of 12 years:
Asea Brown Boveri Procontrol 214,Allen Bradley SLC500,Allen Bradley Ethernet TCP/IP,Allen Bradley DH+,Centralised Fire Panel 2000,Conet Exception-based,ConetPCI Exception-based Driver,,D Le Roux & Associates MUL-T-LINK 8.1,,Eagle PC Card DRiver,Eagle EDR-Enhanced (EDRE) PCI Card,Electromatic Optolink V24 Dupline 128,GSM SMS Driver,GSTProfi Exception-based,GST Profibus SoftFEP,Hitachi Ethernet,Hitachi H,JOYSCC Exception-based,Koyo DirectLogic-105 Range K-Seq.,Koyo DirectLogic-205 Range K-Seq.,
Klockner Moeller SUCOM-A,Koyo DirectLogic Ethernet Protocol Driver,Serial,Mitsubishi AJ71UC24,Mitsubishi AJ71E71 TCP (not UDP),Mitsubishi FX TCP (not UDP),Mitsubishi FXS,Mitsubishi Q/QnA Ethernet,MIT_M2M,Modbus I (RTU and ASCII),Modbus Plus (SA85),Modbus Radio,Modbus Ethernet,Moore ICI 320,Omron SYSMAC,,Opto 22 (OPTOMUX),Pager Exception-based (Discontinued!),PolyComp Sign
(GTX),PolyComp Sign Ethernet,RDC8102 Exception-based,Sascom (RTU and
 ASCII),RS-232 Barcode Scanners etc. (SCANRS),Schiele SYSTRON S 800 3964 and 3964R,Schiele Custom Protocol #1,Siemens S7 Funtions,SiStar PCU Server Driver (Intel),SpaBus,Spectrum Tele-RANGER,Spectrum SCADA-MUX,Square D Sy//Ma
x Serial,Strike ENERMAX,Strike ENERMAXP Serial and Ethernet driver,TDC/I/0550 Serial,Texas Instruments 500/505 Series,Texas Instuments TCP/
IP Ethernet,TOSHBIN (RTU and ASCII),Toshiba (RTU and ASCII),Toshiba T2 Ethernet,Yokogawa uXL DCS.
And those are just the ones I wrote, customers added quite a few, and the full list is double this size.
Basically I helped publish the API and we sold the spec as a public API along with some demo code. What's remarkable for me is that the API started as 14 functions, it expanded to over 21 before being completely re-written later. Moreover it was of the Service Provider Interface (SPI), where API breaks are close on 97% fatal, and so I suppose, that's where and why I learned what you can get from Google in 5 minutes now.


Why does API design matter
The Art of Linux Programming
Why good api design matters : slides :Joshua Bloch
Why good api design matters : video :Joshua Bloch
Image "How the analyst designed it" CC attribution

No comments:

Post a Comment