Design
patterns are micro-architectures that solve a particular design problem. See
the class text Design Patterns. A pattern is a literary form that
describes a problem, the forces surrounding it, and a solution that resolves
those forces.
The
designs described here will influence your framework's structure. They will
also be at work in your implementation classes.
We
will examine several design patterns, and how they've been applied in the Java
class libraries (mostly AWT and Swing).
“Micro-architecture”
is meant to convey the idea of a small piece of the total architecture. It may
consist of only a couple classes, configured in a way that will solve a
particular problem. For example, the Singleton pattern says how to handle
classes that should have only one instance:
·
Make
the constructor private (so clients can’t create them)
·
Provide
a static method getInstance() that returns the single instance.
This
only involves the design structure of one class, but it solves the problem at
hand. More complicated patterns may address two, three, or several classes, but
they don’t try to provide a comprehensive solution to the “whole” problem.
Patterns
as flex axes [TBD]
A
solution to a problem can be thought of as representing something in a
multi-dimensional space. By making the dimensions independent, we attain
greater flexibility in assembling the whole solution. While we study patterns,
we’ll look at not only what they are, but what makes them flexible.
This
is not a comprehensive analysis of patterns. The best reference is still the
original book Design Patterns by Gamma et al. Our goal here is to
provide enough information to briefly expalin what each pattern does, and
relate its flexibility to the needs of frameworks.
|
Pattern |
What
It Makes Flexible |
Form
vs. Function |
|
Factory
Method |
What
is constructed |
Structure |
|
Adapter |
Support
of an interface |
Structure |
|
Facade |
Complex
structure behind a simple interface |
Structure |
|
Composite |
Containment
structure |
Structure |
|
Decorator |
Cumulative
effects |
Algorithm |
|
Template
Method |
Steps
of a fixed algorithm |
Algorithm |
|
Strategy |
Algorithm |
Algorithm |
|
Observer |
Independence
of model from view |
?
[TBD] |
|
Command |
Functions
treated as objects |
?
[TBD] |
You
don't always want to expose the details of how an object is constructed. You
may not want the user to be aware of the particular construction - maybe you'll
change in the future, maybe you build different object types depending on the
circumstances, etc.
One
way around this is to use a Factory Method: a class responsible for building
instances of another class. The factory method is often a static method in a
Factory class; usually the method is named createXxx() or makeXxx() or some such to
indicate its purpose. Sometimes the factory is the same as the class of the
desired object.
When
the constructed class is the same as the factory, you'll see a class structure
like this:
class Xxx {
// Factory methods
public static Xxx makeSomeXxx() { ...}
public static Xxx makeAnotherXxx() { ...}
// Protected constructor
protected Xxx() { ...}
// Public methods
public void doSomething() { ...}
public int getValue() { ...}
}
(Note
that the factory method need not return a "real" Xxx; often it will
return an instance of a subclass of Xxx.)
One
example of this is the Border object in Swing. There are many border types -
etched, line, titled, etc. There is a BorderFactory object that knows how to
return an instance of each.
Why
this separation? For one thing, borders are mostly constant: once created they
can't be changed. So, the factory can keep track of what it has created. Once
it has created the first instance of EtchedBorder, it saves it, and returns the
saved value each time one is requested. (The client doesn't care that it's not
a unique border object.) In this way, we avoid creating many unnecessary
objects.
Recall
that we used a factory method in our graph package. This let us avoid making
clients depend on the actual Node and Edge types. Without the factory, we'd
have client code like this:
Node n1 = new SimpleNode();
If
we ever wanted to use a different node type, we'd have to find and change every
constructor! By using the factory method, our code is like this:
Graph graph = new SimpleGraph();
Node n = graph.MakeNode();
We
still have to worry about the Graph constructor, but we'll create many more
nodes than graphs, and the nodes are now independent of their type.
Use
a Factory Method when you want to vary (hide!) the construction of objects.
[TBD:
returning a specialized subclass]
You
have one interface, but you need another. Use an Adapter to translate between
them.
For
example, we might like the user to select an object, and then we'll do
something with the selection. What's the user selecting from? It could be a
list, tree, table, radio-button, etc. We don't want to commit to a particular
choice, so we have this interface:
public interface ObjectSelectionListener {
public void objectSelected (ObjectSelectionEvent e);
}
The
problem is: list, tree, etc. don't know this interface - they were written
before it even existed! To make a list able to notify us of object selections,
we'll introduce an Adapter. It will be a ListSelectionListener so it can work
with Lists, and it will manage ObjectSelectionListeners so it can work with our
interface.
[TBD
- check whether it's list or ListModel]
public class ListToObjectAdapter implements ListSelectionListener {
protected JList list;
public ListToObjectAdapter (JList list) {
this.list = list;
list.addSelectionListener(this);
}
public void valueChanged (ListSelectionEvent e) {
Event e2 = new ObjectSelectionEvent(list.getSelectedValue());
// for each listener - send e2
}
Vector v = new Vector();
public void addObjectSelectionListener(ObjectSelectionListener listener)
{
v.addElement(listener);
}
public void removeObjectSelectionListener(ObjectSelectionListener listener) {
v.removeElement(listener);
}
}
You
might use it like this:
JList list = new JList(listModel);
ListToObjectAdapter adapter = new ListToObjectAdapter(list);
ObjectSelectionListener osl = new MyObjectDisplay();
adapter.add(osl);
When
an event comes in to the adapter, it broadcasts to its listeners.
Use
an Adapter when you must match two interfaces that are conceptually related,
but physically incompatible.
[TBD]
Objects
often contain instances of their own class (recursively). When you notice such
a structure, you can sometimes make use of the Composite pattern.
Parent Containers
can contain containers, but eventually get to leaves.
^ | |
| V V
Container Child
The
Java AWT hierarchy is like this:
Component
^ ^ ^
| ihs ihs
Container
Button etc.
With
Composite, there's a decision about the containment capabilities: is Component
aware of them or not? The original AWT says "Components are not
containers."
In
the new Swing library, they've reversed that decision
Component
^ ^ ^
Container Button
etc still exist
^
JComponent
^
JButton
etc
The
Swing "Jxxx" components are all containers - they inherit from
JComponent which inherits from Container.
Why
the change? It makes some things a little easier. You can "atomize"
components more. From the outside, you deal with a JButton as a single entity,
but inside it is be composed of its inner area and a border, rather than being
monolithic.
Use
Composite when you have a recursive structure.
Sometimes
you have a recursive structure (perhaps a Composite), and you want to attach
behavior all through the structure. The Decorator pattern tells how to provide
this: make sure each object has a common interface and "wrap" the
additional behavior around the original object. Usually the interface is
cumulative in some sense.
An
example in Swing is the border class. (Borders are an extension/replacement of
Insets.) For example
-------
matte
Title
Line
contents
There
is a type of Border called a CompoundBorder, which layers two borders together
(an outer and an inner border). The above example is represented like this:
Compound Border
MatteBorder
CompoundBorder
TitledBorder
LineBorder
Borders
can report their sizes and draw themselves. A CompoundBorder is the same: its
size is the sum of the two contained border sizes, and it draws itself by
letting the borders draw themselves one inside the other.
[TBD:
more pure if all borders were compound]
Another
example is the ScrollPane. In most ways, it acts like a panel containing a
component. Drawing is done by drawing scrollbars (if needed) and then letting
the the "contained" component draw itself.
Use
this pattern when you have a recursive structure of objects with compatible
interfaces, and behavior that accumulates.
You
have an algorithm that is generally useful, but the details are particular to
an implementation.
[TBD
capsule description]
[TBD
example - applet]
An
example of this is the paint() method used by Component. The Component class
has no idea how a particular component will be drawn, but the AWT knows that
each will draw itself when paint() is called.
Component
=======
paint(Graphics g)
<>
|
MyComponent
==========
paint(Graphics g)
In
some other class, there is code like this:
for each component c
if it's visible
Create a graphics context g
c.paint(g)
end if
end for
The
idea can apply to a set of methods (rather than just one):
setup()
process(Item e)
cleanup()
Use
this pattern when the basic algorithm is stable, but the individual steps
change.
You
may have objects that are stable, but an algorithm that changes.
For
example, the AWT layout is like this. A container's contents are separate from
their layout. You can swap in a new layout manager without affecting how the
contents are built.
Manager
======
arrange() { ----> Strategy
defer to strategy ABCStrategy DEFStrategy
}
Another
classic example is text layout. Your word processor may have an editing mode
(where words are wrapped to fit the screen) and a print preview mode (where
word breaks are balanced against page size). This can be implemented as a
strategy.
Use
a strategy when the algorithm changes.
One
of the best known patterns is the Observer pattern (also known as
Model-View-Controller). This pattern decouples the update of a data from its
display.
Suppose
you have a data structure, and one or more displays (e.g., a text editor). When
the data structure changes, you'd like to update all displays. If the data
structure directly updates the display, there's a problem: every time a new
display is added, the data structure must be changed.
The
Observer pattern breaks this dependency by splitting the model from its views.
The model is coupled to the display only weakly: each display registers with
the model to request notification when the model changes. The display gets the
notice, and queries the model to figure out what changed.
The
Observer pattern is embedded in the JDK 1.1 event model. For instance, a Button
generates ActionEvent notifications. An ActionListener can register with the
button, and when the button is clicked, the listener is notified.
Button MyActionListener
addActionListener (1) actionPerformed
(2)
Button
b = new Button ("test");
b.addActionListener
(new ActionListener() {
public void actionPerformed
(ActionEvent e) {
System.out.println("Button
clicked; event="+e);
}
}
);
Notice
that the button has no knowledge of the particulars of a listener: it just
knows how to refer to one, and to call its actionPerformed() method.
Use
this pattern when you need to decouple classes.
Sometimes
you need a function to act like an object. The Command pattern addresses this.
For
example, suppose you want to provide a camera simulator, to show the effect of
lens filters. You might want the user to drag filters onto a stack, and then
show the effect of the combination. You could use the Command pattern: make the
filters be command objects, each one a function on the image's appearance.
In
the Swing package, there is support for the Command pattern in the Action class
and the undo package. The Action class encapsulates functions and objects. For
example, an editor can give you a list of Actions, corresponding to functions
such as copy or move-to-end-of-line.
The
undo package introduces a class UndoableEdit. Each UndoableEdit can be done or
undone. These objects can be saved to a list, so undo/redo can work by doing or
undoing each of the actions in the list.
Use
this pattern to make a function work as an object.