Adapter Design Pattern

Definition

The Adapter Design Pattern converts the interface of a class into another interface the clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.

Introduction

The Adapter Design Pattern adapts an interface to look like a different interface. When we expect a particular interface but have an object that implements a certain other interface, we can use the adapter design pattern to make the object appear as if it is implementing the first interface. Thus, we can use the object where the first interface is expected.

A Random Access Collection

We have defined our own collection interface. It allows to add an element to the end and get an element from an index. The getElement throws an IllegalArgumentException if the passed index is invalid. It also has a method to get the size of the collection.

package com.javadevcentral.designpatterns.adapter;

public interface RandomAccessCollection<T> {

    /**
     * Adds an element to the collection.
     * @param element The element to be added.
     */
    void add(T element);

    /**
     * Gets the element at an index.
     *
     * @param index The index.
     * @return the element at index passed.
     * @throws IllegalArgumentException if the passed index is not valid.
     */
    T getElement(int index);

    /**
     * Returns the number of elements in the collection.
     * @return the size of the collection
     */
    int size();
}

A simple implementation of this uses an ArrayList as the backing collection.

package com.javadevcentral.designpatterns.adapter;

import java.util.ArrayList;
import java.util.List;

public class RandomAccessCollectionImpl<T> implements RandomAccessCollection<T> {
    private List<T> list = new ArrayList<>();

    @Override
    public void add(T element) {
        list.add(element);
    }

    @Override
    public T getElement(int index) {
        if (index < 0 || index >= size()) {
            throw new IllegalArgumentException("Invalid index");
        }
        return list.get(index);
    }

    @Override
    public int size() {
        return list.size();
    }
}

We explicitly check the bound for the index to check its validity. If it is invalid, we throw an IllegalArgumentException.

A Special collection

We have defined another similar interface; lets call it a SpecialCollection. Apart from the methods available in the RandomAccessCollection, it allows to get the first and the last element of the collection. We throw a NoSuchElementException if these methods are called on an empty collection.

package com.javadevcentral.designpatterns.adapter;

import java.util.NoSuchElementException;

public interface SpecialCollection<T> {
    /**
     * Adds an element to the collection.
     * @param element The element to be added.
     */
    void add(T element);

    /**
     * Gets the element at an index.
     *
     * @param index The index.
     * @return the element at index passed.
     * @throws IllegalArgumentException if the passed index is not valid.
     */
    T getElement(int index);

    /**
     * Returns the number of elements in the collection.
     * @return the size of the collection
     */
    int size();

    /**
     * Gets the first element of the collection.
     * @return the first element.
     * @throws NoSuchElementException if the collection is empty.
     */
    T getFirstElement();

    /**
     * Gets the last element of the collection.
     * @return the last element.
     * @throws NoSuchElementException if the collection is empty.
     */
    T getLastElement();
}
package com.javadevcentral.designpatterns.adapter;

import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;

public class SpecialCollectionImpl<T> implements SpecialCollection<T> {
    private List<T> list = new ArrayList<>();

    @Override
    public void add(T element) {
        list.add(element);
    }

    @Override
    public T getElement(int index) {
        if (index < 0 || index >= size()) {
            throw new IllegalArgumentException("Invalid index");
        }
        return list.get(index);
    }

    @Override
    public int size() {
        return list.size();
    }

    @Override
    public T getFirstElement() {
        checkListIsNonEmpty();
        return list.get(0);
    }

    @Override
    public T getLastElement() {
        checkListIsNonEmpty();
        return list.get(list.size() - 1);
    }

    private void checkListIsNonEmpty() {
        if (list.isEmpty()) {
            throw new NoSuchElementException("The collection is empty");
        }
    }
}

We use an ArrayList again to implement this. The add, getElement and the size implementations are similar to what we have seen before for the RandomAccessCollection.

For the getFirstElement and getLastElement, we have an explicit check to ensure that the collection is not empty. If the collection is empty, we throw a NoSuchElementException. This is because calling list.get(..) by passing an invalid index (out of bounds) throws an IndexOutOfBoundsException

Adapting a Random Access Collection as a Special Collection

Consider the following scenario. There are parts of application or code base that uses RandomAccessCollection and some parts use SpecialCollection. Let us say that SpecialCollection is newer and written recently whereas RandomAccessCollection is part of the older parts of the code base. We have a RandomAccessCollection object, we want to pass it to methods or classes that operate on a SpecialCollection i.e., methods or classes written newly. In other words, we want to adapt RandomAccessCollection to a SpecialCollection. Thus, we want to operate on a RandomAccessCollection as if we are operating on a SpecialCollection.

The Adapter Design Pattern

The Adapter Design Pattern makes objects interfaces look like something that they are not. We can adapt an object implementing an interface and pass it to places where it expects a different interface.

Mechanics

To do this, we create a new class (the adapter) that implements the target interface. The target interface is the type that we are adapting something to. In our example it is the SpecialCollection. The adapter holds a reference to the type we are adapting. This is also called as the adaptee interface and in our case it is RandomAccessCollection.

Implementation

package com.javadevcentral.designpatterns.adapter.impl;

import com.javadevcentral.designpatterns.adapter.RandomAccessCollection;
import com.javadevcentral.designpatterns.adapter.SpecialCollection;

