An architectural pattern called the Adapter Design Pattern makes it possible for interfaces that are incompatible to cooperate. An adapter pattern changes a class's interface to one that clients are used to seeing. Through the use of an intermediary Adapter class, it permits a client with an incompatible interface to communicate with the current class. Without changing the original code, it serves as a bridge to connect two incompatible interfaces. When integrating pre-existing systems or libraries with various interfaces, this paradigm is quite helpful.
In this tutorial, we will explore the Adapter Design Pattern in Java, discussing its structure, implementation, best practices, and advantages.
Advantages of Adapter Pattern
- Integration of Incompatible Interfaces : One of the key benefits of utilizing the Adapter pattern is its capacity to facilitate the integration of pre-existing components or systems that have incompatible interfaces. It functions as an intermediary, facilitating the smooth integration of these elements.
- Code Reusability : The Adapter pattern promotes code reusability by allowing the repurposing of existing components or classes with potentially incompatible interfaces. Instead of modifying the current code, adapters can be developed to ensure compatibility with new requirements.
- Interoperability : The Adapter pattern contributes to interoperability, enabling systems or components developed by different teams or organizations to work together seamlessly. It promotes the fusion of various technologies, ensuring their smooth operation in conjunction.
- Supports Legacy Systems : The Adapter pattern proves highly beneficial in dealing with legacy systems resistant to straightforward modifications. Developing adapters for legacy components allows the incorporation of new functionality without the need for alterations to the existing codebase.
- Flexibility and Maintainability : Adapters provide a flexible and easily maintainable solution for handling modifications or enhancements to the system. Adapters can be modified or new ones generated in response to interface changes in existing components without requiring alterations to the client code, enhancing system maintainability.
- Minimizes Impact on Existing Code : The Adapter pattern reduces the impact on existing code when integrating new components or systems. Adapters handle the translation to the adaptee's interface, preventing any changes to the client code while facilitating interaction with the target interface.
- Encapsulation of Adaptee Complexity : The process of encapsulating the complexity of the adaptee involves isolating the client code from the intricate workings of the underlying implementation. This encapsulation promotes a distinct segregation of concerns and streamlines the client interaction with the modified interface.
- Facilitates Third-Party Component Integration : The Adapter pattern presents a standardized approach to ensure the compatibility of third-party components or libraries with the existing system. This streamlines the integration process, enhancing the system's extensibility.
When we should use Adapter pattern
- When client wants to interact with existing system using incompatible interface.
- When a new system wants to interact with legacy(old) system using new interface which is not compatible with interface of legacy system.
- When you want to use a 3rd party framework or library whose interface is incompatible with your system.
Structure of the Adapter Design Pattern
- Target : The interface sought by the client code is referred to as the "target". It represents the desired user interface that the client will utilize.
- Adaptee : The existing class or interface, incompatible with the target interface, is termed the "adaptee" or "source." The adaptee cannot be directly employed by the client code.
- Adapter : Serving as a bridge between the adaptee and the target, the adapter class contains an instance of the adaptee and implements the target interface. Calls to the target interface are interpreted by the adapter, translating them into equivalent calls on the adaptee.
- Client : Utilizing the adapter, the client code communicates with the target interface. The client exclusively engages with the target interface and remains unaware of the adaptee's interface.
- Client calls Adapter using using Target Interface.
- Adapter class translates this request and delegates it to adaptee using one/multiple method calls using Adaptee Interface.
- Adaptee returns response to Adapter class as defined in Adaptee Interface and then Adapter transforms this response before returning it to Client as defined in Target Interface.
We will declare two incompatible interfaces Square and Rectangle.
Square.javapublic interface Square { public void setSide(int sideLength); public void printAreaOfSquare(); }Rectangle.java
public interface Rectangle { public void setLength(int length); public void setWidth(int width); public void printAreaOfRectangle(); }
Create Chessboard.java and Tenniscourt.java implementing Square and Rectangle interfaces respectively.
Chessboard.javapublic class Chessboard implements Square { int sideLength; @Override public void setSide(int sideLength){ this.sideLength = sideLength; } @Override public void printAreaOfSquare(){ System.out.println("Area of Chessbpard is " + sideLength*sideLength); } }Tenniscourt.java
public class Tenniscourt implements Rectangle { int length, width; @Override public void setLength(int length){ this.length = length; } @Override public void setWidth(int width){ this.width = width; } @Override public void printAreaOfRectangle(){ System.out.println("Area of Tennis Court is " + length*width); } }
Now, we will define RectangleAdapter.java which is a wrapper over Rectangle interface. It implements Square interface over Rectangle Interface.
RectangleAdapter.javapublic class RectangleAdapter implements Square { Rectangle rect; public RectangleAdapter(Rectangle rect) { this.rect = rect; } // Setting length and width to same value to make it a square @Override public void setSide(int sideLength){ rect.setLength(sideLength); rect.setWidth(sideLength); } @Override public void printAreaOfSquare(){ rect.printAreaOfRectangle(); } }
Create AdapterPatternExample.java class to show the use of RectangleAdapter to call an Rectangle object using Square interface.
AdapterPatternExample.javapublic class AdapterPatternExample { public static void main(String args[]){ Square square = new Chessboard(); Rectangle rectangle = new Tenniscourt(); Square adapter = new RectangleAdapter(rectangle); // Calculate Area of Square using Square Interface square.setSide(5); square.printAreaOfSquare(); // Calculate Area of Rectangle using Rectangle Interface rectangle.setLength(5); rectangle.setWidth(4); rectangle.printAreaOfRectangle(); // Now we will call Rectangle object using Square interface adapter.setSide(5); adapter.printAreaOfSquare(); } }
Output
Area of Chessbpard is 25 Area of Tennis Court is 20 Area of Tennis Court is 20
- Adapter class changes the interface of an existing object.
- Adapter class is a good example of object composition. Adapter class "has a" instance of the adaptee class.
- We can use an Adapter with any class Implementing Adaptee Interface.
- Adapter wraps an object to change it's interface whereas a decorator wraps an object to add extra functionalities.
Best Practices of Adapter Pattern
- Clearly identify the target interface (expected by the client) and the adaptee interface (existing but incompatible). Understanding the differences between these interfaces is crucial for designing a successful adapter.
- Ensure that the adapter implements the target interface. This allows the client code to interact with the adapter through the expected interface, providing a seamless integration.
- Prefer composition over inheritance when implementing the adapter. Use an instance of the adaptee within the adapter class rather than extending the adaptee class. This promotes flexibility and avoids issues related to multiple inheritance.
- If the data types or formats used by the target and adaptee interfaces are different, handle the necessary data conversion within the adapter. Ensure that the data passed between the client, adapter, and adaptee is compatible.
- Maintain consistent naming conventions between the target and adaptee interfaces to enhance code readability. Choose names for methods in the adapter that reflect the intent and purpose of the original methods in the adaptee.
- Delegate the calls from the adapter's methods to the corresponding methods of the adaptee. This ensures that the behavior of the adaptee is preserved, and the adapter acts as a bridge between the client and the adaptee.
- If there are multiple adaptees or variations, consider creating multiple adapters for different scenarios. This allows for a more modular and maintainable approach, with each adapter focused on a specific use case.
- Design the adapter to follow the Open-Closed Principle, allowing for future extensions without modifying existing code. This ensures that new adaptees or variations can be accommodated without altering the client code or existing adapters.
- Keep the adapter simple and focused on its primary task of making the adaptee compatible with the target interface. Avoid adding unnecessary complexity or additional functionalities that are not directly related to the adaptation process.
Interpreter Design Pattern |
State Design Pattern |
Singleton Design Pattern |
Factory Design Pattern |
Mediator Design Pattern |
List of Design Patterns |