Adapter, Bridge, and
Façade
Objectives
The objectives of this chapter are to identify the following:
•
Complete the exercise in class design.•
Introduce the adapter, bridge, and façade patterns.•
Create basic UML diagrams for these design patterns.Class Exercise
After getting into groups of 3-4, please draw a class diagram, including relation-ships and multiplicity, of the following scenario:A university library system wants a system that meets the following criteria:
•
library holdings include books, periodicals, and videos.•
books and videos circulate, but periodicals don’t.•
patrons can check out materials that circulate.•
patrons may be undergraduate students, graduate students, or faculty.•
patrons may perform searches on items by author or title.•
undergraduates may check out 10 items, graduate students may check out 30 items, and faculty may check out an unlimited number of items.•
the system can report all of the items checked out by a particular patron.•
the system can report on all items that are overdue.Your diagram should include all relevant classes including those for supporting whatever design patterns you choose.
Bridge Pattern
Use the Bridge pattern to:•
Avoid binding an abstraction to its implementation. We’ll see what this means when we look more closely at and extend the SortAlgorithm class from last week.•
Allow both abstractions and implementations to be extensible by subclassing.•
Make changes to an implementation that doesn’t impact the client. This is just another way of saying that we have programmed to an interface and not to an implementation.•
Share an implementation across multiple objects and hide this fact from the client.Bridges have several participants:
•
An Abstraction class which declares an interface an maintains a reference to an Implementor.•
A Refined Abstractor which extends the Abstraction interface.•
An Implementor defines the interface for implementation classes. This inter-face need have no relationship or similarity to the Abstraction interinter-face.•
A Concrete Implementor that implements the Implementor interface. The results we gain from implementing a Bridge pattern are:•
Decoupling of an interface and an implementation. This allows an abstraction to avoid binding itself permanently to a specific implementation. An imple-mentation might be derived and used at run-time rather than compile time which increases the maintainability and potential reuse of our classes.•
Improved extensibility. The abstraction and its implementation may beextended separately.
•
Hidden implementations. The client need not know of what the implementa-tion does behind the scenes. The client communicates with the abstracimplementa-tion and only the abstraction worries about its implementation.To illustrate this design pattern, consider some of the sorting algorithms that were discussed last week, specifically the Quick Sort algorithm. Recall that with Quick Sort you must choose a pivot element and perform sorting on each side of the pivot. It turns out that there are multiple ways of choosing that pivot element. As a general rule:
•
Choose a simple pivot scheme when the array is small. Why?•
Choose a more complex pivot when the array is larger. Why?QuickSort Class
Recall that last week we used the InsertionSort class. This week we’ll focus on the QuickSort class. One implementation might appear as:If we were now to go ahead and code the QuickSort algorithm we might get something like:
void QuickSort::sort(Array & a, int lo, int hi) { if (hi <= low) {
int i = part(a, lo, hi); sort(a, lo, hi - 1); sort(a, lo + 1, hi) }
};
We will also need some kind of partition algorithm. This can easily be imple-mented as another class method:
int QuickSort::part(Array & a, int l, int r) { ...
};
Notice the potential problem here. We have embedded the implementation of the partition logic as a method in the class. This means we have only one possible way to partition our data set. But since what we want is to be able to use different implementations of the partition depending on the characteristics of our data, we need to make some changes. We could:
•
Subclass the QuickSort class and override the partition method. This would work but might lead to a series of different classes which differ only in their partitioning scheme. In this case, subclassing might be a case of using a sledgehammer to swat a fly.•
Use the Bridge pattern to replace the concrete implementation of the parti-tion method with an object. Needless to say, this is the opparti-tion we’re going to use.Revised
SortAlgorithm Class
template <class Item>
public class SortAlgorithm { protected:
const Implementor * option; public:
SortAlgorithm(const Implementor * op = 0); void setOption(const Implementor * op); bool lessthan(const Item & a,
const Item & b) const { if (option) return option->lessthan(a, b); else return (a < b); } };
The Java equivalent might appear as:
public class SortAlgorithm {
protected final Implementor option;
public SortAlgorithm(Implementor op) { option = op; } public void setOption(Implementor op) { option = op; } public boolean lessthan(Object a, Object b) {
if (option != null) return (option.lessthan(a, b)); else return (a < b); } };
Implementor Class
The next class we’ll build will be the implementor which is used as the base class to our implementation. Notice that this is similar to a Strategy pattern save that with a strategy, the entire strategy class is extended whereas with a Bridge pat-tern, the abstraction and its implementor may be extended separately.public abstract class Implementor {
public boolean lessthan(Object a, Object b) { ... } };
Concrete Implementor
Now we can actually write the code for the lessthan method of the Implemen-tor. A C++ version might appear as:template <class Item>
class Pivot : public Implementor { protected:
bool sortAsc, ignoreCase; public:
Pivot(bool so = true, bool ic = true) : sortAsc(so), ignoreCase(ic) { ... }
bool lessthan(const Item &, const Item &) const; };
The Java implementation might appear as:
public class Pivot extends Implementor { protected boolean sortAsc;
protected boolean ignoreCase; public Pivot(bool so, bool ic) {
sortAsc = so; ignoreCase = ic; }
bool lessthan(Object a, Object b) { ... } };
And now we can provide an implementation to the lessthan method which will be used by our sort algorithm to make its determinations:
template <class Item>
bool lessthan (const Item & a, const Item & b) const { if (sortAsc)
return (a < b); else
return (b < a); }
The corresponding Java code might appear as:
boolean lessthan (Object a, Object b) { if (sortAsc)
return (a < b); else
return (b < a); }
Now we can see how some client code might make use of these classes:
int main(int argc, char * argv[]) { Sort * sort = new QuickSort(); Implementor * option = new Pivot(); ArrayList input; cin >> input; sort->setOption(option); sort->sort(input); cout << input; }
Thus when the sort algorithm’s sort method is called, it will begin comparing elements using the implementation defined by the associated option imple-mentor.
Conceivably this means that the sort mechanism and option could be read from the command line and created a run time; you could sort the same data set differ-ent ways without forcing a recompilation of your program.
It would also be feasible to make each Pivot or SortAlgorithm a Singleton since you may never need more than one such object at a time.
Adapter Pattern
Sometimes toolkits or other classes that are purchased off-the-shelf or that come with other products are not compatible with your own internal classes due to dif-ferences in the interface. Your options in this case are somewhat limited. You’d like to take advantage of the libraries provided by your vendor, but you don’tAdapters have several participants:
•
A Target which defines the domain-specific interface that the Client uses.•
A Client which collaborates with objects conforming to the Target interface.•
An Adaptee which defines an existing interface that needs to be adapted.•
An Adapter which adapts the Adaptee to the Target interface.Façade
We have already seen an example of the façade pattern. A façade is simply a way to provide a simple interface to a complicated process. When you use the compile option of your compiler, it is a façade that hides a myriad of details behind the scenes.Use the façade pattern to:
•
Provide a simple interface to a complex subsystem.•
Layer the various subsystems.•
Decouple clients from subsystems and subsystems from one another. This is a common idea behind APIs and layered architectures.Façades have two participants:
•
A Façade which knows which subsystem classes are required to satisfy a given request and is able to delegate client requests to the appropriate sub-system.•
The Subsystem Classes which implement the bulk of the functionality. These classes can handle requests issued to them from the façade but yet have no knowledge of the existence of such a façade.Design Pattern
Consequences
The overall consequences of using the various design patterns may be summa-rized as follows:
•
Clients only need to know about a general interface with which they will be interacting; they remain unaware of the concrete classes working behind the scenes.•
Flexibility is increased because there are no direct dependencies between a class’ interface and its implementation.