Design Patterns - The Adapter Pattern in JavaScript

TL; DR

Source code can be found on GitHub, jsAdapter repo


Design Patterns - The Adapter

A picture is worth a thousand words, this is for sure true once the Adapter design pattern needs to be explained.

Most of us has seen something like this:

travel plug for US/EU

This is a travel plug sometimes it is called plug adaptor. The plug adapter adapts (transforms) one electrical interface to another. In this case the EU standard to the US standard.

It is important to highlight without the adapter the two interfaces cannot communicate with each other. Another important aspect of using/creating adapters is, that adapters are always created for existing components, which need to interact.

There are only minor differences between the Adapter, Decorator, Facade and Bridge design patterns, we will cover these in a future blog post.

Lets assume there is a project, which has a logger library. This was written by the initial project team and is not the most developer friendly to use. Since this project specific logger library was developed, new logger libraries appeared on the market and the project should be able to use these.
One way to solve this is to write adapter(s) for the logger libraries and use those.

The current logger in our imagined app is this:

function BadLogger(name) {  
    this.name = name;
    var LOG_HEADER = '[' + name + ']:';
    var self = this;

    return {
        getName: function getName() {
            return self.name;
        },
        getType: function getType() {
            return 'BadLogger';
        },
        information: function information(message) {
            console.info(LOG_HEADER + message + '- INFORMATION' );
        },
        debg: function debg(message) {
            console.log(LOG_HEADER + message + '- DEBG');
        },
        w: function w(message) {
            console.warn(LOG_HEADER + message + '- W' );
        },
        err: function err(message) {
            console.error(LOG_HEADER + message+ '- ERR' );
        }
    }
}

module.exports = {  
    getLogger: BadLogger
}

I don't want to go into details why this is a bad logger (module named as BadLogger too), but it can be seen that the function names are not quite intuitive and don't really respect any naming convention.

There is another logger which we might want to use:

function ShortLogger(name) {  
    this.name = name;
    var LOG_HEADER = '[' + name + ']';
    var self = this;
    var getTime = function() {
        return '[' + new Date().toISOString() + ']';
    }
    return {
        getName: function getName() {
            return self.name;
        },
        getType: function getType() {
            return 'ShortLogger';
        },
        i: function i(message) {
            console.info(LOG_HEADER + getTime() + '[I]: ' + message);
        },
        d: function d(message) {
            console.log(LOG_HEADER + getTime() + '[D]: ' + message);
        },
        w: function w(message) {
            console.warn(LOG_HEADER + getTime() + '[W]: ' + message);
        },
        e: function e(message) {
            console.error(LOG_HEADER + getTime() + '[E]: ' + message);
        }
    }
}

module.exports = {  
    getLogger: ShortLogger
}

As we can see this applies the Android Logger like standard, where the name of the log method is the first letter of the log message type, for example w stands for warning.

In this case the role of the adapter is to give the possibility to the development team to use any kind of logger what they want.

When building adapters there are two approaches:

  1. Build adapters for every component which have to interact with each other. In this case it means to build two adapters, one for ShortLogger and one for BadLogger.

  2. Build one adapter which can adapt any of the same type component. In this case it means to build one Adapter which should handle any logger type.

Lets stick to the second approach, below is the code for the LoggerAdapter module:

var ShortLogger = require('./ShortLogger');  
var BadLogger = require('./BadLogger');

function LoggerAdapter(loggerObj) {  
    if (!loggerObj) {
        throw Error('Parameter [loggerObj] is not defined.');
    }
    console.log('[LoggerAdapter] is using Logger with name: ' + loggerObj.getName());
    var CONSTANTS = {
        DEBUG: 'DEBUG',
        WARNING: 'WARNING',
        INFORMATION: 'INFORMATION',
        ERROR: 'ERROR',
        BAD_LOGGER: 'BadLogger',
        SHORT_LOGGER: 'ShortLogger'
    };
    var loggerFunctionMapper = {};

    if(loggerObj.getType() === CONSTANTS.BAD_LOGGER) {
        loggerFunctionMapper[CONSTANTS.DEBUG] = loggerObj.debg;
        loggerFunctionMapper[CONSTANTS.INFORMATION] = loggerObj.information;
        loggerFunctionMapper[CONSTANTS.WARNING] = loggerObj.w;
        loggerFunctionMapper[CONSTANTS.ERROR] = loggerObj.err;
    }
    else if (loggerObj.getType() === CONSTANTS.SHORT_LOGGER) {
        loggerFunctionMapper[CONSTANTS.DEBUG] = loggerObj.d;
        loggerFunctionMapper[CONSTANTS.INFORMATION] = loggerObj.i;
        loggerFunctionMapper[CONSTANTS.WARNING] = loggerObj.w;
        loggerFunctionMapper[CONSTANTS.ERROR] = loggerObj.e;
    }

    function information(message) {
        try {
          loggerFunctionMapper[CONSTANTS.INFORMATION](message);
        }
        catch(err) {
            throw Error('No implementation for Logger: ' + loggerObj.toString());
        }
    };

    function debug(message) {
        try {
          loggerFunctionMapper[CONSTANTS.DEBUG](message);
        }
        catch(err) {
            throw Error('No implementation for Logger: ' + loggerObj.toString());
        }
    };
...
    return {
        debug: debug,
        information: information,
        warning: warning,
        error: error
    }
}

