Adapter Pattern

Adapter Pattern connects existing classes or objects with another existing client. It makes classes that are not designed to work together possible to cooperate with each other.

An adapter could be either a class adapter or an object adapter. A class adapter extends the adaptee class and exposes extra APIs that would work with the client. An object adapter, on the other hand, does not extend the adaptee class. Instead, it stores the adaptee as a dependency.

The class adapter is useful when you need to access protected methods or properties of the adaptee class. However, it also has some restrictions when it comes to the JavaScript world:

  • The adaptee class needs to be extendable
  • If the client target is an abstract class other than pure interface, you can't extend the adaptee class and the client target with the same adapter class without a mixin
  • A single class with two sets of methods and properties could be confusing

Due to those limitations, we are going to talk more about object adapters. Taking browser-side storage for example, we'll assume we have a client working with storage objects that have both methods get and set with correct signatures (for example, a storage that stores data online through AJAX). Now we want the client to work with IndexedDB for faster response and offline usage; we'll need to create an adapter for IndexedDB that gets and sets data:

Adapter Pattern

We are going to use Promise for receiving results or errors of asynchronous operations. See the following link for more information if you are not yet familiar with Promise: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise.

Participants

The participants of Adapter Pattern include:

  • Target: Storage

    Defines the interface of existing targets that works with client

  • Adaptee: IndexedDB

    The implementation that is not designed to work with the client

  • Adapter: IndexedDBStorage

    Conforms the interface of target and interacts with adaptee

  • Client.

    Manipulates the target

Pattern scope

Adapter Pattern can be applied when the existing client class is not designed to work with the existing adaptees. It focuses on the unique adapter part when applying to different combinations of clients and adaptees.

Implementation

Start with the Storage interface:

interface Storage { 
  get<T>(key: string): Promise<T>; 
  set<T>(key: string, value: T): Promise<void>; 
} 

Note

We defined the get method with generic, so that if we neither specify the generic type, nor cast the value type of a returned Promise, the type of the value would be {}. This would probably fail following type checking.

With the help of examples found on MDN, we can now set up the IndexedDB adapter. Visit  IndexedDBStorage: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB.

The creation of IndexedDB instances is asynchronous. We could put the opening operation inside a get or set method so the database can be opened on demand. But for now, let's make it easier by creating an instance of IndexedDBStorage that has a database instance which is already opened.

However, constructors usually don't have asynchronous code. Even if they do, it cannot apply changes to the instance before completing the construction. Fortunately, Factory Method Pattern works well with asynchronous initiation:

class IndexedDBStorage implements Storage { 
  constructor( 
    public db: IDBDatabase, 
    public storeName = 'default' 
  ) { } 
 
  open(name: string): Promise<IndexedDBStorage> { 
    return new Promise<IndexedDBStorage>( 
      (resolve, reject) => { 
      let request = indexedDB.open(name); 
       
      // ... 
    }); 
  } 
} 

Inside the Promise resolver of method open, we'll get the asynchronous work done:

let request = indexedDB.open(name); 
 
request.onsuccess = event => { 
  let db = request.result as IDBDatabase; 
  let storage = new IndexedDBStorage(db); 
  resolve(storage); 
}; 
 
request.onerror = event => { 
  reject(request.error); 
}; 

Now when we are accessing an instance of IndexedDBStorage, we can assume it has an opened database and is ready to make queries. To make changes or to get values from the database, we need to create a transaction. Here's how:

get<T>(key: string): Promise<T> { 
  return new Promise<T>((resolve, reject) => { 
    let transaction = this.db.transaction(this.storeName); 
    let store = transaction.objectStore(this.storeName); 
 
    let request = store.get(key); 
 
    request.onsuccess = event => { 
      resolve(request.result); 
    }; 
 
    request.onerror = event => { 
      reject(request.error); 
    }; 
  }); 
} 

Method set is similar. But while the transaction is by default read-only, we need to explicitly specify 'readwrite' mode.

set<T>(key: string, value: T): Promise<void> { 
  return new Promise<void>((resolve, reject) => { 
    let transaction = 
      this.db.transaction(this.storeName, 'readwrite'); 
    let store = transaction.objectStore(this.storeName); 
 
    let request = store.put(value, key); 
 
    request.onsuccess = event => { 
      resolve(); 
    }; 
 
    request.onerror = event => { 
      reject(request.error); 
    }; 
  }); 
} 

And now we can have a drop-in replacement for the previous storage used by the client.

Consequences

By applying Adapter Pattern, we can fill the gap between classes that originally would not work together. In this situation, Adapter Pattern is quite a straightforward solution that might come to mind.

But in other scenarios like a debugger adapter for debugging extensions of an IDE, the implementation of Adapter Pattern could be more challenging.

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

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