Aphorisms of
API Design

Presented by Larry Garfield (@Crell)

@Crell

Larry implements Huggable
  • Director of Runtimes and Integrations, Platform.sh
  • Drupal 8 Web Services Lead
  • Drupal Representative, PHP-FIG
  • implements Huggable

Step 1:

Define your terms

API

Application Programming Interface

Code intended for other code

Aphorism

A concise statement containing a subjective truth or observation cleverly and pithily written.

Wikipedia

They're not rules

They're more like guidelines...

They're more like guidelines

Our Agenda

Clever and pithy sayings about good code.

Learn the rules like a pro
so you can break them like an artist.

— Pablo Picasso

#1

Asimov's Law

If there are multiple universes,
then there are infinite universes

The Gods Themselves

If there are multiple possible implementations,
there are infinite possible implementations

So what?

Fixed-number assumptions


function engage_engine($engine_type) {
    switch ($engine_type) {
        case 'warp':
            // ...
            break;
        case 'hyperspace':
            // ...
            break;
        default:
            throw new UnsupportedSciFiException();
    }
}
        

function engage_engine($engine_type) {
    switch ($engine_type) {
        case 'warp':
            // ...
            break;
        case 'hyperspace':
            // ...
            break;
        case 'slipstream':
            // ...
            break;
        default:
            throw new UnsupportedSciFiException();
    }
}
        

function engage_engine(Engine $engine) {
    // ...
    $engine->engage();
}
        

Beware constants


const ENGINE_WARP = 1;
const ENGINE_HYPERSPACE = 2;
        

const ENGINE_SLIPSTREAM = 3;
        

const ENGINE_INFINITE_IMPROBABILITY = 3;
        

But aren't booleans 2-state?

Boolean != either/or

Booleans don't have 2 values

Booleans are TRUE or not

Access is a boolean

Access control has N-possibilities

Garfield's Law

One is a special case of many.

http://groups.drupal.org/node/8001

node_load_multiple() Druplicon

Drupal 6


// Fetch one node.
function node_load($nid) {
  $query = "SELECT * FROM node WHERE nid=:nid";
  $record = db_query($query, [':nid' => $nid])->fetchObject();

  $record = load_extra_stuff($record);
  return $record;
}
        

// Fetch multiple nodes.
function node_load_multiple(array $nids) {
  foreach ($nids as $nid) {
    $nodes[] = node_load($nid);
  }

  return $nodes;
}
        

SELECT N + 1

(aka, melt your database)

Drupal 7


// Fetch one node.
function node_load($nid) {
  $nodes = node_load_multiple([$nid]);
  return reset($nodes);
}

// Fetch multiple nodes.
function node_load_multiple(array $nids) {
  $query = "SELECT * from node WHERE nid IN (:nids)";
  $records = db_query($query, [':nids' => $nids])->fetchAll();

  $records = load_extra_stuff($records);
  return $records;
}
        

Aphorism #1

N is the only number

#2

Fail Fast, Fail Cheap, Be Lazy.

—Rasmus Lerdorf

Make the code debug for you

Don't plan for everything that could happen

Plan for how it will break

Drupal 6 theme system


function theme($hook) {
  // ...
  if (!isset($hooks[$hook])) {
    return;
  }
  // ...
  return $output;
}
        

Drupal 7 theme system


function theme($hook, $variables = array()) {
  // ...
  if (!isset($hooks[$hook])) {
    if (!isset($candidate)) {
      log('Theme key "{$hook}" not found.');
    }
    return '';
  }
  // ...
  return $output;
}
        

General failure reading Drive C:

PHP Warning: Invalid argument supplied for foreach() in /includes/form.inc on line 2975

Code failure is like voting in Chicago

Fail early, fail often

Constrain inputs

Fail usefully

Type check all the things!

Good APIs are picky


class InsertQuery extends Query {
  public function fields(array $fields, array $values = []) {
    // ...
  }
  public function preExecute() {
    if (array_intersect($iFields, $defaultFields)) {
      throw new FieldsOverlapException('...');
    }
    // ...
    return TRUE;
  }
}
        
A good programmer is someone who always looks both ways before crossing a one-way street.

—Doug Linder

Are you not using...

PHP
E_ALL|E_STRICT
declare(strict_types=1);
Javascript
"use strict"
Python
python -W all file.py

You're Doing It Wrong!

Aphorism #2

Fail fast, fail cheap, fail usefully

#3

You can't teach what you don't know.

You don't know what you can't teach.

You don't understand what you can't document.

I can't understand what you don't document.

Drupal 7 (Module upload system)