module.exports = {  
    LoggerAdapter: LoggerAdapter
}

When creating the adapter, a loggerObj has to be passed in, this is used for the real logging.

Once the project is refactored to use the LoggerAdapter for logging, the underlying logger can be changed much easier.

All the magic in the adapter is the if/else part at the beginning:

if(loggerObj.getType() === CONSTANTS.BAD_LOGGER) {  
        loggerFunctionMapper[CONSTANTS.DEBUG] = loggerObj.debg;
        loggerFunctionMapper[CONSTANTS.INFORMATION] = loggerObj.information;
        loggerFunctionMapper[CONSTANTS.WARNING] = loggerObj.w;
        loggerFunctionMapper[CONSTANTS.ERROR] = loggerObj.err;
    }
    else if (loggerObj.getType() === CONSTANTS.SHORT_LOGGER) {
        loggerFunctionMapper[CONSTANTS.DEBUG] = loggerObj.d;
        loggerFunctionMapper[CONSTANTS.INFORMATION] = loggerObj.i;
        loggerFunctionMapper[CONSTANTS.WARNING] = loggerObj.w;
        loggerFunctionMapper[CONSTANTS.ERROR] = loggerObj.e;
    }

This is where we adapt the interfaces, depending on the type of log message and loggerObj we map the correct function to the log message type.

In case a new logger library needs to be supported, lets say a remote logger library which logs to a RESTful API, only this mapping needs to be adjusted and the new logger library can be used.

Below is a use case of the LoggerAdapter:

var ShortLogger = require('./ShortLogger');  
var BadLogger = require('./BadLogger');  
var LoggerAdapter = require('./LoggerAdapter');  
var shortLog = ShortLogger.getLogger('ShortLoger');  
var badLogger = BadLogger.getLogger('BadLogger');

var loggerAdapter = LoggerAdapter.LoggerAdapter(badLogger);  
loggerAdapter.information('This is logged through LoggerAdapter');  
loggerAdapter.debug('This is logged through LoggerAdapter');  
loggerAdapter.warning('This is logged through LoggerAdapter');  
loggerAdapter.error('This is logged through LoggerAdapter');

console.log();

var loggerAdapter2 = LoggerAdapter.LoggerAdapter(shortLog);  
loggerAdapter2.information('Now This is logged through LoggerAdapter');  
loggerAdapter2.debug('Now This is logged through LoggerAdapter');  
loggerAdapter2.warning('Now This is logged through LoggerAdapter');  
loggerAdapter2.error('Now This is logged through LoggerAdapter');

Once this finished, the output was the following:

[LoggerAdapter] is using Logger with name: BadLogger
[BadLogger]:This is logged through LoggerAdapter- INFORMATION
[BadLogger]:This is logged through LoggerAdapter- DEBG
[BadLogger]:This is logged through LoggerAdapter- W
[BadLogger]:This is logged through LoggerAdapter- ERR

[LoggerAdapter] is using Logger with name: ShortLoger
[ShortLoger][2016-11-23T21:10:59.729Z][I]: Now This is logged through LoggerAdapter
[ShortLoger][2016-11-23T21:10:59.729Z][D]: Now This is logged through LoggerAdapter
[ShortLoger][2016-11-23T21:10:59.730Z][W]: Now This is logged through LoggerAdapter
[ShortLoger][2016-11-23T21:10:59.730Z][E]: Now This is logged through LoggerAdapter

If you enjoyed reading this post, please share it or like the Facebook page of the website to get regular updates. You can also follow us @dealwithjs.