Digital Web Magazine

The web professional's online magazine of choice.

Excerpt: Pro JavaScript Design Patterns

Got something to say?

Share your comments on this topic with other web professionals

In: Articles

By Ross Harmes

By Dustin Diaz

Published on January 14, 2008

The Adapter Pattern

The adapter pattern allows you to adapt existing interfaces to classes that would otherwise be incompatible. Objects that use this pattern can also be called wrappers, since they wrap another object in a new interface. There are many situations where creating adapters can benefit both the programmers and the interface designers. Often when designing classes, some interfaces can’t be used with existing APIs. Adapters allow you to use these classes without modifying them directly. In this chapter we look at some of those situations and explore the ways in which the adapter pattern can be used to join objects together.

Characteristics of an Adapter

Adapters are added to existing code to reconcile two different interfaces. If the existing code already has an interface that is doing agood job, there may be no need for an adapter. But if an interface is unintuitive or impractical for the task at hand, you can use an adapter to provide a cleaner or more option-rich interface.

On the surface, the adapter pattern seems to be very similar to the facade pattern. They both wrap another object and change the interface that it presents to the world. The difference lies in how that interface is changed. A facade presents a simplified interface; it doesn’t contain any extra options and sometimes makes assumptions in an attempt to make common tasks much easier. An adapter converts one interface to another; it doesn’t remove any abilities or otherwise simplify the interface. Adapters are required for clients that expect an API that isn’t available.

Adapters can be implemented as a thin layer of code between incompatible method calls. You might have a particular function that takes three strings as arguments, but the client is holding an array with three string elements. An adapter can be used to allow the two to be used together.

Imagine a case where you have asingle object but a function takes three separate strings as arguments:

var clientObject = {
  string1: 'foo',
  string2: 'bar',
  string3: 'baz' 
}; 
function interfaceMethod(str1, str2, str3) {
  ... 
}

In order to pass clientObject as an argument to interfaceMethod, an adapter is required. You can create one like this:

function clientToInterfaceAdapter(o) {
  interfaceMethod(o.string1, o.string2, o.string3); 
}

You can now simply pass in the entire object to the function:

clientToInterfaceAdapter(clientObject);

Note that clientToInterfaceAdapter simply wraps interfaceMethod and converts the arguments given into what the function expects to receive.

Adapting Existing Implementations

In some cases, code cannot be modified from the client’s end. This is why some programmers avoid creating APIs altogether. Once you change an existing interface, you must update all client code to use this new interface or risk breaking your application. When you introduce a new interface, it’s often wise to give your clients adapters that will implement the new interface for them.

In PC hardware, the PS2 slot was the standard interface for connecting your mouse and keyboard. For many years, nearly all PCs shipped with this interface, giving mouse and keyboard designers (clients in the terminology of this chapter) a single fixed target to aim at. As time passed, hardware engineers figured out away to avoid the PS2 interface entirely, allowing the USB system to support keyboards, mice, and other peripherals.

But then came the problem. To the motherboard engineers, it didn’t really matter if a consumer had a USB keyboard or not. They chose to cut costs (and save real estate) by shipping motherboards without PS2 slots. Suddenly keyboard developers realized that if they hoped to sell the thousands of keyboard and mouse products that they had built with PS2 interfaces, they better get working on an adapter. And so the familiar PS2-to-USB adapter was born.

Example: Adapting One Library to Another

These days there are many JavaScript libraries to choose from. Library users should decide very carefully which set of utilities will most likely to suit their needs and how these may impact their development. There are other things to consider, too: the coding style of other developers, the ease of implementation, and the conflicts and incompatibilities with existing code.

Nevertheless, even when all decisions have been made, a team may decide later to switch libraries without changing the code base, for reasons of performance, security, or design. A company might even incorporate an intermediary set of adapters to assist junior developers—for instance, if they are migrating from a more familiar API.

In the most straightforward scenario, creating an adapter library is often a better alternative than going forward with an entire code rewrite. Let’s look at an example that uses the Prototype library $ function and adapts it to the Yahoo! User Interface (YUI) getmethod. The two are similar in functionality—but take a look at the difference between their interfaces:

