The Adapter pattern is one of the structural design patterns that allows incompatible interfaces to work together. It was introduced in the book "Design Patterns: Elements of Reusable Object-Oriented Software" by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, known as "Gang of Four" (GoF) in 1994.
The Adapter pattern is one of the structural design patterns that allows two incompatible interfaces to work together. It does this by creating an intermediate class, called Adapter, which translates the interface from one class to another.
There are two types of Adapter: the Adapter Class and the Adapter Object.
In the Adapter Class, a class is created that inherits from both classes (the existing class and the target class) and implements the target interface. This intermediate class can then be used to adapt the existing class to the target class.
In the Adapter Object, an instance of an existing class is encapsulated within a new class that implements the target interface. That way this new class can adapt the existing class to the target class.
The Adapter pattern can be used in many situations, some of which include:
- When you have an existing class that needs to be reused, but its interface is not compatible with the class using it.
- When you want to create an intermediate class to translate data from one class to another, without affecting existing classes.
- When you need to work with several classes that have similar but not identical interfaces.
- When you want to create an interface for a legacy class so that it can be used by other classes without modifying the legacy class.
- When you need to work with classes from different libraries or frameworks that have incompatible interfaces.
- Anyway, the Adapter pattern can be used in any situation where it is necessary to adapt or translate an interface so that it can be used by another class.
Below is a simple code example using the Adapter pattern.
class RequestAdapter {
specificRequest() {
return "Adapter request";
}
}
class TargetRequest {
request() {
return "Target request";
}
}
class Adapter extends TargetRequest {
constructor() {
super();
this.requestAdapter = new RequestAdapter();
}
request() {
return this.requestAdapter.specificRequest();
}
}
const target = new TargetRequest();
console.log(target.request()); // "Adapter request"
const adapter = new Adapter();
console.log(adapter.request()); // "Target request"
In this example, we have a RequestAdapter class with a specific method specificRequest() that returns "Adapter request". We also have a TargetRequest class with a request() method that returns "Target request".
The Adapter class inherits from TargetRequest and contains an instance of RequestAdapter. The request() method in the Adapter class is rewritten to call the specificRequest() method on the Adaptee instance and return the result.
When creating an instance of TargetRequest and calling the request() method, it returns "Target request". When creating an instance of Adapter and calling the request() method, it returns "Adapter request" as it is calling the specific method on the RequestAdapter instance.
In this way, the Adapter class is adapting the interface of the RequestAdapter class to meet the interface of the TargetRequest class, without modifying the existing classes.
Simple, right?
Imagine another scenario in which you need to perform a search for some posts in an API while the development team builds the relationship between the posts with the user but would have to move to another request.
Follow the solution below:
class RequestAdapter {
async getPosts() {
return fetch('https://jsonplaceholder.typicode.com/posts/1')
.then((response) => response.json())
}
}
class TargetRequest {
request() {
return { user: { id: 1, name: 'mock', email: 'mock.@gmail.com' } };
}
}
class Adapter extends TargetRequest {
constructor() {
super();
this.requestAdapter = new RequestAdapter();
}
request() {
return this.requestAdapter.getPosts();
}
}
const target = new TargetRequest();
console.log(target.request()); // { user: { id: 1, name: 'mock', email: 'mock.@gmail.com' } }
const adapter = new Adapter();
adapter.request().then(response => console.log(response));
/*
{
userId: 1,
id: 1,
title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
body: "quia et suscipit\nsuscipit...
}
*/
In this example, we have a RequestAdapter class with a specific getPosts() method that uses Fetch to make a GET request and returns the response data. We also have a Target class with a request() method that returns a simple object { user: { id: 1, name: 'mock', email: 'mock.@gmail.com' } }.
The Adapter class inherits from TargetRequest and contains an instance of RequestAdapter. The request() method in the Adapter class is rewritten to call the getPosts() method on the RequestAdapter instance and return the result.
When creating an instance of TargetRequest and calling the request() method, it returns { user: { id: 1, name: 'mock', email: 'mock.@gmail.com' } }. When creating an instance of Adapter and calling the request() method, it returns data from the response of the GET request made through Fetch.
In this way, the Adapter class is adapting the interface of the RequestAdapter class to meet the interface of the TargetRequest class, without modifying the existing classes.
There are several advantages to using the Adapter pattern, some of which include:
- Code reuse: The Adapter pattern allows you to reuse existing code without modifying it. This can be useful when you have an existing class that is valuable, but its interface is not compatible with the class using it.
- Change isolation: The Adapter allows you to make changes to one class without affecting other classes. This can be useful when you need to update or fix an existing class, but don't want to affect other classes that are using it.
- Ease of maintenance: The Adapter pattern makes code easier to maintain, as classes are isolated and dependencies are clear. This can help you identify issues and make code changes more easily.
- Flexibility: The Adapter pattern is flexible and can be used in many situations, such as working with classes from different libraries or frameworks, or creating an interface for a legacy class.
- Abstraction: The Adapter pattern helps to abstract the differences between interfaces, facilitating the interaction between different classes.
Conclusion
Finally, using the Adapter pattern allows you to reuse existing code, isolate changes, facilitate maintenance, increase flexibility and abstract differences between interfaces. This can help keep the code clean, easy to understand, and scalable.
Hope this helps, until next time.
Top comments (0)