API Lockdown
 
Support Ukraine

API Lockdown

...or, how to inadvertently make your code useless.

1. Introduction

Non-trivial programs can't be built from scratch without much effort. In fact, the effort it would take to build most programs from machine-code and up would render the whole project economically unviable, and it would not be undertaken. In order to get around this, programmers re-use modules of code. Instead of having to figure out which voltages to send to the graphics processor to get a pixel set in order to draw a circle, we look at the Application Programming Interface, or API, and call screen.drawCircle (x, y, radius);.

These modules function as prefabricated blocks that can be assembled to form the bulk of the application, freeing us to focus our attention on the unique parts of our product - which is what people will use and pay for.

2. Access Control

In modern programming languages it is possible to specify access modifiers for various elements. This marks certain parts of the program as only being accessible by certain other parts. While this is rarely a security measure, it serves as an important marker to clients of the code. In one end of the spectrum we have elements that can be accessed by anyone, so-called public elements. This is the equivalent of writing "you can use this freely and rely on it being this way in future versions". In the other end there are private elements, the equivalent of writing "don't touch this - you're not even supposed to know it exists".

Properly used, these constructs allows the builder of a code library to present the client with a contract, specifying how the library may be used. The more that is marked as public, the more the library writer promises to the client, and the more that is marked private, the more is purposefully hidden from the client.

Usually libraries end up with a certain ratio of interface, public code or "surface area" to implementation, private code or "volume" - I won't chance on an actual number, but personally, and I'm just guessing here, I'd expect about a ratio of one to five or less for interface to implementation code.

The tradeoff is this: From a library writer's perspective, the less that is promised, the greater the flexibility in implementing the library. From the client's perspective, the less that is promised, the lesser the flexibility in using the library. Many times, this is also considered something good. With only one way to use the library, the client doesn't have to wonder which way is the right one. In this way, the library writer can (and perhaps also ought to) protect the client from themselves. Better to give hard and fast rules for usage that can't be misunderstood and that will work, than to leave it to the client to learn the limits by trial and terror.

3. The Problem

If the desire to provide a small interface is taken too far, the result is a library which easily turns into a "my way or the highway" proposition to the client. With little flexibility in how the interface is used, the client had better want to use it just the way the writer intended.

Several libraries exist that are essentially all API. These are, for example, the Amazon Web Services SDK and the[a] Google GData API[b]. Here, the library doesn't contain the primary functionality that you use them for. They only provide an interface to the actual implementation running on Amazon's or Google's servers.

If we consider implementations to be bricks and their interfaces to be the mortar between them, then these libraries are all mortar. With a library that is "all mortar", you'd expect it to be like mortar - it has to be very flexible to provide maximum contact surface between two bricks - the client program and the web service - that may be very irregularly shaped.

Yet they are written as if they not only fulfilled the role as mortar, but also the role as bricks. They are like mortar with bricks mixed in.

4. Antipatterns

The upside with both SDKs mentioned is that they are open source. This makes the problem of their inflexibility surmountable, but one still has to pay the price of maintaining a patch set for the library. Here are a number of anti-patterns that I have observed.

4.1. Make it Final

The easiest way to keep end-users from messing up your beautiful architecture with their own ideas is to declare classes final. It is rarely done, however, because it takes a deliberate effort and is therefore most often done by someone who knows what they're doing.

/**
 * Good luck customizing this one.
 */
public final class Service {
    ...
}

4.2. Keep it Private

While not as simple and "final" as final, putting something important into inaccessible private fields kills any customization attempt dead.

public class Service {

    private HttpClient httpClient;
    private Credentials credentials;

    public Service () {
    }
    
    /**
     * Override to provide your own HttpClient 
     * implementation. If you can provide one 
     * without access to the credentials field, 
     * that is.
     */
    protected HttpClient createHttpClient () {
        return new HttpClient (credentials);
    }
    
    public final void setCredentials (
        Credentials credentials) {
        
        this.credentials = credentials;
    }

    .
    .
    .
}

I usually resort to the java.lang.reflect package to get around these things. It's not pretty, but it gets the job done.

4.3. Restricted Useful Elements

This case doesn't risk making the SDK useless, which the above two cases might - it just makes the SDK much less useful. It is when a neccessary and very difficult part of the API is tucked away in a private or package private member:

public class Service {
        
    /**
     * Override if you want, but you'll have
     * to implement hideouslyComplexAlgorithm 
     * yourself.
     */
    public void aServiceMethod (Object[] params) {
        
        String callSignature = 
            hideouslyComplexAlgorithm (
                "aServiceMethod", params);
    }
    
    private String hideouslyComplexAlgorithm (
        Object[] params) {
        
        ...
    }
        
}

Of course, one can implement the hideously complex algorithm again, but doing so takes away from the usefulness of having a continuously debugged and updated version in the SDK.

4.4. Fixed Implementation

This pattern appears when a pluggable architecture is first designed and then utterly ignored. For example, suppose we were to design a pluggable messaging system interface:

/**
 * Interface for remote access over 
 * any medium.
 */
public interface Remote {
    public Reply call (Request request)
        throws Exception;
}

/**
 * Communication over a socket.
 */
public class SocketRemote 
    implements Remote {
    ...
}

And then make sure nobody can use any implementation except the one we choose:

public class Service {

    /** 
     * Remote to delegate to.
     */     
    protected final Remote remote;

    public Service () {
        // Everyone will use sockets.
        remote = new SocketRemote ();
    }
}