Adapter

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:

Adapter

The preceding diagram shows how the Adapter is essentially a wrapper for the Adaptee, exposing a different interface. The diagram also highlights 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 provide a bridge to the methods of the Adaptee. This pattern is pretty straightforward, so let's immediately work on an example.

Using LevelUP through the filesystem API

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:

const path = require('path'); 
 
module.exports = function createFsAdapter(db) { 
  const 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 one of the original functions from the fs module:

fs.readFile = (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 
    }, 
    (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:

  1. To retrieve a file from the 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.
  2. If the key is not found in the database, we create an error with ENOENT as the 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).
  3. If the key/value pair is retrieved successfully from the database, we will return the value to the caller using the callback.

As we can 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 = (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 line of code:

  return fs; 
} 

Our new adapter is now ready; if we now write a small test module, we can try to use it:

const fs = require('fs'); 
 
fs.writeFile('file.txt', 'Hello!', () => { 
  fs.readFile('file.txt', {encoding: 'utf8'}, (err, res) => { 
    console.log(res); 
  }); 
}); 
 
//try to read a missing file 
fs.readFile('missing.txt', {encoding: 'utf8'}, (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:

const levelup = require('level'); 
const fsAdapter = require('./fsAdapter'); 
const db = levelup('./fsDB', {valueEncoding: 'binary'}); 
const fs = fsAdapter(db); 

Running our program again should produce the same output, except for the fact that no parts 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 3, Asynchronous Control Flow Patterns with Callbacks, 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 will soon realize that Adapter is also an extremely important pattern when it comes to sharing code with the browser, as we will see in more detail in Chapter 8, Universal JavaScript for Web Applications.

In the wild

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:

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset