Adapter pattern (Adapter) interface conversion practice
The Core Idea of the Adapter Pattern
The adapter pattern is a structural design pattern that allows incompatible interfaces to collaborate. Just like a power adapter in the real world enables plugs from different countries to work, in programming, an adapter acts as a bridge between two incompatible interfaces. This pattern is particularly useful when we need to use a class whose interface doesn't match other code.
Implementation of the Adapter Pattern in JavaScript
In JavaScript, there are two main ways to implement the adapter pattern:
- Class Adapter: Adapts interfaces through inheritance
- Object Adapter: Adapts interfaces through composition
Due to limited support for multiple inheritance in JavaScript, the object adapter is more common. Here's a simple example of an object adapter:
// Target interface
class Target {
request() {
return 'Target: Default behavior';
}
}
// Class to be adapted
class Adaptee {
specificRequest() {
return '.eetpadA eht fo roivaheb laicepS';
}
}
// Adapter
class Adapter extends Target {
constructor(adaptee) {
super();
this.adaptee = adaptee;
}
request() {
const result = this.adaptee.specificRequest().split('').reverse().join('');
return `Adapter: (Translated) ${result}`;
}
}
// Usage
const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
console.log(adapter.request()); // Output: Adapter: (Translated) Special behavior of the Adaptee.
Practical Application Scenarios
Third-Party Library Integration
When introducing third-party libraries, their APIs may not match our existing system's interfaces. For example, we have a unified logging system interface:
class Logger {
log(message) {
console.log(`[LOG] ${message}`);
}
}
But the new third-party logging library uses a different method signature:
class ThirdPartyLogger {
print(msg) {
console.log(`[THIRD PARTY] ${msg}`);
}
}
An adapter can be created:
class LoggerAdapter extends Logger {
constructor(thirdPartyLogger) {
super();
this.thirdPartyLogger = thirdPartyLogger;
}
log(message) {
this.thirdPartyLogger.print(message);
}
}
// Usage
const thirdPartyLogger = new ThirdPartyLogger();
const logger = new LoggerAdapter(thirdPartyLogger);
logger.log('The adapter pattern is really useful!'); // Output: [THIRD PARTY] The adapter pattern is really useful!
Data Format Conversion
Another common scenario is data format conversion. Suppose our system expects user data in this format:
{
fullName: 'Zhang San',
age: 30
}
But the backend API returns data in this format:
{
name: 'Zhang San',
years: 30
}
A user data adapter can be created:
class UserAdapter {
constructor(apiData) {
this.apiData = apiData;
}
get formattedUser() {
return {
fullName: this.apiData.name,
age: this.apiData.years
};
}
}
// Usage
const apiResponse = { name: 'Li Si', years: 25 };
const adaptedUser = new UserAdapter(apiResponse).formattedUser;
console.log(adaptedUser); // Output: { fullName: 'Li Si', age: 25 }
Advanced Adapter Pattern Applications
Multi-Adapter Systems
In complex systems, multiple adapters may be needed to handle different interface variants. For example, adapters for different map APIs:
// Unified map interface
class Map {
display(lat, lng) {}
}
// Google Maps adapter
class GoogleMapsAdapter extends Map {
constructor(googleMaps) {
super();
this.googleMaps = googleMaps;
}
display(lat, lng) {
this.googleMaps.show({ latitude: lat, longitude: lng });
}
}
// Baidu Maps adapter
class BaiduMapsAdapter extends Map {
constructor(baiduMaps) {
super();
this.baiduMaps = baiduMaps;
}
display(lat, lng) {
this.baiduMaps.render(lat, lng);
}
}
// Usage
function displayLocation(map, lat, lng) {
map.display(lat, lng);
}
const googleMap = new GoogleMapsAdapter(externalGoogleMaps);
const baiduMap = new BaiduMapsAdapter(externalBaiduMaps);
displayLocation(googleMap, 39.9042, 116.4074);
displayLocation(baiduMap, 31.2304, 121.4737);
Function Adapters
The adapter pattern can also be applied at the function level, especially when dealing with callback functions or event handlers:
// Original function
function oldApi(callback) {
// Simulate async operation
setTimeout(() => {
callback(null, { data: 'Data returned from old API' });
}, 1000);
}
// New API expects a Promise
function newApi() {
return new Promise((resolve, reject) => {
oldApi((err, result) => {
if (err) reject(err);
else resolve(result);
});
});
}
// Or create a more generic adapter function
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
};
}
const adaptedOldApi = promisify(oldApi);
adaptedOldApi().then(data => console.log(data)); // Output: { data: 'Data returned from old API' }
Pros and Cons of the Adapter Pattern
Advantages
- Single Responsibility Principle: Separates interface conversion code from business logic
- Open/Closed Principle: Introduces new adapters without modifying existing code
- Improved Reusability: Existing classes can be reused in systems with different interfaces
- Decoupling: Client code is decoupled from concrete implementations
Disadvantages
- Increased Complexity: Additional layers add complexity to the code
- Performance Overhead: Indirect calls may introduce slight performance penalties
- Overuse: May lead to too many small classes in the system, making maintenance difficult
Relationship with Other Patterns
- Decorator Pattern: Adapters change object interfaces, decorators enhance object functionality
- Facade Pattern: Adapters make existing interfaces usable, facades define new interfaces
- Proxy Pattern: Adapters convert interfaces, proxies control access
Considerations in Real Projects
When implementing adapters, consider the following factors:
- Interface Differences: Greater differences may lead to more complex adapters
- Performance Impact: Data conversion may become a bottleneck
- Maintenance Costs: Adapters need to be kept in sync with the adapted code
- Testing Strategy: Adapters require thorough testing to ensure correct conversion
For example, adapting different form libraries in a React application:
// Assume we have two form libraries
class FormikForm {
getValues() {
return { /* Formik-formatted data */ };
}
}
class FinalForm {
getFormState() {
return { /* Final Form-formatted data */ };
}
}
// Create a unified form adapter
class FormAdapter {
constructor(formInstance) {
this.form = formInstance;
}
getFormData() {
if (this.form instanceof FormikForm) {
return this.transformFormikData(this.form.getValues());
} else if (this.form instanceof FinalForm) {
return this.transformFinalFormData(this.form.getFormState());
}
throw new Error('Unsupported form type');
}
transformFormikData(data) {
// Convert Formik data to unified format
return { /* Unified format data */ };
}
transformFinalFormData(data) {
// Convert Final Form data to unified format
return { /* Unified format data */ };
}
}
// Usage
const formikForm = new FormikForm();
const finalForm = new FinalForm();
const formikAdapter = new FormAdapter(formikForm);
const finalFormAdapter = new FormAdapter(finalForm);
const unifiedData1 = formikAdapter.getFormData();
const unifiedData2 = finalFormAdapter.getFormData();
Browser API Adaptation
In modern web development, handling browser API differences is common, and the adapter pattern is well-suited for this scenario:
// Unified storage interface
class Storage {
setItem(key, value) {}
getItem(key) {}
}
// LocalStorage adapter
class LocalStorageAdapter extends Storage {
setItem(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
getItem(key) {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : null;
}
}
// IndexedDB adapter
class IndexedDBAdapter extends Storage {
constructor(dbName, storeName) {
super();
this.dbName = dbName;
this.storeName = storeName;
this.db = null;
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName, { keyPath: 'key' });
}
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve();
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}
setItem(key, value) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.put({ key, value });
request.onsuccess = () => resolve();
request.onerror = (event) => reject(event.target.error);
});
}
getItem(key) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.get(key);
request.onsuccess = () => resolve(request.result?.value || null);
request.onerror = (event) => reject(event.target.error);
});
}
}
// Usage
async function useStorage() {
const localStorageAdapter = new LocalStorageAdapter();
localStorageAdapter.setItem('test', { a: 1 });
console.log(localStorageAdapter.getItem('test')); // { a: 1 }
const indexedDBAdapter = new IndexedDBAdapter('MyDB', 'MyStore');
await indexedDBAdapter.init();
await indexedDBAdapter.setItem('test', { b: 2 });
const data = await indexedDBAdapter.getItem('test');
console.log(data); // { b: 2 }
}
useStorage();
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn