Introduction
The Adapter design pattern helps objects with incompatible interfaces to collaborate. It's useful when integrating existing components into a new system, working with legacy code, or combining third-party libraries that have different interface requirements. By acting as a bridge, the Adapter allows otherwise incompatible classes to communicate without modifying their original code, promoting reusability and flexibility in software design.
Definition
The Adapter design pattern is a structural design pattern that allows object with incompatible interfaces collaborate.
What kind of problems can the Adapter design pattern solve?
- How can classes with incompatible interfaces work together?
- How can an alternative interface be provided to the class?
- How can a class be used if it doesn't provide the interface that a client requests?
The existing class can't be used because its interface is incompatible with the interface required by the client.
What solutions does the Adapter design pattern describe? b
- Define a separate adapter object that converts the incompatible interface of a class into the interface required by the client.
- Work with the adapter object for classes that don't have the required interface.
In this case, clients don't know whether they are working with the adapter object or the real object, because the adapter converts the incompatible interface into the required interface.
This pattern is similar to other design patterns, such as Facade, Decorator, and Proxy. You can read more about the differences in the Proxy article.
UML Diagram
The client requires the Target
interface. However, this interface cannot reuse the Adaptee
object because they have incompatible interfaces. Instead, the client works through the Adapter
objects, which transform the Adaptee
object to match the required protocol.
Examples
Let's imagine that we have legacy code in the project, and we need to reuse some functionality from the legacy codebase.
We have a class that defines how to fetch a user from the database. Let's imagine that we don't have access to the source code of the UserFetcher
class and can't modify it.
final class UserFetcher {
func user(_ userID: UUID, completion: @escaping (Result<User, Error>)) {
// Fetches user data in some way and returns a result
// Example: Fetch user from a legacy database or API
completion(.success(User(id: userID, name: "John Doe", email: "john.doe@example.com")))
}
}
There is a new implementation that requires the use of Swift Concurrency with async/await
.
protocol IUserFetcherAdapter {
func user(_ userID: UUID) async throws -> User
}
Here is the adapter class that uses the legacy implementation under the hood, thereby converting the incompatible interface into the interface required by the client.
final class UserFetcherAdapter: IUserFetcherAdapter {
// MARK: Properties
private let legacyUserFetcher: UserFetcher
// MARK: Initialization
init(legacyUserFetcher: UserFetcher) {
self.legacyUserFetcher = legacyUserFetcher
}
// MARK: IUserDatabaseService
func user(_ userID: UUID) async throws -> User {
try await withCheckedThrowingContinuation { continuation in
legacyUserFetcher.user(userID) { result in
switch result {
case let .success(user):
continuation.resume(returning: user)
case let .failure(error):
continuation.resume(throwing: error)
}
}
}
}
}
Summary
The Adapter design pattern allows objects with incompatible interfaces to work together by acting as a bridge between them. It transforms the interface of an existing class into one that the client expects, enabling seamless integration without modifying the original code. This pattern is particularly useful when integrating legacy systems, third-party libraries, or different components within a software project. In this article, we explored the principles of the Adapter pattern, its implementation, and real-world use cases.
Top comments (0)