Lecture 13: Adapter Pattern
Adapter Pattern Lecture Videos.
13.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—
/** * 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.
13.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 create 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 { }
@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.
13.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 create 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(); }
13.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(); }
13.4 The ViewModel
In the Model-View-Controller architecture, the view must render the data that is maintained by the model. In such circumstances, how will the data from the model make it to the view?
One way would be for the controller to act as an intermediary for this data exchange. The controller would get the data from the model, and then supply it to the view. In this design, the view does not “ask” for the data–the controller provides it at the appropriate time and tells the view to use it. This can be termed as a “push” design: the controller pushes the data to the view. One drawback of this design is that the controller must repeatedly pass (possibly a lot of) data from the model to the view. This can be made more efficient if the view could “pull” the data out whenever it needed.
The “pull” design can be as simple as having the view store a reference to the model. This design has an even bigger potential pitfall. The model interface likely offers operations that access data, and others that mutate it. By giving the view a reference to the model, we give the view the power to mutate the model directly. Such direct mutation is undesirable for several reasons: a view can circumvent checks that are implemented in the controller, a view can make changes that conflict with the workings of another view, etc. If only there was a way for the view to provide restricted access to the model: it can access, but it may not mutate.
We can achieve this by creating a ViewModel class that implements a IViewModel interface. A ViewModel object would then extract data from the original model object and provide it to its client, essentially avoiding direct contact between the client and the model.
A possible design would be for the IViewModel interface to contain methods that only get the data. The ViewModel class would use the actual model object to provide the data. Since the data resides in a specific, pre-existing model object, the ViewModel class is essentially an object adapter that adapts the model with the viewmodel interface.
|