Introduction
The Adapter Pattern acts as a bridge between two incompatible interfaces. This type of design pattern comes under the structural pattern category as it combines the functionality of two independent interfaces. The primary motivation behind this pattern is compatibility: it allows classes that could not otherwise work together due to incompatible interfaces to collaborate.
In software engineering, this pattern involves wrapping a class (the Adaptee) in another class (the Adapter) that supports the interface required by the client (the Target). While it enhances flexibility and reusability, overusing the adapter pattern can lead to complex call graphs. Essentially, every adaptation is a form of compromise, and excessive compromise often necessitates eventual refactoring.
There are three main types of adapter implementations:
-
Class Adapter
-
Object Adapter
-
Interface Adapter (or Default Adapter)
-
Class Adapter
The Class Adapter pattern utilizes inheritance to achieve compatibility. The adapter class inherits from the existing class (Adaptee) and implements the target interface. This approach allows the adapter to possess the properties and methods of the original class while presenting the interface expected by the client.
Scenario: Consider a scenario where we have an OldDatabase class that handles data storage, but our new client code expects a DatabaseService interface.
Code Structure:
// Target Interface
public interface DatabaseService {
void saveData(String data);
}
// Adaptee Class
public class OldDatabase {
public void insertRecord(String record) {
System.out.println("OldDatabase: Inserting record '" + record + "'");
}
}
// Class Adapter
public class DatabaseAdapter extends OldDatabase implements DatabaseService {
@Override
public void saveData(String data) {
System.out.println("Adapter converting data format...");
// Delegating to the legacy method
insertRecord(data);
}
}
// Usage
public class Application {
public static void main(String[] args) {
DatabaseService service = new DatabaseAdapter();
service.saveData("User Configuration");
}
}
- Object Adapter
The Object Adapter achieves the same goal as the Class Adapter but uses composition instead of inheritance. Following the "Composition over Inheritance" principle, the adapter holdss an instance of the Adaptee class as a member variable. This approach is generally preferred in languages like Java that support single inheritance, as it preserves the ability to inherit from other classes.
Code Structure:
// Object Adapter
public class DatabaseObjectAdapter implements DatabaseService {
private OldDatabase database;
// Constructor injection
public DatabaseObjectAdapter(OldDatabase database) {
this.database = database;
}
@Override
public void saveData(String data) {
System.out.println("Object Adapter preparing data...");
database.insertRecord(data);
}
}
// Usage
public class Application {
public static void main(String[] args) {
OldDatabase legacyDb = new OldDatabase();
DatabaseService service = new DatabaseObjectAdapter(legacyDb);
service.saveData("User Configuration");
}
}
- Interface Adapter
The Interface Adapter, also known as the Default Adapter, is useful when an interface contains multiple methods, but a client class only needs to implement a subset of them. Instead of forcing the client to implement every method, we create an abstract class that implements the interface and provides default (often empty) implementations for all methods. The concrete classes can then extend this abstract class and override only the methods relevant to them.
Scenario: A system uses a Worker interface defining various tasks, but different types of workers perform only specific tasks.
Code Structure:
// Complex Interface
public interface Worker {
void work();
void eat();
void sleep();
}
// Default Adapter (Abstract Class)
public abstract class BaseWorker implements Worker {
@Override
public void work() {}
@Override
public void eat() {}
@Override
public void sleep() {}
}
// Concrete Implementation: RobotWorker
public class RobotWorker extends BaseWorker {
@Override
public void work() {
System.out.println("Robot is working continuously.");
}
// Robot does not need to eat or sleep, so we don't override those
}
// Concrete Implementation: HumanWorker
public class HumanWorker extends BaseWorker {
@Override
public void work() {
System.out.println("Human is working.");
}
@Override
public void eat() {
System.out.println("Human is having lunch.");
}
@Override
public void sleep() {
System.out.println("Human is sleeping.");
}
}
// Usage
public class Test {
public static void main(String[] args) {
Worker robot = new RobotWorker();
Worker human = new HumanWorker();
robot.work();
human.work();
human.eat();
human.sleep();
}
}
Summary
- Class Adapter: Relies on multiple inheritance (where supported) or inheriting from the Adaptee and implementing the Target. It binds the adapter tightly to the Adaptee.
- Object Adapter: Uses composition to hold a reference to the Adaptee. This is more flexible and adheres to the Single Responsibility Principle, making it the preferred approach in most scenarios.
- Interface Adapter: Provides a default implementation for an interface, allowing subclasses to pick and choose which methods to override. This reduces boilerplate code when dealing with "fat" interfaces.
The Adapter Pattern is essential a remedial pattern, often used during system maintenance or integration phases. It is a form of wrapper pattern, similar to the Decorator pattern, but with a focus on interface conversion rather than behavior enhancement. While powerful, developers should be cautious not to use it to patch fundamental design flaws; excessive adaptation suggests a need for architectural refactoring.