3

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?

2 Answers 2

4

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.

2

As a complement to what has already been said, I would use an approach by function (not in the sense of functional programming, but "what is the purpose of what I am writing?", "What is it supposed to do"?)

In other words, I wouldn't have only a bottom-up approach (inductive), trying to start from what the three APIs have in common. Well, I guess you would first have to familiarize yourself with the lower implementations, to get familiarized with what is possible, difficult, or impossible.

But since you want to create an abstraction (abstract means "thought of apart from concrete realities, specific objects, or actual instances"), you are trying to put a thought there that wasn't there in the first place. So you really have to devote some thinking to your entity in itself, apart from the actual instances. As a rule of thumb, "the source of the abstraction is to be found in you, not in the instances".

More specifically: you want to create a class for Printer (or something of that sort).

  1. Ideally, how would you like that Printer to behave, to match your needs exactly? How would you describe it? Work out your use cases, what methods and properties it should export.

  2. Then only, work out how you would implement your object using each of the ERP implementations you have at hand (typically by subclassing, or by switching). Some might be easier, others might be more difficult.

  3. When you feel there is something unsolvable, or you see an opportunity to improve your common model, work back up.

  4. Work alternatively up and down until you are finished.

If you have you own view of what the ideal Printer should be (according to your specific needs), you will not be biased by one implementation or another. You will not be swayed into implementing things you don't need ("just in case"). Your code will be short and to the point. And if the process is done well, you might end up providing a nice and clean API that is 10 times easier to use than the original ones -- at least for your own use case.

And apart of saving a developer the time needed to provide for each platform, that would be a second merit.

Conversely, if you simply implement a "highest common denominator", or a "common implementation with holes", the result might only be another layer, adding more complexity (and documentation requirements) instead of removing it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.