API Design – Creating Abstraction Over API

abstractionapidesigndomain-specific-languages

I'm trying to figure out how to create abstraction over different APIs, which have common things. For example let's take the mobile platforms you have Android, Windows Phone and IOS. Let's say I want to create and API or program or domain-specific language, which will allow me to write one program and work on all of the given platforms. Of course this is just an example, and in the end all of the platforms have a lot in common which allow me to write programs and libraries without thinking too much about the abstraction, for example all of them have C compiler. A better example is ERP systems, let's say I want to write the same plugin for 3 different ERP systems, how could I create abstraction above their APIs which will allow me to write it once, and compile it(build it or whatever you can call it), and use it on all the systems. For the sake of simplicity let's say that those ERP system are all writing in the same programming language, but their API have nothing or little in common in terms of function names etc.. they only have similar functionality. I've though about using metaprogramming, in such way that I tell the program what I want, and using set of rules different for every API will output programs for every one of the API. Another think what I've thought of is encapsulate all of the common features in classes, which will hold the code(data) for every API, and output it when I request the functionality for given API. I'll give an example. Let's say I've 3 api which all have class for printing, and the names are like follows:

  1. Printer – PrintToLog(Data)
  2. Outputer – OutputToLog(Data, lenght)
  3. IO – PrntLogData(Data, logPtr)

For the sake of simplicity let all those classes be static.
I could create class like this(pseudo-code)

Class Printer{
    api //holds for which API the code is requested
    constructor(API){
        this.api = API
    }
    func logData(Data){
        switch API{
            case API_1 //let's say the API are stored in ENUM or something similar
                return "PrintToLog(" + Data + ")"
            case API_2
                return "OutputToLog(" + Data + "," + len(Data) + ")" // len returns the lenght of data
            case API_3
                return "PrntLogData(" + Data + ", /default/log/location)"
        }
    }
}

However I'm not sure how practical,reliable are those methods. Also I wasn't able to find a lot of information for those types for creating abstraction. Are there any sets of patterns, methodologies or principles for such abstractions?

Best Answer

This is a common problem in software development. There are two techniques which can be used:

  • You may design the interface in a way it contains only common methods which are shared by every underlying implementation.

    The benefit is that moving from one implementation to another would be extremely easy, without any need to change any code. For instance, a common interface may make it possible to interact with Google Maps or Bing Maps while containing methods which are supported by both Google's and Microsoft's APIs. Since both APIs are very similar, it is relatively easy to create a common interface which would still remain very capable.

  • You may provide methods which are supported only in some implementations, but not others, or not well. This will require the caller to be able to determine whether a given action is supported (without necessarily knowing what is the implementation in use).

    The benefit is that you can provide much more features than the common subset, and also use the strong points of some implementations. For instance, some features are performed much better by iOS, and others are performed better by Android. If you barely take common features, you won't benefit from those features.

Implementation

I don't like much your switch statement. A common solution is to use inheritance: in your case, it means to have one adapter per implementation which will ensure the communication between your API and the underlying system or API.

Using Dependency Injection, you'll then be able to select the adapter to use at runtime. Each adapter would be able to be modified independently from others.

Example:

interface IGeolocation
{
    Coordinates addressToCoords(string address);
    string coordsToAddress(Coordinates position);
}

class GoogleMapsGeolocationAdapter : IGeolocation
{
    ... // Concrete implementation of addressToCoords and coordsToAddress.
        // Each method calls the Google's API.
}

class BingMapsGeolocationAdapter : IGeolocation
{
    ... // Concrete implementation of addressToCoords and coordsToAddress.
        // Each method calls the Microsoft's API.
}

The class which needs to get latitude and longitude from an address simply needs to require IGeolocation to be passed to its constructor, and then call addressToCoords, independently of the concrete implementation which was passed through Dependency Injection. It belongs to the application itself to determine, based on configuration, which class should be initialized: GoogleMapsGeolocationAdapter or BingMapsGeolocationAdapter.

In the case of iOS vs. Android, the implementation choice would be made either based on the platform, or during the compilation of the application.

Related Topic