The Adapter pattern allows us to access the functionality of an object using a different interface. As the name suggests, it adapts an object so that it can be used by components expecting a different interface. The following diagram clarifies the situation:
The preceding diagram shows how the Adapter is essentially a wrapper for the Adaptee, exposing a different interface. The diagram also highlight the fact that the operations of the Adapter can also be a composition of one or more method invocations on the Adaptee. From an implementation perspective, the most common technique is composition where the methods of the Adapter provides a bridge to the methods of the Adaptee. This pattern is pretty straightforward so let's work immediately on an example.
We are now going to build an adapter around the LevelUP API, transforming it into an interface that is compatible with the core fs
module. In particular, we will make sure that every call to readFile()
and writeFile()
will translate into calls to db.get()
and db.put()
; this way we will be able to use a LevelUP database as a storage backend for simple filesystem operations.
Let's start by creating a new module named fsAdapter.js
. We will begin by loading the dependencies and exporting the createFsAdapter()
factory that we are going to use to build the adapter:
var path = require('path'); module.exports = function createFsAdapter(db) { var fs = {}; //...continues with the next code fragments
Next, we will implement the readFile()
function inside the factory and ensure that its interface is compatible with the one of the original function from the fs
module:
fs.readFile = function(filename, options, callback) { if(typeof options === 'function') { callback = options; options = {}; } else if(typeof options === 'string') { options = {encoding: options}; } db.get(path.resolve(filename), { //[1] valueEncoding: options.encoding }, function(err, value) { if(err) { if(err.type === 'NotFoundError') { //[2] err = new Error('ENOENT, open \'' + filename +'\''); err.code = 'ENOENT'; err.errno = 34; err.path = filename; } return callback && callback(err); } callback && callback(null, value); //[3] } ); };
In the preceding code, we had to do some extra work to make sure that the behavior of our new function is as close as possible to the original fs.readFile()
function. The steps performed by the function are described as follows:
db
class, we invoke db.get()
using filename
as a key, by making sure to always use its full path (using path.resolve()
). We set the value of valueEncoding
used by the database to be equal to any eventual encoding
option received as an input.ENOENT
as error code, which is the code used by the original fs
module to indicate a missing file. Any other type of error is forwarded to callback
(for the scope of this example, we are adapting only the most common error condition).callback
.As we see, the function that we created is quite rough; it does not want to be a perfect replacement for the fs.readFile()
function but it definitely does its job in the most common situations.
To complete our small adapter, let's now see how to implement the writeFile()
function:
fs.writeFile = function(filename, contents, options, callback) { if(typeof options === 'function') { callback = options; options = {}; } else if(typeof options === 'string') { options = {encoding: options}; } db.put(path.resolve(filename), contents, { valueEncoding: options.encoding }, callback); }
Also, in this case, we don't have a perfect wrapper, we will ignore some options such as file permissions (options.mode
), and we will forward any error that we receive from the database as it is.
Finally, we only have to return the fs
object and close the factory function using the following lines of code:
return fs; }
Our new adapter is now ready; if we now write a small test module, we can try to use it:
var fs = require('fs'); fs.writeFile('file.txt', 'Hello!', function() { fs.readFile('file.txt', {encoding: 'utf8'}, function(err, res) { console.log(res); }); }); //try to read a missing file fs.readFile('missing.txt', {encoding: 'utf8'}, function(err, res){ console.log(err); });
The preceding code uses the original fs
API to perform a few read and write operations on the filesystem and should print something like the following to the console:
{ [Error: ENOENT, open 'missing.txt'] errno: 34, code: 'ENOENT', path: 'missing.txt' } Hello!
Now, we can try to replace the fs
module with our adapter, as follows:
var levelup = require('level'); var fsAdapter = require('./fsAdapter'); var db = levelup('./fsDB', {valueEncoding: 'binary'}); var fs = fsAdapter(db);
Running again our program should produce the same output, except the fact that none of the file that we specified is read or written using the filesystem; instead, any operation performed using our adapter will be converted into an operation performed on a LevelUP database.
The adapter that we just created might look silly; what's the purpose of using a database in place of the real filesystem? However, we should remember that LevelUP itself has adapters that enable the database to also run in the browser; one of these adapters is level.js
(https://npmjs.org/package/level-js). Now, our adapter should make perfect sense; we can think of using it to share with the browser code, which relies on the fs
module! For example, the web spider that we created in Chapter 2, Asynchronous Control Flow Patterns, uses the fs
API to store the web pages downloaded during its operations; our adapter will allow it to run in the browser, by applying only minor modifications! We soon realize that Adapter is an extremely important pattern also when it comes to sharing code with the browser, as we will see in more detail in Chapter 6, Recipes.
There are plenty of real-world examples of the Adapter pattern: we list some of the most notable examples here for you to explore and analyze:
jugglingdb
is a multi-database ORM and of course, multiple adapters are used to make it compatible with different databases. Take a look at some of them at https://github.com/1602/jugglingdb/tree/master/lib/adapters.level-filesystem
(https://www.npmjs.org/package/level-filesystem), which is the proper implementation of the fs
API on top of LevelUP.