This is not rocket science, but when planning your web or mobile architecture, it is important to make sure your client application is not tightly coupled to a specific data access strategy.
The Problem
In tightly coupled applications, the presentation logic is intertwined with data access logic (for example, $.ajax() calls), and it leads to some of the problems below:
- Slows down the development process. Client side developers often have to wait for services to be built. The back and forth between client side and server side development hampers fast iterations.
- Hard to test. It’s hard to unit test a client application that is hard-wired to server components.
- Hampers the designer or developer creativity. It can be hard for the designer or client side developer to think creatively about the User Experience if he or she is constrained by a set of pre-existing data services.
- Hard to change. Changing the data access strategy requires major changes in the client code.
- Hard to switch the data access strategy on the fly. For example when the application connectivity status changes from online to offline.
The Solution: Pluggable Data Adapters
The solution is of course to decouple the client code from a specific data access strategy. Practically, this goal can be achieved by using pluggable data adapters.
I often start with an in-memory (or mock data) adapter that allows me to iterate quickly at the client side while the server side may not even exist yet. Later on, I can unplug the in-memory adapter and plug in an Ajax adapter to run my application against actual server data, or a WebSQL (or IndexedDB) adapter to run it against an embedded database.
There are many strategies to implement data adapters, and a complete review of these strategies is beyond the scope of this article. Here is a basic approach…
Define a Common Interface
To be interchangeable, your adapters have to expose a common API. Here is a simple example for an Employee Directory use case:
function initialize() {}; // Some adapters may require initialization. function findByName(key) {}; function findById(id) {};
Assume Asynchronous
In addition to defining methods with the same name, part of defining a common interface is also to use a common method invocation approach. For example, your in-memory and Ajax adapters wouldn’t be interchangeable if the in-memory adapter assumed synchronous method invocation and the Ajax adapter assumed asynchronous invocation. A good approach is to provide all your adapters with an asynchronous API. Fortunately, Deferred Objects and Promises make it really easy to work with asynchronous operations. There are different implementations of Deferred Objects and Promises. In the example below, we will use the jQuery implementation.
In-Memory Adapter Example
In this adapter, we work with mock data held in a private array. The public API methods return a promise that is resolved immediately (since looping through an array is a synchronous operation).
JSONP Adapter Example
In this adapter, we get the data from RESTful services using JSONP. The $.ajax() method returns a jqXHR object which implements the Promise interface.
The Client Application
Depending on the adapter you want to use, you import either memory-adapter.js or jsonp-adpater.js in index.html. You then instantiate the appropriate adapter on line 16 and you don’t have to change anything else in the application.
Source Code and Additional Adapters
The source code for this simple implementation of data adapters is available in this GitHub repository. The repository includes the following adapters:
- In-Memory Adapter
- LocalStorage Adapter
- WebSQL Adapter
- JSONP Adapter