Adapter Design Pattern In Java
Adapter Design Pattern: The Art of Making the Incompatible Compatible
Have you ever encountered a scenario where two pieces of your software wouldn’t talk to each other, much like two people who don’t speak the same language?
In this blog, we’ll explore the Adapter Pattern through a lively, conversational exchange between a curious coder and a guiding mentor.
Conversation Starts:
Curious Coder: I encountered one exciting problem, but I must figure out what to do here.
Guiding Mentor: Yes, tell me what is the problem.
Curious Coder: Our image processing application applies different image filters. So far, all of our filters have been in-house filters.
I will show it to you through the example below:
We have our Image class (Internal Image class) and different filters to which we pass this image object. These filters are applied to that image.
Sample Code:
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Image originalImage = new Image();
ImageFilter grayscaleFilter = new GrayscaleFilter();
ImageFilter noiseFilter = new NoiseFilter();
Image grayscaleFilterAppliedImage = grayscaleFilter.applyFilter(originalImage);
Image noiseFilterAndGrayscaleFilterAppliedImage = noiseFilter.applyFilter(grayscaleFilterAppliedImage);
noiseFilterAndGrayscaleFilterAppliedImage.printAllFiltersApplied();
}
}
class Image{
// image class property
Set<String> filtersApplied;
Image(){
filtersApplied = new HashSet<>();
}
void printAllFiltersApplied(){
System.out.println("Following filters are applied on image: ");
filtersApplied.forEach(System.out::println);
}
void addFilterApplied(String filter){
filtersApplied.add(filter);
}
}
interface ImageFilter{
Image applyFilter(Image image);
}
class GrayscaleFilter implements ImageFilter{
@Override
public Image applyFilter(Image image) {
System.out.println("Applying GrayscaleFilter filter on image");
image.addFilterApplied("GrayscaleFilter");
return image;
}
}
class NoiseFilter implements ImageFilter{
@Override
public Image applyFilter(Image image) {
System.out.println("Applying NoiseFilter filter on image");
image.addFilterApplied("NoiseFilter");
return image;
}
}
Output:
Guiding Mentor: Got it. But What is the problem now?
Curious Coder: We also want our application to use external filters, but the problem is that those filters don’t accept the same input type we accept in our internal filters.
Guiding Mentor: Okay, can you change the type of these filters so they accept the same type as we do internally?
Curious Coder: No, that is impossible because it’s a third-party library.
Guiding Mentor: Can we change our internal code to use the external API's image type?
Curious Coder: We can, but that is not feasible because we can’t use the same Image class everywhere. Changing it would be dangerous and break some other piece of code.
Guiding Mentor: Imagine you have a class that accepts this internal image type and returns the internal image without you thinking of how to convert each time and call an external filter.
Curious Coder: How is that possible?
Guiding Mentor: This is where the Adapter design pattern comes into the picture.
It hides all the complexity behind it and looks like it works like other filters, but it handles all the conversion logic behind the scenes.
Real-World Example: The Language Translator
Imagine traveling to a country where you don’t speak the local language and need to communicate with someone who doesn’t understand your language. In this situation, you’d likely rely on a translator who understands both your language and the local language. This translator acts as a bridge, enabling you to communicate effectively despite the language barrier.
In the software world, the Adapter Design Pattern serves a similar purpose. Just as a translator converts your words into a language the other party understands, a software adapter converts data or calls from one interface into a form that another interface can understand. This enables two incompatible systems to work together seamlessly.
Let me show it to you:
Curious Coder: That looks great. Can you please tell me more about the Adapter design pattern?
Guiding Mentor: Yes.
- An adapter design pattern is a structural design pattern.
- It allows incompatible interfaces to collaborate.
- In our example above, we can’t use an external library because it expects a different image type. So, the Adapter acts as a bridge, accepting our Internal image type and converting it into an external one. It then talks to the external library. However, at the high level, we don’t care about that magic happening behind the scenes. We only care that we pass our internal image type and get the output by applying that filter to that image.
- The adapter pattern also allows you to use any number of external libraries that you want, and we can have multiple adapters that will do this magic behind the scenes for us.
Curious Coder: I get you, but why can’t we create a simple class that can convert an internal image to an external one and call the external library directly?
Guiding Mentor: That is a good question, and yes, you can do the same; however, using the Adapter pattern has some benefits:
- Integration with existing systems: It does everything for you to connect to an external library and acts as if it is the same as another library.
- Maintainability and Scalability: As the system evolves, you might need to interact with more such libraries, each expecting a different image type of additional input parameters. The adapter can be extended or modified to accommodate these changes with minimal impact on client code.
- Separation of concerns: The adapter pattern maintains separation of concerns, by isolating the conversion logic with adapters, you keep your system’s business logic decoupled from specific external system components
- Reusability: Adapters can be reused in another piece of code, reducing the duplication of logic.
Curious Coder: Yes, it looks like it’s a great design pattern.
Guiding Mentor:
There are two ways to implement the Adapter pattern:
- Using Inheritance
- Using Composition
We will look at both.
Sample Code using Inheritance:
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Image originalImage = new Image();
ImageFilter grayscaleFilter = new GrayscaleFilter();
ImageFilter noiseFilter = new NoiseFilter();
Image grayscaleFilterAppliedImage = grayscaleFilter.applyFilter(originalImage);
Image noiseFilterAndGrayscaleFilterAppliedImage = noiseFilter.applyFilter(grayscaleFilterAppliedImage);
// Applying external AI filter on image
ExternalAIFilterAdapter externalAIFilterAdapter = new ExternalAIFilterAdapter();
Image
allFiltersAppliedImage
= externalAIFilterAdapter.applyFilter(noiseFilterAndGrayscaleFilterAppliedImage);
allFiltersAppliedImage
.printAllFiltersApplied();
}
}
// Internal Library
class Image{
// image class property
Set<String> filtersApplied;
Image(){
filtersApplied = new HashSet<>();
}
void printAllFiltersApplied(){
System.out.println("Following filters are applied on image: ");
filtersApplied.forEach(System.out::println);
}
void addFilterApplied(String filter){
filtersApplied.add(filter);
}
}
interface ImageFilter{
Image applyFilter(Image image);
}
class GrayscaleFilter implements ImageFilter{
@Override
public Image applyFilter(Image image) {
System.out.println("Applying GrayscaleFilter filter on image");
image.addFilterApplied("GrayscaleFilter");
return image;
}
}
class NoiseFilter implements ImageFilter{
@Override
public Image applyFilter(Image image) {
System.out.println("Applying NoiseFilter filter on image");
image.addFilterApplied("NoiseFilter");
return image;
}
}
class ExternalAIFilterAdapter extends ExternalAIFilter implements ImageFilter{
@Override
public Image applyFilter(Image image) {
System.out.println("Applying ExternalAIFilterAdapter filter on image");
ExternalImage externalImage = convertToExternalImage(image);
ExternalImage aiFilterAppliedExternalImage = super.apply(externalImage);
// Logic to convert external image to internal
covertExternalToInternalImage(image, aiFilterAppliedExternalImage);
image.addFilterApplied("ExternalAIFilter");
return image;
}
private ExternalImage convertToExternalImage(Image image){
// some logic to convert internal image to external
return new ExternalImage();
}
private Image covertExternalToInternalImage(Image image, ExternalImage externalImage){
// some logic to convert external image to internal image
return image;
}
}
// Part of external library
class ExternalImage{
}
class ExternalAIFilter{
public ExternalImage apply(ExternalImage externalImage){
System.out.println("Applying External AI Filter");
return externalImage;
}
}
Output:
Sample Code using Composition:
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Image originalImage = new Image();
ImageFilter grayscaleFilter = new GrayscaleFilter();
ImageFilter noiseFilter = new NoiseFilter();
Image grayscaleFilterAppliedImage = grayscaleFilter.applyFilter(originalImage);
Image noiseFilterAndGrayscaleFilterAppliedImage = noiseFilter.applyFilter(grayscaleFilterAppliedImage);
// Applying external AI filter on image
ExternalAIFilterAdapter externalAIFilterAdapter = new ExternalAIFilterAdapter(new ExternalAIFilter());
Image allFiltersAppliedImage = externalAIFilterAdapter.applyFilter(noiseFilterAndGrayscaleFilterAppliedImage);
allFiltersAppliedImage
.printAllFiltersApplied();
}
}
// Internal Library
class Image{
// image class property
Set<String> filtersApplied;
Image(){
filtersApplied = new HashSet<>();
}
void printAllFiltersApplied(){
System.out.println("Following filters are applied on image: ");
filtersApplied.forEach(System.out::println);
}
void addFilterApplied(String filter){
filtersApplied.add(filter);
}
}
interface ImageFilter{
Image applyFilter(Image image);
}
class GrayscaleFilter implements ImageFilter{
@Override
public Image applyFilter(Image image) {
System.out.println("Applying GrayscaleFilter filter on image");
image.addFilterApplied("GrayscaleFilter");
return image;
}
}
class NoiseFilter implements ImageFilter{
@Override
public Image applyFilter(Image image) {
System.out.println("Applying NoiseFilter filter on image");
image.addFilterApplied("NoiseFilter");
return image;
}
}
class ExternalAIFilterAdapter implements ImageFilter{
ExternalAIFilter externalAIFilter;
ExternalAIFilterAdapter(ExternalAIFilter externalAIFilter){
this.externalAIFilter = externalAIFilter;
}
@Override
public Image applyFilter(Image image) {
System.out.println("Applying ExternalAIFilterAdapter filter on image");
ExternalImage externalImage = convertToExternalImage(image);
ExternalImage aiFilterAppliedExternalImage = externalAIFilter.apply(externalImage);
// Logic to convert external image to internal
covertExternalToInternalImage(image, aiFilterAppliedExternalImage);
image.addFilterApplied("ExternalAIFilter");
return image;
}
private ExternalImage convertToExternalImage(Image image){
// Logic to convert internal image to external
return new ExternalImage();
}
private Image covertExternalToInternalImage(Image image, ExternalImage externalImage){
// some logic to convert external image to internal image
return image;
}
}
// Part of external library
class ExternalImage{
}
class ExternalAIFilter{
public ExternalImage apply(ExternalImage externalImage){
System.out.println("Applying External AI Filter");
return externalImage;
}
}
Output:
Curious Coder: That looks great, hiding all complexity behind the adapter pattern.
But if there are two ways to implement the adapter pattern, which should we use?
Guiding Mentor:
Let’s look at the benefits and drawbacks of using Inheritance:
Benefits:
- Using inheritance is a simple solution.
- Inheritance allows the adapter to override or extend the functionalities of the base class.
Drawbacks:
- Inheritance creates a tight coupling between the adapter and the adapted class.
- Multiple inheritance limitation
Now, let’s look at the benefits and drawbacks of Composition:
Benefits:
- Composition is more flexible than inheritance.
- It allows you to inject and change object type at runtime.
- Composition promotes loose coupling.
Drawbacks:
- It might lead to complex code structures, requiring explicitly defining and managing relationships between objects.
- An additional layer of indirection is introduced by composition.
In short,
Choosing between inheritance and composition for implementing the Adapter pattern depends on various factors, including language capabilities, the specific requirements of your project, and the principles you prioritize in your design. Composition is generally preferred in modern software design for its flexibility and alignment with SOLID principles, but there are scenarios where inheritance might offer a simple and more efficient solution.
Curious Coder: Great. Thank you so much for the detailed explanation.
Guiding Mentor: You are welcome.
Conversation Ends. 😊
I hope you enjoyed this article.
Thank you for dedicating a precious time to reading this blog post! 💖 🕒