// Prototype $ function. 
function $() {
  var elements = new Array();
  for(var i = 0; i < arguments.length; i++) {
    var element = arguments[i];
    if(typeof element == 'string')
      element = document.getElementById(element);
    if(arguments.length == 1)
      return element;
    elements.push(element);
  }
  return elements;
}

/* YUI get method. */ 
YAHOO.util.Dom.get = function(el) {
  if(YAHOO.lang.isString(el)) {
    return document.getElementById(el);
  }
  if(YAHOO.lang.isArray(el)) {
    var c = [];
    for(var i = 0, len = el.length; i < len; ++i) {
      c[c.length] = Y.Dom.get(el[i]);
    }
    return c;
  }
  if(el) {
    return el;
  }
  return null;
};

The key difference is that get accepts a single argument, which can be an HTML element, a string, or an array of strings or HTML elements. In contrast, the $ function doesn’t take any formal parameters but rather allows the client to pass in an arbitrary number of arguments, accepting both strings and HTML elements.

Let’s take a look at what an adaptermight look like if you migrate Prototype’s $ function to use YUI’s get method (and vice versa). The implementation is surprisingly simple:

function PrototypeToYUIAdapter() {
  return YAHOO.util.Dom.get(arguments);
} 
function YUIToPrototypeAdapter(el) {
  return $.apply(window, el);
}

Note how the adapters wrap the adaptee methods, allowing existing clients to implement afamiliar API. When a Prototype library user wants to take advantage of the YUI method, she can adapt all her existing code simply by plugging in the $ function to the adapter function. You don’t have to modify any of the methods; just add this line, for those migrating from Prototype to YUI:

$ = PrototypeToYUIAdapter;

or vice versa, for those who are migratingfrom YUI to Prototype:

YAHOO.util.Dom.get = YUIToPrototypeAdapter;

Example: Adapting an Email API

In this example we look at a webmail API that allows you to retrieve mail, send mail, and perform some other tasks. We’ll use Ajax-like techniques by fetching messages from a server and then loading message details into the DOM. When you’ve finished the application interface, you’ll see how you can write wrapper functions that allow this API to work with clients that expect a different interface.

First things first; let’s take alook at the entire application:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" 
  "http://www.w3.org/TR/html4/strict.dtd" 
