What is Adapter design pattern and it’s advantages
What is Adapter design pattern?
Certainly! The Adapter Pattern is a structural design pattern that allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces, making them compatible without changing their existing code.
Example# 1:
let’s consider an example where you have a legacy system that uses a socket to connect electronic devices, and you want to introduce a new system that uses an electric base to connect devices. The existing system has a LegacySocket
class with methods that are incompatible with the new ElectricBase
interface. We’ll create an adapter to make the legacy socket compatible with the new interface.
Here’s the Java code:
/*
* Author: Zameer Ali Mohil
* */
// Existing LegacySocket class with an incompatible interface
class LegacySocket {
public void plugIn(String device) {
System.out.println("Legacy Socket: Plugged in " + device);
}
}
// New ElectricBase interface expected by client code
interface ElectricBase {
void connectDevice(String device);
}
// Adapter class that adapts LegacySocket to ElectricBase interface
class LegacySocketAdapter implements ElectricBase {
private LegacySocket legacySocket;
public LegacySocketAdapter(LegacySocket legacySocket) {
this.legacySocket = legacySocket;
}
@Override
public void connectDevice(String device) {
// Call the existing LegacySocket method from the adapted interface
legacySocket.plugIn(device);
}
}
// Client code that expects ElectricBase interface
class Client {
public void connectUsingElectricBase(ElectricBase electricBase, String device) {
electricBase.connectDevice(device);
}
}
public class AdapterPatternSocketExample {
public static void main(String[] args) {
// Creating an instance of LegacySocket
LegacySocket legacySocket = new LegacySocket();
// Creating an adapter to make LegacySocket compatible with ElectricBase interface
ElectricBase electricBase = new LegacySocketAdapter(legacySocket);
// Client code can now use the ElectricBase interface
Client client = new Client();
client.connectUsingElectricBase(electricBase, "Laptop");
}
}
Let’s consider a basic example where we have an existing LegacyPrinter
class with a method print(String)
and we want to use it in a new system that expects a Printer
interface with methods printDocument(String)
and scanDocument()
. We’ll create an adapter to make these two interfaces compatible.
In this example, the LegacySocketAdapter
class adapts the LegacySocket
to the ElectricBase
interface. The client code interacts with the ElectricBase
interface, and the adapter takes care of translating the method calls to the underlying LegacySocket
implementation. This allows you to introduce the new system with the ElectricBase
interface without modifying the existing LegacySocket
class.
Example# 2:
1. Define the Legacy Interface:
// Legacy interface
interface LegacyPrinter {
void print(String content);
}
2. Create the Legacy Class:
// Legacy class
class OldPrinter implements LegacyPrinter {
@Override
public void print(String content) {
System.out.println("Printing: " + content);
}
}
3. Define the Target Interface:
// Target interface
interface Printer {
void printDocument(String document);
void scanDocument();
}
4. Create the Adapter Class:
// Adapter class
class PrinterAdapter implements Printer {
private LegacyPrinter legacyPrinter;
public PrinterAdapter(LegacyPrinter legacyPrinter) {
this.legacyPrinter = legacyPrinter;
}
@Override
public void printDocument(String document) {
// Adapt the call to the existing LegacyPrinter interface
legacyPrinter.print(document);
}
@Override
public void scanDocument() {
// Provide a default implementation for scanning
System.out.println("Scanning not supported by LegacyPrinter");
}
}
5. Use the Adapter in the Client Code:
// Client code
public class AdapterPatternExample {
public static void main(String[] args) {
// Use the LegacyPrinter with the new Printer interface
// Create an instance of the LegacyPrinter
LegacyPrinter legacyPrinter = new OldPrinter();
// Create an adapter to make it compatible with the new Printer interface
Printer printerAdapter = new PrinterAdapter(legacyPrinter);
// Use the new Printer interface
printerAdapter.printDocument("Adapter Pattern Example");
printerAdapter.scanDocument();
}
}
In this example:
- The
LegacyPrinter
interface represents the existing interface that we want to adapt. - The
OldPrinter
class is the existing class implementing theLegacyPrinter
interface. - The
Printer
interface is the target interface that our client code expects. - The
PrinterAdapter
class is the adapter that makes theLegacyPrinter
compatible with thePrinter
interface. - In the
AdapterPatternExample
, we create an instance ofOldPrinter
(the legacy class) and then use thePrinterAdapter
to adapt it to the newPrinter
interface.
This allows the client code to use the new Printer
interface seamlessly with the existing LegacyPrinter
implementation, thanks to the adapter pattern.
Advantages of adapter design pattern
The Adapter design pattern in Java, as in other programming languages, offers several advantages:
1. Compatibility and Reusability:
- Integration with Existing Code: Adapters allow you to integrate new classes or systems with existing code that has incompatible interfaces. This promotes the reuse of legacy code and avoids unnecessary modifications.
- Interoperability: Adapters enable interoperability between different systems or components with disparate interfaces, fostering compatibility.
2. Minimizes Code Changes: The Adapter pattern helps in minimizing changes to existing code. Instead of modifying the existing code to match new interfaces, you create adapters that mediate between the old and the new.
3. Easy Maintenance: Adapters encapsulate the changes required to make the systems compatible. This encapsulation makes it easier to maintain and modify the adapters without affecting the rest of the codebase.
4. Promotes Separation of Concerns: The Adapter pattern promotes the separation of concerns by isolating the code responsible for adapting the interfaces. This makes the codebase more modular and easier to understand.
5. Facilitates Legacy System Integration: When dealing with legacy systems or third-party libraries, the Adapter pattern is particularly useful. It allows you to integrate new functionality or systems seamlessly without requiring changes to existing, well-established code.
6. Testability: Adapters can simplify the testing process. Since the adapter encapsulates the translation between interfaces, you can easily create test cases for the adapted interfaces without affecting the underlying implementations.
7. Promotes Design Flexibility: The Adapter pattern promotes design flexibility by allowing different implementations to work together. It accommodates changes in requirements without necessitating extensive modifications to the existing codebase.
8. Enhances Code Readability: By using adapters, the code can be more readable and maintainable. Adapters provide a clear separation between the client code and the adapted classes, making it easier to understand the interactions.
9. Facilitates Dependency Inversion: The Adapter pattern adheres to the Dependency Inversion Principle by allowing high-level modules (client code) to depend on abstractions (common interfaces) rather than on specific implementations. This contributes to a more flexible and scalable design.
10. Enables Design Patterns Composition: Adapters can be combined with other design patterns to address complex design scenarios. For example, you might use an Adapter in conjunction with a Composite or Decorator pattern to achieve specific functionality.
In summary, the Adapter design pattern is a valuable tool for achieving interoperability and promoting flexibility in software design. It is particularly useful in scenarios where existing systems or components need to be integrated with new functionality, and their interfaces are initially incompatible.