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.
  • This pattern should be used carefully. It can make bugs appear as a normal program execution.
  • We should not implement this pattern just to avoid null checks and make the code more readable. Actually, it is harder to read code that is moved to another place, like the null object class.
  • We have to perform additional testing to make sure that there is nowhere we have to assign a null instead of the null object.

Let’s take a look at an example to better understand this pattern.

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 org.trishinfotech.adapter;
public interface Shape {
      void draw();
      void resize();
      String description();
      boolean isHide();
}

Below is the code of the concrete class, Rectangle:

package org.trishinfotech.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 org.trishinfotech.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 org.trishinfotech.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 org.trishinfotech.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 org.trishinfotech.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 Rhombusclass:

package org.trishinfotech.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?

  1. We change our code and we change/remove our Shape interface and start using the  GeometricShape interface. Or, we can convert the GeometricShape interface into our  Shape interface, if its open source and changes are minimal. But, it’s not always possible because of other functionality and code dependency.
  2. 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. The Drawing class is the client in our example.

Adapter Design Pattern Implementation

We have two different approaches to implement the adapter pattern.

1st Approach – 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 org.trishinfotech.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 org.trishinfotech.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.

2nd Approach – 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 org.trishinfotech.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 org.trishinfotech.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 org.trishinfotech.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 org.trishinfotech.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 org.trishinfotech.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 org.trishinfotech.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!

Source Code can be found here: Adapter-Design-Pattern-Sample-Code

Leave a comment