>
<html>
  <head>
    <title>Mail API Demonstration</title>
    <style type="text/css" media="screen">
      body {
        font: 62.5% georgia,times,serif;
      }
      #doc {
        margin: 0 auto;
        width: 500px;
        font-size: 1.3em;
      }
    </style>
    <script src="lib-utils.js"></script>
    <script type="text/javascript">
      // application utilities
      var DED = {};
      DED.util = {
        substitute: function (s, o) {
          return s.replace(/{([^{}]*)}/g,
            function (a, b) {
              var r = o[b];
              return typeof r === 'string' || typeof r === 'number' ? r : a;
            }
          );
        },
        asyncRequest: function() {
          function handleReadyState(o, callback) {
            var poll = window.setInterval(
              function() {
                if(o && o.readyState == 4) {
                  window.clearInterval(poll);
                  if ( callback ){
                    callback(o);
                  }
                }
              },
              50
            );
          }
          var getXHR = function() {
            var http;
            try {
              http = new XMLHttpRequest;
              getXHR = function() {
                return new XMLHttpRequest;
              };
            }
            catch(e) {
              var msxml = [
                'MSXML2.XMLHTTP.3.0',
                'MSXML2.XMLHTTP',
                'Microsoft.XMLHTTP'
              ];
              for (var i=0, len = msxml.length; i < len; ++i) {
                try {
                  http = new ActiveXObject(msxml[i]);
                  getXHR = function() {
                    return new ActiveXObject(msxml[i]);
                  };
                  break;
                }
                catch(e) {}
              }
            }
            return http;
          };
          return function(method, uri, callback, postData) {
            var http = getXHR();
            http.open(method, uri, true);
            handleReadyState(http, callback);
            http.send(postData || null);
            return http;
          };
        }()
      }

      // dedMail applicationinterface.
      var dedMail = (function() {
        function request(id, type, callback) {
          DED.util.asyncRequest(
            'GET',
            'mail-api.php?ajax=true&id=' + id + '&type=' + type,
            function(o) {
              callback(o.responseText);
            }
          );
        }
        return {
          getMail: function(id, callback) {
            request(id, 'all', callback);
          },
          sendMail: function(body, recipient) {
            // Send mail with body text to the supplied recipient.
          },
          save: function(id) {
            // Save adraft copy with the supplied email ID.
          },
          move: function(id, destination) {
            // Move the email to the supplied destination folder.
          },
          archive: function(id) {
            // Archive the email. This can be abasic facade method that uses
            // the move method, hard-coding the destination.
          },
          trash: function(id) {
            // This can also be afacademethod which moves the message to
            // the trash folder.
          },
          reportSpam: function(id) {
            // Move message to spam folder and add sender to the blacklist.
          },
          formatMessage: function(e) {
            var e = e || window.event;
            try {
              e.preventDefault();
            }
            catch(ex) {
              e.returnValue = false;
            }
            var targetEl = e.target || e.srcElement;
            var id = targetEl .id.toString().split('-')[1];
            dedMail.getMail(id, function(msgObject) {
              var resp = eval('('+msgObject+')');
              var details =  '<p><strong>From:</strong> {from}<br>';
              details += '<strong>Sent:</strong> {date}</p>';
              details += '<p><strong>Message:</strong><br>';
              details += '{message}</p>';
              messagePane.innerHTML = DED.util.substitute(details, resp);
            }
          };
        })();

        // Set up mail implementation.
        addEvent(window, 'load', function() {
          var threads = getElementsByClass('thread', 'a');
          var messagePane = $('message-pane');
          for (var i=0, len=threads.length; i<len; ++i) {
            addEvent(threads[i], 'click', formatMessage);
          }
        });
    </script>
  </head>
  <body>
    <div id="doc">
      <h1>Email Application Interface</h1>
      <ul>
        <li>
          <a class="thread" href="#" id="msg-1">
            load message Sister Sonya
          </a>
        </li>
        <li>
          <a class="thread" href="#" id="msg-2">
            load message Lindsey Simon
          </a>
        </li>
        <li>
          <a class="thread" href="#" id="msg-3">
            load message Margaret Stoooart
          </a>
        </li>
      </ul>
      <div id="message-pane"></div>
    </div>
  </body>
</html>

Before going into more detail about the code, here is a brief snapshot of the final output after clicking one of the message items. It should give you a better idea of what you’ll be working with.

Screen capture

The first thing you might notice is that the base set of utilities, which includes getElementsByClass, $, and addEvent, is included. Next, a few application utilities are added onto the DED.util namespace, which will aid in the application development. The DED.util.substitute method basically allows you to substitute strings when supplied an object literal.

Here is an example:

var substitutionObject = {
  name: "world" 
  place: "Google" 
}; 
var text = 'Hello {name}, welcome to {place}';
var replacedText = DED.util.substitute(text, substitutionObject);
console.log(replacedText);
// produces "Hello world, welcome to Google"

The next utility function is an asyncRequest function that lets you make calls to the back end. Note also that alazy loading technique is used that abstracts the XMLHttpRequest object by branching at load time to take care of browser differences. Then the getXHR function is reassigned after the first time it is called to get the XHR object. This will speed up the application tremendously by reducing the amount of object detection. Instead of detecting browser differences on every call, it is only done once.

Finally, let’s move to the dedMail singleton:

var dedMail = (function() {...

This object allows you to run the common mail methods such as getMail, sendMail, move, archive, and so on. Note that logic is only written for the getMail method, which retrieves mail from the server using the supplied ID as a reference. After the message has finished loading, the callback is notified with the response text. You could in fact use a publish/subscribe pattern to listen for a ready event, but this functional style is fairly common when doing XHR calls. It is also a matter of preference for interface developers.

Wrapping the Webmail API in an Adapter

Now that the application interface is all set up and ready to be used, you can call it in the client code. Everything seems to work fine: you used the supplied methods, you took the precaution of testing the callbacks, and you parsed the data object and loaded it into the DOM accordingly. But wait. The folks over in the experimental engineering team have already written their code to use the old fooMail system, and they would like to take advantage of the new and improved dedMail interface. The problem is that their methods expect HTML fragments. It also only takes in an ID into the constructor. And lastly, their getMail function requires a callback function as its only argument. It’s a bit old-school (so think the engineers on the dedMail team), but the fooMail engineers can definitely benefit from dedMail’s performance testing. Last but not least, the fooMail engineers would like to avoid an entire code rewrite. And so the decision is made: let there be adapters.

Migrating from fooMail to dedMail

Just like the Prototype and YUI adapters, migrating from fooMail to dedMail should be a relatively simple task. With proper knowledge of both the suppliers and the receivers, you can intercept incoming logic from the suppliers and transform them in away that the receivers can understand.

First let’s look at apiece of code that uses the fooMail API:

fooMail.getMail(function(text) {
  $('message-pane').innerHTML = text;
});

Notice that the getMail method takes in a callback method, which is a response in plain text including each sender’s name, date, and message. It’s not ideal, but the fooMail engineers don’t want to change it and risk breaking the existing application. Here’s how you can write a basic adapter for the fooMail implementers without altering their existing code:

var dedMailtoFooMailAdapter = {}; 
dedMailtoFooMailAdapter.getMail = function(id, callback) {
  dedMail.getMail(id, function(resp) {
    var resp = eval('('+resp+')');
    var details =  '<p><strong>From:</strong> {from}<br>';
    details += '<strong>Sent:</strong> {date}</p>';
    details += '<p><strong>Message:</strong><br>';
    details += '{message}</p>';
    callback(DED.util.substitute(details, resp));
  }); 
}; 
// Other methods needed to adapt dedMail to the fooMail interface. 
... 
// Assign the adapter to the fooMail variable. 
fooMail = dedMailtoFooMailAdapter;

Here, the fooMail object is overwritten with the dedMailtoFooMailAdapter singleton. The getMail method is implemented within this singleton. It will properly handle the callback method and deliver it back to the client in the HTML format it is looking for.

When Should the Adapter Pattern Be Used?

Adapters should be used in any place where clients expect aparticular interface but the interface offered by the existing API is incompatible. Adapters should only be used to reconcile differences in syntax; the method you are adapting still needs to be able to perform the needed task. If this is not true, an adapter will not solve your problem. Adapters can also be used when clients prefer a different interface, perhaps one that is easier for them to use. When you create an adapter, just like a bridge or a facade, you decouple an abstraction from its implementation, allowing them to vary independently.

Benefits of the Adapter Pattern

As mentioned throughout this chapter, adapters can help avoid massive code rewrites. They handle logic by wrapping a new interface around that of an existing class so you can use new APIs (with different interfaces) and avoid breaking existing implementations.

Drawbacks of the Adapter Pattern

The main reason some engineers may wish to avoid adapters is that they necessarily entail writing brand-new code. Some say adapters are unnecessary overhead that can be avoided by simply rewriting existing code. Adapters may also introduce a new set of utilities to be supported. If an existing API is not finalized or, even more likely, a newer interface is not finalized, the adapters may not continue to work. In the case where keyboard hardware engineers created PS2-to-USB adapters, it made complete sense because the PS2 plug was essentially finalized on thousands of keyboards; and the USB interface became the new standard. In software development, this is not always guaranteed.

Summary

The adapter pattern is a useful technique that allows you to wrap classes and objects, thus giving client code exactly the interface that it expects. You can avoid breaking existing implementations and adapt to newer, better interfaces. You can customize the interface to your own needs as an implementer. Adapters do in fact introduce new code; however, the benefits most likely outweigh the drawbacks in large systems and legacy frameworks.

Excerpted with permission from Pro JavaScript Design Patterns by Ross Harmes and Dustin Diaz. Copyright 2007. Published by Apress. Visit the official site at jsdesignpatterns.com.

Got something to say?

Share your comments  with other professionals (1 comment)

Related Topics: Technology, Programming

 

Ross Harmes works as a frontend engineer for Yahoo! in Sunnyvale, California, where he creates modular and reusable JavaScript components. His technical writings and online projects, such as the YUI Bundle for TextMate, can be found at techfoolery.com.

 

Dustin Diaz is a User Interface Engineer for Google, the author of DED|Chain JavaScript library, and founder of CSS Naked Day. Blogger, podcaster, screencaster, Dustin does whatever is in his imagination to help other people learn how to make interactive and usable interfaces to create passionate users.

Media Temple

via Ad Packs