一起学习
Summary
The Decorator design pattern lets you attach responsibilities to objects at runtime. This pattern proves more flexible than inheritance, which is static. In this latest installment of Java Design Patterns, David Geary explores Decorator with an implementation example. (2,800 words; December 14, 2001)
Being somewhat of a closet Luddite, I only recently purchased my first cell phone. My phone is faceplate-ready -- in other words, I can snap on a stylish faceplate. As far as I can tell, faceplates are the only dynamically changeable aspect of my cell phone. For example, I cannot snap on antennas or speakers; those are set statically at the factory.
Software decorators, like cell phone faceplates, encapsulate some functionality that you can snap onto an existing object; for example, here's a code fragment that snaps sorting onto an existing Swing table model:
TableSortDecorator sortDecorator = new TableSortDecorator(table.getModel());
table.setModel(sortDecorator);
The preceding code swaps out a table's model for a decorator. After the swap, whenever the table accesses its model, it unknowingly accesses the sort decorator. The decorator adds sorting capabilities to the model it decorates, and delegates other functionality to the real model.
Like cell phones, software heavily reliant on inheritance includes a high percentage of statically defined objects. Just as a cell phone's antenna or speaker is statically defined at the factory, base classes and their extensions are statically defined at compile time. Because inheritance is static, it does not allow you to swap out an aspect of an object's behavior at runtime (such as the table model decorator in the code fragment above). Because you can use decorators much like Legos to snap together an object with desired behaviors at runtime, the Decorator design pattern proves far more flexible than inheritance.
In this article, I first introduce the Decorator pattern, starting with an example that shows how to use Java's I/O decorators. I then outline the Decorator pattern's statics and dynamics with UML class and sequence diagrams, respectively. I conclude with an example decorator that, as alluded to above, adds sorting to Swing tables.
Note: In the first installment of this column -- "Amaze Your Developer Friends with Design Patterns" -- I introduced Decorator, among other patterns. You may wish to read that introduction before proceeding with this more detailed look.
The Decorator pattern demystified
Decorator: Attaches responsibilities to objects at runtime. Decorators prove more flexible than inheritance, which attaches responsibilities to classes at compile time.
In "Amaze Your Developer Friends with Design Patterns," I described input streams and the Decorator pattern. Example 1 from that article demonstrates instantiating a line number reader:
Example 1. Instantiating I/O decorators
1. FileReader frdr = new FileReader(filename);
2. LineNumberReader lrdr = new LineNumberReader(frdr);
The preceding code creates a reader -- lrdr -- that reads from a file and tracks line numbers. Line 1 creates a file reader (frdr), and line 2 adds line-number tracking.
At runtime, decorators forward method calls to the objects they decorate. For example, in the code above, the line number reader, lrdr, forwards method calls to the file reader, frdr. Decorators add functionality either before or after forwarding to the object they decorate; for example, our line number reader tracks the current line number as it reads from an input stream.
Alternatively, of course, you could write Example 1 like this:
LineNumberReader lrdr = new LineNumberReader(new FileReader(filename));
Both methods outlined above adhere to the same construction idiom: wrapping objects recursively -- a hallmark of the Decorator design pattern.
The code in Example 2 below shows how to instantiate and use a line number reader. The example reads lines of text from a file and prints each line with its line number:
Example 2. Using I/O decorators
try {
LineNumberReader lrdr = new LineNumberReader(new FileReader(filename));
for(String line; (line = lrdr.readLine()) != null;)rticle.txt {
System.out.print(lrdr.getLineNumber() ":\t" line);
}
}
catch(java.io.FileNotFoundException fnfx) {
fnfx.printStackTrace();
}
catch(java.io.IOException iox) {
iox.printStackTrace();
}
Decorators represent a powerful alternative to inheritance. Whereas inheritance lets you add functionality to classes at compile time, decorators let you add functionality to objects at runtime.
Decorator statics and dynamics
Mechanical engineers study statics and dynamics. Statics analyze forces on objects that don't move very much -- bridges or buildings, for instance. Dynamics analyze forces on moving objects, typically in machines. The centrifugal forces on jet engine blades or clock pendulums are good examples.
Statics and dynamics have direct counterparts in the realm of software design patterns. Design pattern statics analyze class relationships specified at compile time, whereas design pattern dynamics analyze the runtime sequence of events in which objects participate. In this section, I show design pattern statics with UML class diagrams and design pattern dynamics with UML sequence diagrams.
Throughout the Java Design Pattern column, I will discuss the static and dynamic aspects of each design pattern we explore. Let's begin by examining the statics and dynamics of the pattern at hand -- Decorator.
Decorator statics
Decorators decorate an object by enhancing (or in some cases restricting) its functionality. Those objects are referred to as decorated. Figure 1 shows the static relationship between decorators and the decorated.
Figure 1. Decorator class diagram. Click on thumbnail to view full-size image.
Decorators extend the decorated class (or implement the decorated interface), which lets decorators masquerade as the objects they decorate. They also maintain a reference to a Decorated instance. That instance is the object that the decorator decorates. As an example of how the classes in the Decorator pattern relate, Figure 2 depicts the static relationships among four decorators from the java.io package:
BufferedReader
LineNumberReader
FilterReader
PushbackReader
Figure 2. I/O decorators
BufferedReader and FilterReader are decorators just like the one shown in Figure 1. Both classes extend the abstract Reader class, and both forward method calls to an enclosed Reader. Because they extend BufferedReader and FilterReader, respectively, LineNumberReader and PushbackReader are also decorators.
Decorator dynamics
At runtime, decorators forward method calls to the objects they decorate, as shown in Figure 3.
Figure 3. Decorator dynamics. Click on thumbnail to view full-size image.
Developers often refer to Decorators as wrappers because they wrap method calls to decorated objects. Figure 3 clearly depicts such wrapping. Figure 4 shows the dynamics of the code listed in Example 2.
Figure 4. I/O decorator dynamics
Now that we have a high-level understanding of Decorator's statics and dynamics, let's use an example to examine the implementation of the Decorator pattern.
Sort and filter decorators for Swing tables
The Decorator pattern excels at attaching functionality, such as sorting and filtering, to Swing tables. In fact, the Decorator pattern is flexible enough that we can attach functionality to any Swing table at runtime. Before we can discuss how decorators enhance Swing tables in detail, we must have a basic understanding of Swing tables; so, let's take a short detour.
Swing tables
Swing components are implemented with the Model-View-Controller (MVC) design pattern. Each component consists of a model that maintains data, views that display the data, and controllers that react to events. For example, Swing tables, instances of JTable, are commonly created with an explicit model. Example 3 lists an application that does just that. (Try to picture the table in Example 3 before viewing it in Figure 5.)
Example 3. Create a Swing table
import javax.swing.*;
import javax.swing.table.*;
public class Test extends JFrame {
public static void main(String args[]) {
Test frame = new Test();
frame.setTitle("Tables and Models");
frame.setBounds(300, 300, 450, 300);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.show();
}
public Test() {
TableModel model = new TestModel();
getContentPane().add(new JScrollPane(new JTable(model)));
}
private static class TestModel extends AbstractTableModel {
final int rows = 100, cols = 10;
public int getRowCount() { return rows; }
public int getColumnCount() { return cols; }
public Object getValueAt(int row, int col) {
return "(" row "," col ")";
}
}
}
The application creates a table with a model. That table is placed in a scrollpane and added to the frame's content pane. Figure 5 shows the application listed in Example 3.
Figure 5. A simple Swing table
The application shown above creates a table model with 100 rows and 10 columns. When asked for a cell value, the model creates a string that represents the cell's row and column. The application uses that model to construct a Swing table (an instance of JTable). Table models produce a table's data, in addition to metadata such as the number of rows and columns.
It's important to understand that Swing tables possess models that maintain a table's data. Table models, as evidenced in Example 3, do not have to actually store any data -- they must produce a value for a given row and column.
A sort decorator
With a basic understanding of the Decorator pattern and Swing tables, we can now implement a table sort decorator. First, we'll see an application that uses the decorator, followed by a discussion of the decorator's implementation.
The application shown in Figure 6 contains a table with two columns: one for items and another for price. You can sort columns by clicking on column headers. When the application starts, nothing is sorted, as evidenced by the status bar in the bottom of the left picture in Figure 6. I took the middle picture in Figure 6 after I clicked the Item column header. I took the the right picture after I clicked the Price/Lb. column header.
Figure 6. A sorting decorator. Click on thumbnail to view full-size image.
The application shown in Figure 6 replaces the table's model with a sort decorator. That application is partially listed in Example 4. (You can download the full source code from the Resources section below.)
Example 4. Sorting a Swing table
import java.awt.*;
import java.awt.event.*;
import java.util.Locale;
import java.util.ResourceBundle;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
public class Test extends JFrame {
public static void main(String args[]) {
SwingApp.launch(new Test(), "A Sort Decorator",
300, 300, 200, 250);
}
public Test() {
// Create the decorator that will decorate the table's
// original model. The reference must be final because it's
// accessed by an inner class below.
final TableSortDecorator decorator =
new TableSortDecorator(table.getModel());
// Set the table's model to the decorator. Because the
// decorator implements TableModel, the table will never
// know the difference between the decorator and it's
// original model.
table.setModel(decorator);
...
// Obtain a reference to the table's header
JTableHeader header = (JTableHeader)table.getTableHeader();
// React to mouse clicks in the table header by calling
// the decorator's sort method.
header.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
TableColumnModel tcm = table.getColumnModel();
int vc = tcm.getColumnIndexAtX(e.getX());
int mc = table.convertColumnIndexToModel(vc);
// Perform the sort
decorator.sort(mc);
// Update the status area
SwingApp.showStatus(headers[mc] " sorted");
}
});
}
...
}
In the preceding code, a sort decorator decorates the table's original model. When you click on a column header, the application calls the decorator's sort() method. The interesting code in this example is not the application, but the sort decorator. Before looking at the source to TableSortDecorator, let's look at its class diagram, shown in Figure 7.
Figure 7. Sorting decorator class diagram
Since instances of TableSortDecorator decorate table models, TableSortDecorator implements the TableModel interface. TableSortDecorator also maintains a reference to the real model that it decorates. That reference can point to any table model. Example 5 lists the TableSortDecorator class:
Example 5. A table sort decorator
class TableSortDecorator implements TableModel,TableModelListener{
public TableSortDecorator(TableModel model) {
this.realModel = model;
realModel.addTableModelListener(this);
allocate();
}
// The following nine methods are defined by the TableModel
// interface; all of those methods forward to the real model.
public void addTableModelListener(TableModelListener l) {
realModel.addTableModelListener(l);
}
public Class getColumnClass(int columnIndex) {
return realModel.getColumnClass(columnIndex);
}
public int getColumnCount() {
return realModel.getColumnCount();
}
public String getColumnName(int columnIndex) {
return realModel.getColumnName(columnIndex);
}
public int getRowCount() {
return realModel.getRowCount();
}
public boolean isCellEditable(int rowIndex, int columnIndex) {
return realModel.isCellEditable(rowIndex, columnIndex);
}
public void removeTableModelListener(TableModelListener l) {
realModel.removeTableModelListener(l);
}
public Object getValueAt(int row, int column) {
return getRealModel().getValueAt(indexes[row], column);
}
public void setValueAt(Object aValue, int row, int column) {
getRealModel().setValueAt(aValue, indexes[row], column);
}
// The getRealModel method is used by subclasses to
// access the real model.
protected TableModel getRealModel() {
return realModel;
}
// tableChanged is defined in TableModelListener, which
// is implemented by TableSortDecorator.
public void tableChanged(TableModelEvent e) {
allocate();
}
// The following methods perform the bubble sort ...
public void sort(int column) {
int rowCount = getRowCount();
for(int i=0; i < rowCount; i ) {
for(int j = i 1; j < rowCount; j ) {
if(compare(indexes[i], indexes[j], column) < 0) {
swap(i,j);
}
}
}
}
private void swap(int i, int j) {
int tmp = indexes[i];
indexes[i] = indexes[j];
indexes[j] = tmp;
}
private int compare(int i, int j, int column) {
TableModel realModel = getRealModel();
Object io = realModel.getValueAt(i,column);
Object jo = realModel.getValueAt(j,column);
int c = jo.toString().compareTo(io.toString());
return (c < 0) ? -1 : ((c > 0) ? 1 : 0);
}
private void allocate() {
indexes = new int[getRowCount()];
for(int i=0; i < indexes.length; i) {
indexes[i] = i;
}
}
private TableModel realModel; // We're decorating this model
private int indexes[];
}
Table sort decorators do not change their real model when they sort; instead, decorators maintain an array of integers representing sorted row positions. When a sort decorator, masquerading as a table model, is asked for a value at a specific row and column, it uses the row value as an index into its array, and returns the corresponding value from the array. In that way, sort decorators overlay sorting onto Swing table models without modifying the original model in any way.
TableSortDecorator also implements the TableModelListener interface and registers itself as a listener with the real model. When the real model fires a table changed event, the decorator reallocates its array of sorted index references. That allocation happens in TableSortDecorator.tableChanged.
TableSortDecorator has 11 public methods. Nine of those methods forward to the real model. Of those nine methods, only two methods -- getValueAt() and setValueAt() -- provide any additional functionality. Such ratios are not unusual among decorators; most decorators add a small amount of behavior to a decorated object that already performs several functions.
Refactoring the sort decorator
The sort decorator discussed above works as advertised by decorating arbitrary table models with sorting functionality at runtime. This gives you flexibility because the decorated models do not need to be modified, which means that any table model can be decorated with sorting capabilities.
But the TableSortDecorator as implemented above is not designed for reuse because it implements two responsibilities for which it is not well suited. The first responsibility is forwarding to the real model. Because other table model decorators will need almost exactly the same code, that responsibility is too general, and should be moved up the class hierarchy. The second responsibility is sorting, which in this example is a bubble sort. That sorting algorithm is hardcoded into the sort decorator, which means sorting cannot be changed without rewriting the entire decorator. The sorting algorithm is too specific, so it should be moved down the hierarchy.
Figure 8 shows the result of refactoring the sort decorator as recommended in the preceding paragraph.
Figure 8. Refactored sorting decorator class diagram. Click on thumbnail to view full-size image.
The refactoring split the original TableSortDecorator into three different classes:
TableModelDecorator: Implements TableModel by forwarding to the real model
TableSortDecorator: Extends TableModelDecorator and adds an abstract sort method that subclasses must implement
TableBubbleSortDecorator: Extends TableSortDecorator and implements the bubble sort
By refactoring the sort decorator, we can reuse the code that forwards to the real table model. Encapsulating that code in TableModelDecorator lets us easily develop other table model decorator types, such as the filter decorators -- which are not discussed in this article -- shown in Figure 8.
The abstract TableSortDecorator class defers the implementation of its sort() method to subclasses, making it trivial to implement sort decorators with a different sorting algorithm.
Example 6 lists the TableModelDecorator class.
Example 6. TableModelDecorator
import javax.swing.table.TableModel;
import javax.swing.event.TableModelListener;
// TableModelDecorator implements TableModelListener. That
// listener interface defines one method: tableChanged(), which
// is called when the table model is changed. That method is
// not implemented in this abstract class; it's left for
// subclasses to implement.
public abstract class TableModelDecorator
implements TableModel, TableModelListener {
public TableModelDecorator(TableModel model) {
this.realModel = model;
realModel.addTableModelListener(this);
}
// The following 9 methods are defined by the TableModel
// interface; all of those methods forward to the real model.
public void addTableModelListener(TableModelListener l) {
realModel.addTableModelListener(l);
}
public Class getColumnClass(int columnIndex) {
return realModel.getColumnClass(columnIndex);
}
public int getColumnCount() {
return realModel.getColumnCount();
}
public String getColumnName(int columnIndex) {
return realModel.getColumnName(columnIndex);
}
public int getRowCount() {
return realModel.getRowCount();
}
public Object getValueAt(int rowIndex, int columnIndex) {
return realModel.getValueAt(rowIndex, columnIndex);
}
public boolean isCellEditable(int rowIndex, int columnIndex) {
return realModel.isCellEditable(rowIndex, columnIndex);
}
public void removeTableModelListener(TableModelListener l) {
realModel.removeTableModelListener(l);
}
public void setValueAt(Object aValue,
int rowIndex, int columnIndex) {
realModel.setValueAt(aValue, rowIndex, columnIndex);
}
// The getRealModel method is used by subclasses to
// access the real model.
protected TableModel getRealModel() {
return realModel;
}
private TableModel realModel; // We're decorating this model
}
Notice that public TableModelDecorator methods do nothing but forward to the real model. That makes them good defaults for other classes to inherit when they extend TableModelDecorator. Example 7 lists TableSortDecorator.
Example 7. TableSortDecorator
import javax.swing.table.TableModel;
public abstract class TableSortDecorator extends
TableModelDecorator {
// Extensions of TableSortDecorator must implement the
// abstract sort method, in addition to tableChanged. The
// latter is required because TableModelDecorator
// implements the TableModelListener interface.
abstract public void sort(int column);
public TableSortDecorator(TableModel realModel) {
super(realModel);
}
}
The abstract TableSortDecorator class defines one abstract method -- sort() -- that must be implemented by concrete subclasses. The TableBubbleSortDecorator, listed in Example 8, is one such class.
Example 8. TableBubbleSortDecorator
import javax.swing.table.TableModel;
import javax.swing.event.TableModelEvent;
public class TableBubbleSortDecorator extends TableSortDecorator {
// The lone constructor must be passed a reference to a
// TableModel. This class decorates that model with additional
// sorting functionality.
public TableBubbleSortDecorator(TableModel model) {
super(model);
allocate();
}
// tableChanged is defined in TableModelListener, which
// is implemented by TableSortDecorator.
public void tableChanged(TableModelEvent e) {
allocate();
}
// Two TableModel methods are overridden from
// TableModelDecorator ...
public Object getValueAt(int row, int column) {
return getRealModel().getValueAt(indexes[row], column);
}
public void setValueAt(Object aValue, int row, int column) {
getRealModel().setValueAt(aValue, indexes[row], column);
}
// The following methods perform the bubble sort ...
public void sort(int column) {
int rowCount = getRowCount();
for(int i=0; i < rowCount; i ) {
for(int j = i 1; j < rowCount; j ) {
if(compare(indexes[i], indexes[j], column) < 0) {
swap(i,j);
}
}
}
}
private void swap(int i, int j) {
int tmp = indexes[i];
indexes[i] = indexes[j];
indexes[j] = tmp;
}
private int compare(int i, int j, int column) {
TableModel realModel = getRealModel();
Object io = realModel.getValueAt(i,column);
Object jo = realModel.getValueAt(j,column);
int c = jo.toString().compareTo(io.toString());
return (c < 0) ? -1 : ((c > 0) ? 1 : 0);
}
private void allocate() {
indexes = new int[getRowCount()];
for(int i=0; i < indexes.length; i) {
indexes[i] = i;
}
}
private int indexes[];
}
With the refactoring of the original table sort decorator, we now have a hierarchy of table decorators that we can extend at will.
Decorator applicability
In Java, developers typically use inheritance to add functionality to objects. Base classes implement shared behavior, and extensions add functionality. For example, instead of the sort and filter decorators discussed above, you could implement SortModel, FilterModel, and, perhaps, SortFilterModel classes.
Decorators prove more flexible than inheritance because the relationship between decorators and the objects they decorate can change at runtime, but relationships between base classes and their extensions are fixed at compile time.
Of course, both inheritance and the Decorator pattern have their places; in fact, inheritance is used to implement the Decorator pattern. So when should you use the Decorator pattern? It's useful when:
You have many simple behaviors you would like to combine in numerous ways at runtime. Implementing the functionality with inheritance would result in a subclass explosion for every possible combination of behaviors.
You need to transparently add functionality to individual objects at runtime. Transparently means that existing classes need not have been modified to support the extra functionality; indeed, those classes are not aware that they are being decorated.
You want to restrict the use of an object's public methods. Instead of forwarding calls to a restricted method, a decorator can veto a method by throwing an exception from the wrapper method.
Homework
For this month's homework, implement a filter decorator -- extending the TableFilterDecorator class shown in Figure 8 -- that filters high-priced items in the table shown in Figure 6.
Homework from last column
If you look at the reader classes in java.io, you will find another decorator: BufferedReader. That class buffers reads, making it more efficient than an unbuffered reader. In light of this new discovery, you might decide to make Example 3 more efficient, like so:
FileReader frdr = new FileReader(filename);
BufferedReader brdr = new BufferedReader(frdr); // "mix in" a buffered reader
LineNumberReader lrdr = new LineNumberReader(brdr);
Here are the questions from last time, followed by their correct answers:
Will the preceding code fragment compile?
Yes (provided you import java.io.BufferedReader)
How can you add two lines of code to the main method to see if the example runs faster?
Add:
long time = System.out.getCurrentMillis();
...
System.out.println("Elapsed time: " System.out.getCurrentMillis() - time);
Will the modified example run more quickly? Why or why not?
The application will run more slowly because LineNumberReader extends BufferedReader. Since line number readers are buffered to begin with, mixing in a buffered decorator will not improve performance; in fact, it will slow things down because the buffered decorator must forward calls, which slows things down.
Email
Thanks to everyone who sent me email after the first installment of this column. I received voluminous feedback with many interesting suggestions. As a notable example, Mark Riggle wrote:
I have an idea that you may already plan to incorporate. As you say in the article, object composition is preferable to inheritance. Many of the Gang of Four patterns reflect this concept by using delegation to pass a method to an object that will do the work. Strategy and Decorator are certainly two such patterns. The problem for languages like Java is the required dispatching methods. The decorator object must implement the methods of the object it is decorating, and most will likely just directly pass on to the decorated object. This is needed so that the client code is left unchanged. So whenever the decorated class changes its signature, the decorators must also be updated. This is a coupling problem solvable using some of the dynamic features added to Java. The most important one now is the dynamic proxy. Because many of the methods added to classes are merely forwarding calls to a delegated object, the dynamic proxy lets you do exactly that without actually adding the method definitions...
Thus, I think that the dynamic proxy and pattern use in Java go hand in hand, and a discussion on patterns needs to include a discussion on the dynamic proxy.
Mark is absolutely correct. Since JDK 1.3, Java directly supports the Proxy pattern. The Proxy pattern is closely related to the Decorator pattern, and is the topic for my next Java Design Patterns installment. In that article, I will show you how to use the built-in JDK support for the Proxy (and Decorator) patterns to implement the sort decorator example from this article.
下载本文示例代码
Decorate your Java codeDecorate your Java codeDecorate your Java codeDecorate your Java codeDecorate your Java codeDecorate your Java codeDecorate your Java codeDecorate your Java codeDecorate your Java codeDecorate your Java codeDecorate your Java codeDecorate your Java code