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:
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:
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.
The participants of Adapter Pattern include:
Storage
Defines the interface of existing targets that works with client
IndexedDB
The implementation that is not designed to work with the client
IndexedDBStorage
Conforms the interface of target and interacts with adaptee
Manipulates the target
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.
Start with the Storage
interface:
interface Storage { get<T>(key: string): Promise<T>; set<T>(key: string, value: T): Promise<void>; }
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.
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.
3.143.17.27