import java.util.NoSuchElementException;

public class RandomAccessCollectionAdapter<T> implements SpecialCollection<T> {

    private final RandomAccessCollection<T> randomAccessCollection;

    public RandomAccessCollectionAdapter(RandomAccessCollection<T> randomAccessCollection) {
        this.randomAccessCollection = randomAccessCollection;
    }

    @Override
    public void add(T element) {
        randomAccessCollection.add(element);
    }

    @Override
    public T getElement(int index) {
        return randomAccessCollection.getElement(index);
    }

    @Override
    public int size() {
        return randomAccessCollection.size();
    }

    @Override
    public T getFirstElement() {
        checkUnderlyingCollectionIsNonEmpty();
        return randomAccessCollection.getElement(0);
    }

    @Override
    public T getLastElement() {
        checkUnderlyingCollectionIsNonEmpty();
        int lastElementIndex = size() - 1;
        return randomAccessCollection.getElement(lastElementIndex);
    }

    private void checkUnderlyingCollectionIsNonEmpty() {
        if (randomAccessCollection.size() == 0) {
            throw new NoSuchElementException("The collection is empty");
        }
    }
}

The RandomAccessCollectionAdapter implements SpecialCollection, the target interface. It is composed with an instance of RandomAccessCollection. Calling the get, getElement and size methods delegates to the adaptee. But the adaptee interface does not support getFirstElement and getLastElement calls. Hence, it is the job of the adapter to call the right method(s) (adapt) on the adaptee. 

Here, we find the right index and call getElement by passing the index. Since, getElement ofRandomAccessCollection throws an IllegalArgumentException on receiving an index that is out-of-bound, we perform the index validation in the adapter itself and throw a NoSuchElementException if the collection is empty (as documented for getFirstElement and getLastElement).

Flow

Since the type of the adapter is same as the target inteface, the client does not know if it is using a real class that implements the interface or using an adapter. Yes, even the adapter implements the target interface. But it does not actually provide the functionality. It just has the logic to adapt the adaptee to the concrete interface.

  1. The client having the adapter object calls a method on the target interface.
  2. The adapter converts the call/request on the target interface to one or more calls on the adapter.
  3. Client gets the result. The client is unaware of the adapter and also the involvement of the adaptee. The adaptee too is not coupled to the client.

Client code

package com.javadevcentral.designpatterns.adapter;

import com.javadevcentral.designpatterns.adapter.impl.RandomAccessCollectionAdapter;

public class AdapterClient {
    public static void main(String[] args) {
        //Collection 1 (part I)
        RandomAccessCollection<String> randomAccessCollection = new RandomAccessCollectionImpl<>();
        randomAccessCollection.add("one");
        randomAccessCollection.add("two");
        randomAccessCollection.add("three");

        System.out.println(randomAccessCollection.size()); //3
        System.out.println(randomAccessCollection.getElement(0)); //one
        System.out.println(randomAccessCollection.getElement(1)); //two
        System.out.println(randomAccessCollection.getElement(2)); //three

        //Collection 2 (part II)
        SpecialCollection<String> stringCollection = new SpecialCollectionImpl<>();
        stringCollection.add("string-one");
        stringCollection.add("string-two");
        stringCollection.add("string-three");

        System.out.println(stringCollection.size()); //3
        System.out.println(stringCollection.getElement(0)); //string-one
        System.out.println(stringCollection.getFirstElement()); //string-one
        System.out.println(stringCollection.getLastElement()); //string-three

        //Adapt (part III)
        SpecialCollection<String> newStringCollection = new RandomAccessCollectionAdapter<>(randomAccessCollection);

        System.out.println(newStringCollection.size()); //3
        System.out.println(newStringCollection.getElement(0)); //one
        System.out.println(newStringCollection.getElement(1)); //two
        System.out.println(newStringCollection.getFirstElement());//one
        System.out.println(newStringCollection.getLastElement());//three
    }
}

Looking at the third part, we are able to use a RandomAccessCollection as if it were a SpecialCollection. Thus, we have adapted a RandomAccessCollection to a SpecialCollection using the Adapter Design Pattern.

Object and class adapter

The version of the adapter pattern we saw above is called the object adapter. There is a variation of this called the class adapter. The adapter instead of composing an instance of adaptee, extends it. Thus, it uses inheritance instead of composition.

Advantages of class adapter:

  • Since we are inheriting, adding a new method to the adaptee interface needs no change to the adapter. 
  • Easily override a method of the concrete adaptee (if needed).

Disadvantage of class adapter:

  • Ties the adapter to a specific subclass of the adaptee interface. With object adapter, we can compose with any subclass of the adaptee interface.

Structure

Participants

Target (SpecialCollection):

  • Defines the interfaces that the client uses
Client:
  • Uses the target interface
Adaptee(RandomAccessCollection):
  • Defines an existing interface that will be adapted to (the target interface)
Adapter(RandomAccessCollectionAdapter):
  • Adapts the adaptee interface to the target interface.

Design principles used

Conclusion

In this post, we looked at what an adapter pattern is along with an example. We adapted a collection allowing random access to be used as another collection that in addition to random access allows to get the first and the last element of the collection.

There are other similar looking design patterns that use object composition to achieve other results

References

Leave a Reply