abstract class FileTransferFTP extends FileTransfer {
   /**
    * Return an object which can implement the FTP protocol.
    *
    * @param string $jail
    * @param array $settings
    *
    * @return FileTransferFTP
    *    The appropriate FileTransferFTP subclass based on available    
    *    options. If the FTP PHP extension is available, use it.
    */
   static function factory($jail, $settings) { }
}
        

Date.module (Drupal 6)


/**
 * Getter callback to return date values as datestamp in UTC from the field.
 */
function date_entity_metadata_field_getter($object, array $options,
                                           $name, $obj_type, &$context) { }
        

No, seriously, WTF?

Lack of comments indicate

  • Laziness
  • Indifference
  • Lack of comprehension
  • Embarrassment

What to document

  • Every function
  • Every method
  • Every class
  • Every object property
  • Every constant
  • Every parameter
  • Every return value

No exceptions (Well, document those, too)

Type check all the things!

Usage docs

A picture is worth 1000 words

A code sample is worth 1000 comments

Gearman's documentation provides both

You wish you were as cool as Gearman's docs

<aside>

Clearly written code with well-named methods is self-documenting. Um, no.

protected function normalizeCharset($value) {
  switch (mb_detect_encoding($value)) {
    case 'ASCII':
      break;
    case 'UTF-8':
      $value = htmlentities($value, ENT_NOQUOTES);
      $value = html_entity_decode($value, ENT_NOQUOTES, 'UTF-8');
      break;
    case FALSE:
      $value = iconv('Windows-1252', 'UTF-8//TRANSLIT', $value);
      $this->log->warn("Detected Windows-1252 encoding.");
      break;
    default:
      $this->log->warn("Unrecognized character encoding.");
  }
  return $value;
}
        

WTF is up with UTF-8? And why is FALSE Windows-1252?


protected function normalizeCharset($value) {
  // mb_detect_encoding() in most situations supports only two character sets,
  // ASCII and UTF-8.  However, it is sadly not unusual for incoming data
  // to be in Windows-1252, aka MS Word. We therefore guess that a
  // false/not-found character set is Windows-1252, and try to convert that
  // to UTF-8.
  // Note: The odds of this breaking on some other character encoding besides
  // those three is rather high.

  // ...
}
        

switch (mb_detect_encoding($value)) {
  // I have absolutely no idea why UTF-8 strings need to be converted
  // from UTF-8 to UTF-8, but if this code is removed many strings end up
  // failing with invalid multi-byte encoding.
  case 'UTF-8':
    // Convert any characters we possibly can to their HTML encoded entities.
    // If we don't specify a character encoding then this should do at least
    // a passingly decent job of detecting it, or at least doesn't care as much
    // as other APIs do.
    $value = htmlentities($value, ENT_NOQUOTES);
    // Convert those HTML entities back into real characters, but this time
    // insist on UTF-8.  This will at worst convert UTF-8 characters back
    // to UTF-8 and at best convert ISO-8859-1 characters to HTML entities and
    // from HTML entities to UTF-8 characters.
    $value = html_entity_decode($value, ENT_NOQUOTES, 'UTF-8');
    break;
        

  // ...

  // A False return from mb_detect_encoding() means that it couldn't
  // figure out what the encoding is.  In a standard configuration mb_* only
  // knows about ASCCI and UTF-8, so that's not especially useful. We will
  // make an assumption that anything else is Windows-1252, aka MS Word legacy
  // garbage. If correct, this will convert $value from Windows-1252 to
  // UTF-8, transliterating where necessary rather than just failing.
  case FALSE:
    $value = iconv('Windows-1252', 'UTF-8//TRANSLIT', $value);
    $this->log->warn("Detected Windows-1252 character encoding.");
    break;
}
        

</aside>

Aphorism #3

Docs or it didn't happen

#4

A UI is not an API

A User Interface is not
an Application Programming Interface

A User is not a Program

A UI is a client of your API

What good is a website without a UI?

Who said you're building a web site?

Cilex, PHP command line micro-framework

PHPUnit PHPUnit

QUnit

PyTest

