On this page:
18.1 Reuse via adaptation
18.1.1 Adapters
18.1.2 A two-way adapter
18.1.3 Adaptation in Model-View-Controller
18.2 Reuse and de-clutter
6.3

Lecture 18: Adapters and Facades

Related files:
  adapters.zip  

18.1 Reuse via adaptation

In Lecture 16: Inheritance vs composition we discussed inheritance vs composition as simpler examples of reuse.

Person A’s IntSet class and its IntSet1 implementation underwent some subtle changes over time. Here is the updated interface and implementation.

 
import java.util.Collection;
import java.util.Iterator;
 
/** * Interface for simple sets of integers. This is part of a demonstration of * the dangers of inheritance. */
public interface IntSet extends Iterable<Integer> {
/** * Adds the given value to this set. * * @param value the integer add */
void add(int value);
 
/** * Adds all the values in the given collection to this set. * * @param values the integers to add */
default void addAll(Collection<Integer> values) {
for (int z : values) {
add(z);
}
}
 
/** * Removes the given value from this set, if present. * * @param value the integer to remove */
void remove(int value);
 
/* * Determines whether a particular integer value is a member of this set. * * @param value the integer to check * @return whether value is a member of this set */
boolean member(int value);
 
/** * Returns an iterator over the elements. * * @return an iterator */
Iterator<Integer> iterator();
}
/** * An implementation of IntSet using the Collections Framework's * HashSet. */
public class IntSetImpl implements IntSet {
private final Set<Integer> set = new HashSet<>();
 
@Override
public final void add(int value) {
set.add(value);
}
 
@Override
public final void remove(int value) {
set.remove(value);
}
 
@Override
public final boolean member(int value) {
return set.contains(value);
}
 
@Override
public final Iterator<Integer> iterator() {
return set.iterator();
}
}

This interface and implementation is part of a larger application that extensively uses sets of integers in this way.

Now she wishes to add a new feature to her program. In order to use that feature she uses another existing library (a collection of several classes and packages). The specific class she wishes to use within this library also uses sets of integers. However in order to make the library self-contained, it contains a different kind of set. Its methods concentrate on other mathematical set operations.

public interface IntSet2 {
/** * Inserts the elements of other into this set. */
void unionWith(IntSet2 other);
 
/** * Removes the elements of other from this set. */
void differenceFrom(IntSet2 other);
 
/** * Checks whether this set is a superset of another set. * @param other the other set * @return whether other} is a subset of this */
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. * @return the list of integers in this set */
List<Integer> asList();

Person A is in a fix: in order to reuse the library, she must use an implementation of the IntSet2 interface. But her own application extensively uses her own IntSet interface and its IntSetImpl implementation. It is frustrating because both represent sets of integers, only the methods look different. Is it possible to reuse the IntSetImpl class while offering the IntSet2 interface as the new library wants?

18.1.1 Adapters

The problem we face is that of adaptation. We need a class that offers the required interface but reuses an existing implementation with a different interface. In general the client requires an object that offers a Target interface. All the required functionality already exists in an object, denoted as the Adaptee. We must write an Adapter that reuses Adaptee and offers the Target interface. Inheritance offers one way to do this:

The Adapter class implements the Target interface and extends the Adaptee class. It then implements all the methods in the Target interface by using the methods it inherited from the Adaptee class. In our specific example we can write an adapter as follows:

/** * Adapts IntSet1Impl to the IntSet2 interface. This is the * class adapter pattern. */
public class IntSetImplToIntSet2Adapter
extends IntSetImpl
implements IntSet2
{
/** * Constructs a new empty IntSet1Adapter. */
public static IntSetImplToIntSet2Adapter empty() {
return new IntSetImplToIntSet2Adapter();
}
 
/** * Constructs a new single-element IntSet1Adapter. */
public static IntSetImplToIntSet2Adapter singleton(int i) {
IntSetImplToIntSet2Adapter result = new IntSetImplToIntSet2Adapter();
result.add(i);
return result;
}
 
@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;
}
}

Although this adapter works correctly, it may have limitations and side-effects:

In this case we can use composition as an alternative.

An object adapter implements the Target interface but reuses the Adaptee by delegating to it. As it wraps the Adaptee object it can control which of its methods it wishes to expose. The coupling between the adapter and adaptee is also reduced, as the adapter can delegate to any object that has the same interface as the adaptee. However one must write extra code for methods that perfectly match, that would simply delegate.

18.1.2 A two-way adapter

Our IntSet2ToIntSet1Adapter adapter lets us use an IntSet2 as an IntSet1. But what if we want to use the same 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. For example, we can declare our IntSet2ToIntSet1Adapter to implement both interfaces:

public final class IntSet2ToIntSet1Adapter implements IntSet1, IntSet2 { ... }
A class adapter is naturally a two-way adapter, but one may be constructed from an object adapter as well.

18.1.3 Adaptation in Model-View-Controller

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.

  

Another design could be for the IViewModel interface to be the same as the model interface. In this case the ViewModel class would implement all the model’s methods, but suppress any mutating operations (for example, by throwing UnsupportedOperationException). A third design would select elements of these two designs. We create the IViewModel interface to have only accessor methods, and make our model implement both the model interface and the IViewModel interface. When we provide the model interface to the view, we provide it as a IViewModel interface. This means that the model is available to the view with a restricted IViewModel interface.

18.2 Reuse and de-clutter

Often reusing existing code is complicated because it offers a lot more than what we need. Consider adding a new feature that can be implemented using existing functionality from many classes. This is very common in web applications, and is referred to as “code mashup”. More generally they are required when new functionality must be created by combining existing functionality from many different components that were not designed to work together. If one simply creates a new component that is composed of these components, it may expose too many methods to the client and lead to confusion and misuse.

The new component should offer a new, simpler interface. A simple interface would minimize misusing exposed methods. It also makes it easier to understand the new functionality and makes it seem more cohesive. This idea seems related to an object adapter, and is called a facade.

Facades provide an extra layer of indirection between the client classes and the classes they wish to use. This reduces direct coupling between the two. The primary objective of the facade is to facilitate this. Therefore, even though the facade object is tightly coupled to both sides, it is unlikely to change and hence the detrimental effects of coupling are practically minimal.