Lecture 9: The Adapter Pattern
1 A tale of two interfaces
2 The class adapter pattern
3 The object adapter pattern
3.1 A two-way adapter
6.7

Lecture 9: The Adapter Pattern

1 A tale of two interfaces

It’s rare that real, significant programs are designed completely from scratch. Most real programs use libraries, often several. It’s also rare that all the libraries used by a program were designed to work together, using all the same abstractions. More often, libraries have to define some interfaces of their own. Sometimes, you need to integrate two libraries that define different interfaces for similar or even equivalent abstractions.

For example, suppose we were using two different libraries that each offered its own set interface incompatible with the other. Suppose a library for working with graphs—call it graphrelies on a set abstraction. So it declares set operations, which in this case work with elements of sets1We limit these to sets of int to avoid the additional complexity of generics.:

/**
 * Simple sets of integers.
 */
public interface IntSet1 extends Iterable<Integer> {
  /**
   * Adds the given value to this set.
   */
  void add(int value);

  /**
   * Removes the given value from this set, if present.
   */
  void remove(int value);

  /*
   * Determines whether a particular integer value is a member of this set.
   */
  boolean member(int value);

  /**
   * Returns an iterator over the elements.
   */
  Iterator<Integer> iterator();
}

The graph library provides a class IntSet1Impl that implements IntSet1 and has a default constructor:

public class IntSet1Impl implements IntSet1 {
  /** Creates a new, empty integer set. */
  public IntSet1Impl() { ... }

  ...
}

The other library, for probability computations (prob), declares set operations that work mostly with sets rather than elements:

public interface IntSet2 {
  /**
   * Inserts the elements of {@code other} into this set.
   */
  void unionWith(IntSet2 other);

  /**
   * Removes the elements of {@code other} from this set.
   */
  void differenceFrom(IntSet2 other);

  /**
   * Checks whether this set is a superset of the {@code other} set.
   */
  boolean isSupersetOf(IntSet2 other);

  /**
   * The contents of the set as a list of integers. Modifying the returned
   * list will have no effect on this set.
   */
  List<Integer> asList();
}

The prob library provides a class IntSet2Impl that implements IntSet2. For creating instances, IntSet2Impl provides two static factories, empty() and singleton(int):

public class IntSet2Impl implements IntSet2 {
  /** Creates a new, empty integer set. */
  public static empty() { ... }

  /** Creates a new, singleton integer set with the given member. */
  public static singleton(int member) { ... }

  ...
}

What if we want to use both of these libraries from the same program, and furthermore, we want to use some sets with both. Perhaps we want to use graph’s sets, which implement IntSet1, with code that works with prob’s sets, which implement IntSet2. Can we do it? Yes, with an adapter.

2 The class adapter pattern

To make this abstract for a moment, suppose we have a class AdapteeImpl and we want to reuse its implementation where an object implementing interface Target is needed. AdapteeImpl and Target don’t have exactly the same methods, but they’re close enough that we can write a class AdapteeImplToTargetAdapter that implements interface Target by subclassing AdapteeImpl and implementing Targets methods in terms of the inherited methods primarily using an instance of Adaptee to do so. In UML it looks like this:

If the client wants a Target then we can use AdapteeImpl to implement it by writing AdapteeToTargetAdapter, which extends AdapteeImpl and implements Target by translating Target methods to AdapteeImpl methods. Or more concretely, some code from prob (the client) knows how to use IntSet2s, and we implement IntSet2 based on IntSet1Impl by writing an adapter that wraps an IntSet1Impl and uses it as the representation of an IntSet2.

We start the adapter by extending IntSet1Impl:

public class IntSet2ToIntSet1Impl
  extends IntSet1Impl implements IntSet2
{
}

For the remaining methods, each is about implementing a method that
takes a set in and translates it to the individual member operations
that IntSet supports.

@Override
public final void unionWith(IntSet2 other) {
  for (int i : other.asList()) {
    add(i);
  }
}

@Override
public final void differenceFrom(IntSet2 other) {
  for (int i : other.asList()) {
    remove(i);
  }
}

