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—graph
—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 Target
s 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 IntSet2
s, 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—
wrapping an already-existing object, or
wrapping objects of different types than the one for which it was intended.
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 IntSet1
s, 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.