Using the Adapter Design Pattern in Java
Here I am with another useful design pattern for you — the adapter design pattern. I will also highlight the differences between the decorator design pattern (see my previous article, Decorator Design Pattern in Java, here) and the adapter design pattern.
Adapter Design Pattern
- The adapter design pattern is a structural design pattern that allows two unrelated/uncommon interfaces to work together. In other words, the adapter pattern makes two incompatible interfaces compatible without changing their existing code.
- Interfaces may be incompatible, but the inner functionality should match the requirement.
- The adapter pattern is often used to make existing classes work with others without modifying their source code.
- Adapter patterns use a single class (the adapter class) to join functionalities of independent or incompatible interfaces/classes.
- The adapter pattern also is known as the wrapper, an alternative naming shared with the decorator design pattern.
- This pattern converts the (incompatible) interface of a class (the adaptee) into another interface (the target) that clients require.
- The adapter pattern also lets classes work together, which, otherwise, couldn't have worked, because of the incompatible interfaces.
- For example, let's take a look at a person traveling in different countries with their laptop and mobile devices. We have a different electric socket, volt, and frequency measured in different countries and that makes the use of any appliance of one country to be freely used in a different country. In the United Kingdom, we use Type G socket with 230 volts and 50 Hz frequency. In the United States, we use Type A and Type B sockets with 120 volts and 60 Hz frequency. In India, we use Type C, Type D. and Type M sockets with 230 volts and 50 Hz. lastly, in Japan, we use Type A and Type B sockets with 110 volts and 50 Hz frequency. This makes the appliances we carry incompatible with the electric specifications we have at different places.
- This makes the adapter tool essential because it can make/convert incompatible code into compatible code. Please notice here that we have not achieved anything additional here — there is no additional functionality, only compatibility.
To better understand this, let's look at an example of geometric shapes. I am keeping the example relatively simple to keep the focus on the pattern. Suppose we have a project of drawing, in which we are required to develop different kinds of geometric shapes that will be used in the Drawing
via a common interface called Shape
.
Below is the code of theShape
interface:
package design.adapter;
public interface Shape {
void draw();
void resize();
String description();
boolean isHide();
}
Below is the code of the concrete class, Rectangle
:
package design.adapter;
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Drawing Rectangle");
}
@Override
public void resize() {
System.out.println("Resizing Rectangle");
}
@Override
public String description() {
return "Rectangle object";
}
@Override
public boolean isHide() {
return false;
}
}
Below is the code of the concrete class, Circle
:
package design.adapter;
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing Circle");
}
@Override
public void resize() {
System.out.println("Resizing Circle");
}
@Override
public String description() {
return "Circle object";
}
@Override
public boolean isHide() {
return false;
}
}
Below is the code of the Drawing
class:
package design.adapter;
import java.util.ArrayList;
import java.util.List;
public class Drawing {
List<Shape> shapes = new ArrayList<Shape>();
public Drawing() {
super();
}
public void addShape(Shape shape) {
shapes.add(shape);
}
public List<Shape> getShapes() {
return new ArrayList<Shape>(shapes);
}
public void draw() {
if (shapes.isEmpty()) {
System.out.println("Nothing to draw!");
} else {
shapes.stream().forEach(shape -> shape.draw());
}
}
public void resize() {
if (shapes.isEmpty()) {
System.out.println("Nothing to resize!");
} else {
shapes.stream().forEach(shape -> shape.resize());
}
}
}
Below is the code of the Main
class to execute and test the Drawing
.
package design.adapter;
public class Main {
public static void main(String[] args) {
System.out.println("Creating drawing of shapes...");
Drawing drawing = new Drawing();
drawing.addShape(new Rectangle());
drawing.addShape(new Circle());
System.out.println("Drawing...");
drawing.draw();
System.out.println("Resizing...");
drawing.resize();
}
}
Below is the output of the program:
Creating drawing of shapes...
Drawing...
Drawing Rectangle
Drawing Circle
Resizing...
Resizing Rectangle
Resizing Circle
So far, so good. As we progress, we come to know that there are some extra geometric shapes that are already developed either by some other team within our organization. Or, we have a third-party API, which is available to us. Below are the classes ready to use.
Below is the code of the GeometricShape
interface:
package design.adapter.extra;
// Part of Extra-Geometric-Shape API
public interface GeometricShape {
double area();
double perimeter();
void drawShape();
}
Below is the code of the concrete Triangle
class:
package design.adapter.extra;
// Part of Extra-Geometric-Shape API
public class Triangle implements GeometricShape {
// sides
private final double a;
private final double b;
private final double c;
public Triangle() {
this(1.0d, 1.0d, 1.0d);
}
public Triangle(double a, double b, double c) {
this.a = a;
this.b = b;
this.c = c;
}
@Override
public double area() {
// Heron's formula:
// Area = SquareRoot(s * (s - a) * (s - b) * (s - c))
// where s = (a + b + c) / 2, or 1/2 of the perimeter of the triangle
double s = (a + b + c) / 2;
return Math.sqrt(s * (s - a) * (s - b) * (s - c));
}
@Override
public double perimeter() {
// P = a + b + c
return a + b + c;
}
@Override
public void drawShape() {
System.out.println("Drawing Triangle with area: " + area() + " and perimeter: " + perimeter());
}
}
Below is the code of the concrete Rhombus
class:
package design.adapter.extra;
// Part of Extra-Geometric-Shape API
public class Rhombus implements GeometricShape {
// sides
private final double a;
private final double b;
public Rhombus() {
this(1.0d, 1.0d);
}
public Rhombus(double a, double b) {
this.a = a;
this.b = b;
}
@Override
public double area() {
double s = a * b;
return s;
}
@Override
public double perimeter() {
return 2 * (a + b);
}
@Override
public void drawShape() {
System.out.println("Drawing Rhombus with area: " + area() + " and perimeter: " + perimeter());
}
}
Since these are done via other teams or organizations, there is a very high chance that they will be using their own specifications. All of these ready-to-use geometric shapes are not implementing our Shape
interface. Obviously, we can see that Triangle
and Rhombus
are implementing the GeometricShape
interface. And, the GeometricShape
interface is different from our Shape
interface (incompatible).
OurDrawing
client class can work only withShape
and not GeometricShape
. This makesGeometricShape
incompatible with ourDrawing
class. See the addShape() and getShapes() method of Drawing class:
public void addShape(Shape shape) {
shapes.add(shape);
}
public List<Shape> getShapes() {
return new ArrayList<Shape>(shapes);
}
This means that we have some ready-to-use code that is very similar to what we were expecting, but it is not according to our coding specifications — just like electric specifications of different countries.
Now, What Should We Do?
- We change our code and we change/remove our
Shape
interface and start using theGeometricShape
interface. Or, we can convert theGeometricShape
interface into ourShape
interface, if its open source and changes are minimal. But, it's not always possible because of other functionality and code dependency. - Continuing with what we are coding, should we not use the ready-to-use code/APIs?
No. Actually, all we need to have here is an adapter, which makes this ready-to-use code compatible with our code and the Drawing
in this example.
Now, when we are clear on why we need the adapter, let's take a closer look at what the adapter actually does. Before we start, below is the list of classes/objects used in the adapter pattern:
- Target — This defines the domain-specific interface that the client uses. This is the
Shape
interface in our example. - Adapter — This adapts the interface from the adaptee to the target interface. I will point the adapter classes based on the different approach below.
- Adaptee — This defines an existing interface that needs adapting. This is the
GeometricShape
interface in our example. - Client — This collaborates with objects conforming to the
Target
interface. TheDrawing
class is the client in our example.
Adapter Design Pattern Implementation
We have two different approaches to implement the adapter pattern.
Object Adapter Pattern
In this approach, we will use the Java composition, and our adapter contains the source object. The composition is used as a reference to the wrapped class within the adapter. In this approach, we create an adapter class that implements the target ( Shape
in this case) and references the adaptee — GeometricShape
in this case. We implement all of the required methods of the target (Shape
) and do the necessary conversion to fulfill our requirement.
Below is the code of the GeometricShapeObjectAdapter
:
package design.adapter;
import design.adapter.extra.GeometricShape;
import design.adapter.extra.Rhombus;
import design.adapter.extra.Triangle;
public class GeometricShapeObjectAdapter implements Shape {
private GeometricShape adaptee;
public GeometricShapeObjectAdapter(GeometricShape adaptee) {
super();
this.adaptee = adaptee;
}
@Override
public void draw() {
adaptee.drawShape();
}
@Override
public void resize() {
System.out.println(description() + " can't be resized. Please create new one with required values.");
}
@Override
public String description() {
if (adaptee instanceof Triangle) {
return "Triangle object";
} else if (adaptee instanceof Rhombus) {
return "Rhombus object";
} else {
return "Unknown object";
}
}
@Override
public boolean isHide() {
return false;
}
}
Now, below is the ObjectAdapterMain
class to execute and test our object adapter pattern:
package design.adapter;
import design.adapter.extra.Rhombus;
import design.adapter.extra.Triangle;
public class ObjectAdapterMain {
public static void main(String[] args) {
System.out.println("Creating drawing of shapes...");
Drawing drawing = new Drawing();
drawing.addShape(new Rectangle());
drawing.addShape(new Circle());
drawing.addShape(new GeometricShapeObjectAdapter(new Triangle()));
drawing.addShape(new GeometricShapeObjectAdapter(new Rhombus()));
System.out.println("Drawing...");
drawing.draw();
System.out.println("Resizing...");
drawing.resize();
}
}
Below is the output of the program:
Creating drawing of shapes...
Drawing...
Drawing Rectangle
Drawing Circle
Drawing Triangle with area: 0.4330127018922193 and perimeter: 3.0
Drawing Rhombus with area: 1.0 and perimeter: 4.0
Resizing...
Resizing Rectangle
Resizing Circle
Triangle object can't be resized. Please create new one with required values.
Rhombus object can't be resized. Please create new one with required values.
Class Adapter Pattern
In this approach, we use the Java Inheritance
and extend the source class. So, for this approach, we have to create separate adapters for the Triangle
and Rhombus
classes, as shown below:
Below is the code of the TriangleAdapter
:
package design.adapter;
import design.adapter.extra.Triangle;
public class TriangleAdapter extends Triangle implements Shape {
public TriangleAdapter() {
super();
}
@Override
public void draw() {
this.drawShape();
}
@Override
public void resize() {
System.out.println("Triangle can't be resized. Please create new one with required values.");
}
@Override
public String description() {
return "Triangle object";
}
@Override
public boolean isHide() {
return false;
}
}
Below is the code of the RhombusAdapter
:
package design.adapter;
import design.adapter.extra.Rhombus;
public class RhombusAdapter extends Rhombus implements Shape {
public RhombusAdapter() {
super();
}
@Override
public void draw() {
this.drawShape();
}
@Override
public void resize() {
System.out.println("Rhombus can't be resized. Please create new one with required values.");
}
@Override
public String description() {
return "Rhombus object";
}
@Override
public boolean isHide() {
return false;
}
}
Now, below is the ClassAdapterMain
class to execute and test the object adapter pattern:
package design.adapter;
public class ClassAdapterMain {
public static void main(String[] args) {
System.out.println("Creating drawing of shapes...");
Drawing drawing = new Drawing();
drawing.addShape(new Rectangle());
drawing.addShape(new Circle());
drawing.addShape(new TriangleAdapter());
drawing.addShape(new RhombusAdapter());
System.out.println("Drawing...");
drawing.draw();
System.out.println("Resizing...");
drawing.resize();
}
}
Below is the output of the program:
Creating drawing of shapes...
Drawing...
Drawing Rectangle
Drawing Circle
Drawing Triangle with area: 0.4330127018922193 and perimeter: 3.0
Drawing Rhombus with area: 1.0 and perimeter: 4.0
Resizing...
Resizing Rectangle
Resizing Circle
Triangle can't be resized. Please create new one with required values.
Rhombus can't be resized. Please create new one with required values.
Both approaches have the same output. But:
- Class adapters use inheritance and can wrap a class only. I can't wrap an interface since, by definition, it must be derived from some base class.
- Object adapters use the composition and can wrap classes as well as interfaces. It contains a reference to the class or interfaces object instance. The object adapter is the easier one and can be applied in most of the scenarios.
We can also create the adapter by implementing the target (Shape
) and the adaptee (GeometricShape
). That approach is known as the two ways adapter.
Two Ways Adapter
The two-ways adapters are adapters that implement both interfaces of the target and adaptee. The adapted object can be used as the target in new systems dealing with target classes or as the adaptee in other systems dealing with the adaptee classes. The use of the two ways adapter is bit rare, and I never get a chance to write such an adapter in a project. But, the provided code below explores the possible implementation of the two ways adapter.
Below is the code of the ShapeType
enum for various types of shape objects:
package design.adapter;
public enum ShapeType {
CIRCLE,
RECTANGLE,
TRIANGLE,
RHOMBUS
}
Below is the code for the TwoWaysAdapter
, which can serve as the Triangle
, Rhombus
, Circle
, or Rectangle
.
package design.adapter;
import design.adapter.extra.GeometricShape;
import design.adapter.extra.Rhombus;
import design.adapter.extra.Triangle;
public class TwoWaysAdapter implements Shape, GeometricShape {
// sides
private ShapeType shapeType;
public TwoWaysAdapter() {
this(ShapeType.TRIANGLE);
}
public TwoWaysAdapter(ShapeType shapeType) {
super();
this.shapeType = shapeType;
}
@Override
public void draw() {
switch (shapeType) {
case CIRCLE:
new Circle().draw();
break;
case RECTANGLE:
new Rectangle().draw();
break;
case TRIANGLE:
new Triangle().drawShape();
break;
case RHOMBUS:
new Rhombus().drawShape();
break;
}
}
@Override
public void resize() {
switch (shapeType) {
case CIRCLE:
new Circle().resize();
break;
case RECTANGLE:
new Rectangle().resize();
break;
case TRIANGLE:
System.out.println("Triangle can't be resized. Please create new one with required values.");
break;
case RHOMBUS:
System.out.println("Rhombus can't be resized. Please create new one with required values.");
break;
}
}
@Override
public String description() {
switch (shapeType) {
case CIRCLE:
return new Circle().description();
case RECTANGLE:
return new Rectangle().description();
case TRIANGLE:
return "Triangle object";
case RHOMBUS:
return "Rhombus object";
}
return "Unknown object";
}
@Override
public boolean isHide() {
return false;
}
@Override
public double area() {
switch (shapeType) {
case CIRCLE:
case RECTANGLE:
return 0.0d;
case TRIANGLE:
return new Triangle().area();
case RHOMBUS:
return new Rhombus().area();
}
return 0.0d;
}
@Override
public double perimeter() {
switch (shapeType) {
case CIRCLE:
case RECTANGLE:
return 0.0d;
case TRIANGLE:
return new Triangle().perimeter();
case RHOMBUS:
return new Rhombus().perimeter();
}
return 0.0d;
}
@Override
public void drawShape() {
draw();
}
}
Now, below is the TwoWaysAdapterMain
class to execute and test the object adapter pattern:
package design.adapter;
public class TwoWaysAdapterMain {
public static void main(String[] args) {
System.out.println("Creating drawing of shapes...");
Drawing drawing = new Drawing();
drawing.addShape(new TwoWaysAdapter(ShapeType.RECTANGLE));
drawing.addShape(new TwoWaysAdapter(ShapeType.CIRCLE));
drawing.addShape(new TwoWaysAdapter(ShapeType.TRIANGLE));
drawing.addShape(new TwoWaysAdapter(ShapeType.RHOMBUS));
System.out.println("Drawing...");
drawing.draw();
System.out.println("Resizing...");
drawing.resize();
}
}
Below is the output of the program:
Creating drawing of shapes...
Drawing...
Drawing Rectangle
Drawing Circle
Drawing Triangle with area: 0.4330127018922193 and perimeter: 3.0
Drawing Rhombus with area: 1.0 and perimeter: 4.0
Resizing...
Resizing Rectangle
Resizing Circle
Triangle can't be resized. Please create new one with required values.
Rhombus can't be resized. Please create new one with required values.
Here, we have same output because we are using TwoWaysAdapter into our Drawing client in the same way. The only difference here is that by using TwoWaysAdapter, our Shape interface can also be use with the extra geometric shapes APIs Client class. So Shape and GeometricShape both can be use interchangeably.
Adapter vs. Decorator Design Pattern:
Here, are some key points to distinguise between Adapter and Decorator Pattern (see more in my article Decorator Design Pattern in Java).
Adapter Pattern:
Makes a wrapper (Adapter) to create compatibility/conversion from one interface to the other interface which are incompatible.
Wrapper (Adapter) works on two incompatible interfaces/classes.
The intenstion of writing the wrapper class is to resolve the differences and make the interfaces compatible.
We rarely add any functionality in the wrapper class.
Decorator Pattern:
Makes a wrapper (Decorator) to add/modify functionalities in the interface/class without changing the original code of the class. We use Abstract wrapper to implement this pattern, in general.
Wrapper (Decorator) works on single interface/class.
The intension of writing the wrapper class is to add/modify functionalities of the interface/class.
There is no incompatibility issue since we deal only with one interface/classes at a time.
Liked the article? Don't forget to press that like button. Happy coding!
Need more articles on Design Patterns? Below are some of them I have shared with you.
- Null Object Pattern in Java
Using the Adapter Design Pattern in Java
Using the Bridge Design Pattern in Java
Strategy vs. Factory Design Patterns in Java
Decorator Design Pattern in Java
How to Use Singleton Design Pattern in Java
Singleton Design Pattern: Making Singleton More Effective in Java
Some additional Articles:
Java Enums: How to Make Enums More Useful
Java Enums: How to Use Configurable Sorting Fields