(Tests or it didn't happen)

The HAL Browser, a generic reference implementation
Drupal, the world's best Open Source CMS

A website is not an API

A website uses an API

An API does not need a web site

You're not done until you have
three implementations

3 implementations?

  1. Unit test
  2. Web services call
  3. Command line
  4. Web site

Aphorism #4

A UI is not an API

#5

You know that saying about standing on the shoulders of giants?
Drupal is standing on a huge pile of midgets.

— Jeff Eaton

Don't reinvent the wheel. We have enough already.

Don't add to API bloat

Obligatory XKCD Wisdom

There are too many APIs in the world

Leverage existing patterns

  • Mimicry is the highest form of flattery
  • Mimicry is easier to remember
  • Mimicry takes less work

Existing patterns

  • Design patterns
  • Your platform's patterns
    • Drupal (Entities, hooks, etc.)
    • Symfony (Events, Bundles, YAML config files)
    • Language idioms (Go, Python, etc.)
    • Language coding style guides

Go with the flow... it makes docs easier

Aphorism #5

The best API is the API you didn't have to write

#6

Use uncertainty as a driver.

—Kevlin Henney, 97 Things Every Software Architect Should Know

Don't make decisions if you don't have to.

Make changing your mind cheap.



(Your client will change their mind.)

(Twice.)

You can only change what is encapsulated.

Logging

  • Database
  • Syslog
  • Display on screen
  • Pager / SMS message
  • Twitter

Don't decide for me!

Interfaces are your friend


interface LogInterface {
  public method log($message, $severity);
}
        

PSR-3


namespace Psr\Log;

interface LoggerInterface {
  public function log($level, $message, array $context = array());

  public function emergency($message, array $context = array());
  public function alert($message, array $context = array());
  public function critical($message, array $context = array());
  public function error($message, array $context = array());
  public function warning($message, array $context = array());
  public function notice($message, array $context = array());
  public function info($message, array $context = array());
  public function debug($message, array $context = array());
}
        

function engage_engine(Engine $engine) {
    // ...
    $engine->engage();
}
        

One is a special case of many

How many implementations will you have?

Encapsulation avoids decision making

Avoiding decision making requires
loose coupling

Explicit interfaces


namespace Psr\Log;

interface LoggerInterface {
  public function log($level, $message, array $context = array());

  public function emergency($message, array $context = array());
  public function alert($message, array $context = array());
  public function critical($message, array $context = array());
  public function error($message, array $context = array());
  public function warning($message, array $context = array());
  public function notice($message, array $context = array());
  public function info($message, array $context = array());
  public function debug($message, array $context = array());
}
        

YAGNI is the devil

All things in moderation

Dependency Injection

Not just for testing...

Also for day-before-launch-changes

Separate Logic from Data

Logic
Data

Separation of concerns

  • Interface-driven development
  • Context-free (stateless) services
  • Single-Responsibility Principle
  • Dependency injection

Aphorism #6

Avoid making decisions

#7

Delegation adds indirection

Indirection requires abstraction

Abstraction solves hides complexity

Abstraction is not free

Performance

  • Language level
    • call_user_func_array() = 3 functions
    • __call() = 3 methods
  • Query builders in Drupal
    • Query builder is 30% slower than raw queries
    • Even for simple query builder!

What the heck is going on under the hood?


... 14 method calls later I still don't know.

There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies.

— C. A. R. Hoare

The unavoidable price of reliability is simplicity.

— C. A. R. Hoare

Aphorism #7

There is no problem that cannot be solved by adding another layer of abstraction...

except abstraction

#8

Your software has idiosyncrasies,
but it's not special.

No one understands Drupalisms,
except Drupal developers

(And not even all of them)

No one understands Symfonyisms,
except Symfony developers

(And not even all of them)

No one understands PHPisms,
except PHP developers

(And not even all of them)

No one understands your system,
except you

(And sometimes not even then)

Karoly Negyesi and John Resig

No matter how cool you are,
John Resig knows more Javascript than you do.

Some numbers

  • Age of 3rd gen languages: ~60 years
  • Age of PHP: 22 years
  • Age of Drupal: 16 years
  • Age of your website: 6 months

Odds of you doing something original...?

Find existing wheels

Use existing platforms

Use existing standards

  • PHP-FIG Specifications
  • Learn your HTTP!
  • REST API: I can go home and rest and not design from scratch

Learn from existing wheels

  • PECL
  • jQuery
  • Wordpress(!)
  • Symfony, Zend Framework, etc.
  • Other languages!
  • Design patterns

Feature no software needs


Ego

Durden's Law

Tyler Durden, Philosopher

Aphorism #8

You are not a special and unique snowflake

Grand Unified Theory of API design

Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.

—Martin Golding

You will always know where you live.

In six months, some one will need to do something you never thought of with your code and will not be able to edit it.

That will be you.

Plan accordingly.

Larry Garfield

Director of Runtimes and Integrations Platform.sh

Continuous Deployment Cloud Hosting

Stalk us at @PlatformSH

Further reading