In my previous posts I explained the idea of Service Objects, and how to test them. I also prepared the repository with the working code on GitHub - check it out if you feel like it.The code contains also another fascinating design pattern - adapter for Facebook API. Let's take a closer look at this bad boy.The idea behind this adapter is to abstract the implementation of the performed operation (communication with Facebook API) and isolate the rest of the code from details.Here is what the adapter looks like:// async.js
var FB = require('fb');
var config = require('config/config');
module.exports = function FacebookAdapter() {
FB.options({ version: 'v2.8' });
FB.setAccessToken(config.FB_TOKEN);
this.fetch = function (pathname, options) {
return new Promise(
function (resolve, reject) {
FB.api(
pathname,
'get',
options,
function (response) {
if (!response) {
reject('Error occurred');
}
if (response.error) {
reject(response.error.message);
}
resolve(response);
}
);
}
);
};
};
The adapter exposes one method, and for the rest of the code, it doesn't matter HOW it's done. The service objects (or whatever is supposed to communicate with Facebook API) don't care about what specific library is used, what API is, what's the configuration etc. Those are under the hood, inside the adapter.All that matters for the external world are exposed by the adapter's API. In our case, it's only one method - fetch - with two parameters.The rationale behind having the layer of adapters for 3rd parties is wide:
It's testable
Adapter is a class. You can fairly easily test it by creating an instance of it, if you'd like.
You define the API
The adapter uses fb library which exposes a multitask method - FB.api. We don't want to use it in its full power - we need only to fetch some data.The adapter defines the API that is convenient for our code.
DRY
Note that fetch takes 2 arguments while FB.api takes 4. The remaining two are defined only once in the adapter.
EASY TO CHANGE
The logic can be easily changed because it resides in one class. Perhaps it's a big deal when changing the underlying library or upgrading its version.
CLEAN CODE
The details are put aside, which makes the parts of the code that use it easier to read and understand.Would you rather work with such a code:var pathname = pageId;
var options = {
fields: 'name,about,link,location,posts.limit(5).fields(message,type)',
};
return facebookAdapter.fetch(pathname, options);
or that one?FB.api(
pageId,
'get',
{ fields: 'name,about,link,location,posts.limit(5).fields(message,type)',},
function (response) {
if (!response) {
reject('Error occurred');
}
if (response.error) {
reject(response.error.message);
}
resolve(response);
}
);
Adapter sets the boundary between the logic and the implementation details.
MANY IMPLEMENTATIONS
You can have multiple implementations of the adapters.One useful trick with the adapter is switching the implementation based on the environment.Usually, we don't want to use real network connection in your test. One way to deal with this is to write something like FakeFacebookAdapter that only simulates the real behavior. Because we have no intention to use it in production, we can add a "test-interface", helpful for our test.Here is an example implementation:// fake.js
module.exports = function FacebookAdapter() {
var that = this;
var exampleResponse = {
// whatever
};
this.requestSent = [];
this.clear = function () {
that.requestSent = [];
};
this.fetch = function (pathname, options) {
that.requestSent.push(pathname);
return new Promise(
function (resolve, reject) {
resolve(exampleResponse);
}
);
};
};
fetch method "pretends" the communication with real API. Its implementation is simple and would be ever simpler - however, we need to abide the contract between the "real" adapter and the application, and therefore use Promise.No network is involved - welcome, fast and reliable tests!This implementation allows us not to use mocks for web requests at all.To distinguish the implementation of the adapter, one has to write the code that matches the environment to implementation. This is my proposition - a singleton-like interface:// facebookAdapter.js
var Async = require('adapters/facebook/async');
var Fake = require('adapters/facebook/fake');
if (process.env.ENV == 'test') {
global.__facebookAdapter = global.__facebookAdapter || new Fake();
} else {
global.__facebookAdapter = global.__facebookAdapter || new Async();
}
module.exports = global.__facebookAdapter;
(if you wonder what the naming came from - async, fake - I usually consider also adding a synchronous implementation, sometimes useful in a development process. The names of the implementations indicate the way they work)This way the following line has a different meaning in different environments:var facebookAdapter = require('adapters/facebook/facebookAdapter');
Check the repo if you need more explanation.
SUMMARY
Use adapters! This is a design pattern with a variety of pros... and perhaps no cons.I think one has to have a really good reason not to use adapter for communicating with boundaries. And I mean a wide range of them - not only Facebook but also, for example, AWS, database, file system... maybe the API? BackendAdapter? Why not?In the next post, I'm going to explain the builder pattern and you'll see how nicely the abstraction of the adapter goes with this one.