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

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 Rhombusclass:

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?

  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:

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:

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:

Decorator Pattern:

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.

Some additional Articles:

 

 

 

 

Top