@Override
public final boolean isSupersetOf(IntSet2 other) {
  for (int z : other.asList()) {
    if (! member(z)) {
      return false;
    }
  }

  return true;
}

@Override
public final List<Integer> asList() {
  List<Integer> result = new ArrayList<>();
  for (int i : this) {
    result.add(i);
  }
  return result;
}

The class adapter pattern is simple to use—just extend the adaptee and then implement the target. The resulting object, by virtue of extending the adaptee, inherits its interface for free. However, there are many things it cannot do, such as:

In order to do those things and more, we can replace inheritance with delegation and gain flexibility as well.

3 The object adapter pattern

To make this once again abstract, suppose we have an object of type Adaptee (which could be an interface or a class) and we want to use it in a context where an object implementing interface Target is needed. Adaptee and Target don’t have exactly the same methods, but they’re close enough that we can write a class AdapteeToTargetAdapter that implements interface Target primarily using an instance of Adaptee to do so. In UML it looks like this:

If the client wants a Target then we can use an Adaptee in its place by writing an AdapteeToTargetAdapter, which owns an Adaptee and implements Target by translating Target methods to Adaptee methods. Or more concretely, some code from graph (the client) knows create IntSet1s, and we can use an IntSet1 as that IntSet2 by writing an adapter that wraps an IntSet1 and uses it as the representation of an IntSet2.

We start the adapter by declaring that it implements IntSet1, which is what the client wants, and has a field containing an IntSet2, which is the object to adapt:

public final class IntSet2ToIntSet1Adapter implements IntSet1 {
  private final IntSet2 adaptee;
}

There are two ways we might want to construct an IntSet2ToIntSet1Adapter. If we have an IntSet2 object already, we can adapt that object:

public IntSet2ToIntSet1Adapter(IntSet2 adaptee) {
  this.adaptee = Objects.requireNonNull(adaptee);
}

Whereas sometimes we might not have an IntSet2 already and not need access to it directly, so we provide a convenience constructor that creates the adaptee for us:

public IntSet2ToIntSet1Adapter() {
  this(IntSet2Impl.empty());
}

Next we need to implement the methods of IntSet1 interface in terms of IntSet2 by translating method calls to the adaptee. For example, to add an int to the set, we use adaptee’s unionWith method, passing it a singleton IntSet2 containing the int value:

@Override
public void add(int value) {
  adaptee.unionWith(IntSet2Impl.singleton(value));
}

The remove and member methods are equally straightforward:

@Override
public void remove(int value) {
  adaptee.differenceFrom(IntSet2Impl.singleton(value));
}

@Override
public boolean member(int value) {
  return adaptee.isSupersetOf(IntSet2Impl.singleton(value));
}

Finally, to implement the iterator method, we ask the adaptee for its elements in a list, since IntSet2 supports that, and then return an iterator over the list:

@Override
public Iterator<Integer> iterator() {
  return adaptee.asList().iterator();
}

3.1 A two-way adapter

Our IntSet2ToIntSet1Adapter adapter lets us use an IntSet2 from prob as an IntSet1 that we can pass to graph. But what if we’d like to use the resulting object with both libraries, with no further adaptation? A two-way adapter implements two interfaces, most often being a Target as well as an Adaptee, sending requests received via either interface to the underlying adaptee object. For example, we can declare our IntSet2ToIntSet1Adapter to implement both interfaces:

public final class IntSet2ToIntSet1Adapter implements IntSet1, IntSet2 { ... }

the implementation from the previous section remains, and to implement IntSet2 as well, we merely need to forward those requests to the IntSet2 adaptee:

@Override
public void unionWith(IntSet2 other) {
  adaptee.unionWith(other);
}

@Override
public void differenceFrom(IntSet2 other) {
  adaptee.differenceFrom(other);
}

@Override
public boolean isSupersetOf(IntSet2 other) {
  return adaptee.isSupersetOf(other);
}

@Override
public List<Integer> asList() {
  return adaptee.asList();
}

1We limit these to sets of int to avoid the additional complexity of generics.