Improving
the design
The
solutions in
Design
Patterns
are organized around the question “What will change as this program
evolves?” This is usually the most important question that you can ask
about any design. If you can build your system around the answer, the results
will be two-pronged: not only will your system allow easy (and inexpensive)
maintenance, but you might also produce components that are reusable, so that
other systems can be built more cheaply. This is the promise of object-oriented
programming, but it doesn’t happen automatically; it requires thought and
insight on your part. In this section we’ll see how this process can
happen during the refinement of a system.
The
answer to the question “What will change?” for the recycling system
is a common one: more types will be added to the system. The goal of the
design, then, is to make this addition of types as painless as possible. In the
recycling program, we’d like to encapsulate all places where specific
type information is mentioned, so (if for no other reason) any changes can be
localized to those encapsulations. It turns out that this process also cleans
up the rest of the code considerably.
“Make
more objects”
This
brings up a general object-oriented design principle that I first heard spoken
by Grady
Booch: “If the design is too complicated, make more objects.” This
is simultaneously counterintuitive and ludicrously simple, and yet it’s
the most useful guideline I’ve found. (You might observe that
“making more objects” is often equivalent to “add another
level of indirection.”) In general, if you find a place with messy code,
consider what sort of class would clean that up. Often the side effect of
cleaning up the code will be a system that has better structure and is more
flexible.
Consider
first the place where
Trash
objects are created, which is a
switch
statement inside
main( ):
for(int i = 0; i < 30; i++)
switch((int)(Math.random() * 3)) {
case 0 :
bin.addElement(new
Aluminum(Math.random() * 100));
break;
case 1 :
bin.addElement(new
Paper(Math.random() * 100));
break;
case 2 :
bin.addElement(new
Glass(Math.random() * 100));
}
This
is definitely messy, and also a place where you must change code whenever a new
type is added. If new types are commonly added, a better solution is a single
method that takes all of the necessary information and produces a handle to an
object of the correct type, already upcast to a trash object. In
Design
Patterns
this is broadly referred to as a creational
pattern
(of which there are several). The specific pattern that will be applied here is
a variant of the Factory
Method
.
Here, the factory method is a
static
member of
Trash,
but more commonly it is a method that is overridden in the derived class.
The
idea of the factory method is that you pass it the essential information it
needs to know to create your object, then stand back and wait for the handle
(already upcast to the base type) to pop out as the return value. From then on,
you treat the object polymorphically. Thus, you never even need to know the
exact type of object that’s created. In fact, the factory method hides it
from you to prevent accidental misuse. If you want to use the object without
polymorphism, you must explicitly use RTTI and casting.
But
there’s a little problem, especially when you use the more complicated
approach (not shown here) of making the factory method in the base class and
overriding it in the derived classes. What if the information required in the
derived class requires more or different arguments? “Creating more
objects” solves this problem. To implement the factory method, the
Trash
class gets a new method called
factory.
To hide the creational data, there’s a new class called
Info
that contains all of the necessary information for the
factory
method to create the appropriate
Trash
object. Here’s a simple implementation of
Info:
class Info {
int type;
// Must change this to add another type:
static final int MAX_NUM = 4;
double data;
Info(int typeNum, double dat) {
type = typeNum % MAX_NUM;
data = dat;
}
}
An
Info
object’s only job is to hold information for the
factory( )
method. Now, if there’s a situation in which
factory( )
needs more or different information to create a new type of
Trash
object, the
factory( )
interface doesn’t need to be changed. The
Info
class can be changed by adding new data and new constructors, or in the more
typical object-oriented fashion of subclassing.
The
factory( )
method for this simple example looks like this:
static Trash factory(Info i) {
switch(i.type) {
default: // To quiet the compiler
case 0:
return new Aluminum(i.data);
case 1:
return new Paper(i.data);
case 2:
return new Glass(i.data);
// Two lines here:
case 3:
return new Cardboard(i.data);
}
}
Here,
the determination of the exact type of object is simple, but you can imagine a
more complicated system in which
factory( )
uses an elaborate algorithm. The point is that it’s now hidden away in
one place, and you know to come to this place when you add new types.
The
creation of new objects is now much simpler in
main( ):
for(int i = 0; i < 30; i++)
bin.addElement(
Trash.factory(
new Info(
(int)(Math.random() * Info.MAX_NUM),
Math.random() * 100)));
An
Info
object is created to pass the data into
factory( ),
which in turn produces some kind of
Trash
object on the heap and returns the handle that’s added to the
Vector
bin.
Of course, if you change the quantity and type of argument, this statement will
still need to be modified, but that can be eliminated if the creation of the
Info
object is automated. For example, a
Vector
of arguments can be passed into the constructor of an
Info
object (or directly into a
factory( )
call, for that matter). This requires that the arguments be parsed and checked
at runtime, but it does provide the greatest flexibility.
You
can see from this code what “vector
of change” problem the factory is responsible for solving: if you add new
types to the system (the change), the only code that must be modified is within
the factory, so the factory isolates the effect of that change.
A
pattern for prototyping creation
A
problem with the design above is that it still requires a central location
where all the types of the objects must be known: inside the
factory( )
method. If new types are regularly being added to the system, the
factory( )
method must be changed for each new type. When you discover something like
this, it is useful to try to go one step further and move
all
of the information about the type – including its creation – into
the class representing that type. This way, the only thing you need to do to
add a new type to the system is to inherit a single class.
To
move the information concerning type creation into each specific type of
Trash,
the
“prototype”
pattern (from the
Design
Patterns
book)
will be used. The general idea is that you have a master sequence of objects,
one of each type you’re interested in making. The objects in this
sequence are used
only
for making new objects, using an operation that’s not unlike the clone( )
scheme built into Java’s root class
Object.
In this case, we’ll name the cloning method
tClone( ).
When
you’re ready to make a new object, presumably you have some sort of
information that establishes the type of object you want to create, then you
move through the master sequence comparing your information with whatever
appropriate information is in the prototype objects in the master sequence.
When you find one that matches your needs, you clone it.
In
this scheme there is no hard-coded information for creation. Each object knows
how to expose appropriate information and how to clone itself. Thus, the
factory( )
method doesn’t need to be changed when a new type is added to the system.
One
approach to the problem of prototyping is to add a number of methods to support
the creation of new objects. However, in Java 1.1 there’s already support
for creating new objects if you have a handle to the
Class
object. With Java
1.1
reflection
(introduced in Chapter 11) you can call a constructor even if you have only a
handle to the
Class
object. This is the perfect solution for the prototyping problem.
The
list of prototypes will be represented indirectly by a list of handles to all
the
Class
objects you want to create. In addition, if the prototyping fails, the
factory( )
method will assume that it’s because a particular
Class
object wasn’t in the list, and it will attempt to load it. By loading the
prototypes dynamically like this, the
Trash
class doesn’t need to know what types it is working with, so it
doesn’t need any modifications when you add new types. This allows it to
be easily reused throughout the rest of the chapter.
//: Trash.java
// Base class for Trash recycling examples
package c16.trash;
import java.util.*;
import java.lang.reflect.*;
public abstract class Trash {
private double weight;
Trash(double wt) { weight = wt; }
Trash() {}
public abstract double value();
public double weight() { return weight; }
// Sums the value of Trash in a bin:
public static void sumValue(Vector bin) {
Enumeration e = bin.elements();
double val = 0.0f;
while(e.hasMoreElements()) {
// One kind of RTTI:
// A dynamically-checked cast
Trash t = (Trash)e.nextElement();
val += t.weight() * t.value();
System.out.println(
"weight of " +
// Using RTTI to get type
// information about the class:
t.getClass().getName() +
" = " + t.weight());
}
System.out.println("Total value = " + val);
}
// Remainder of class provides support for
// prototyping:
public static class PrototypeNotFoundException
extends Exception {}
public static class CannotCreateTrashException
extends Exception {}
private static Vector trashTypes =
new Vector();
public static Trash factory(Info info)
throws PrototypeNotFoundException,
CannotCreateTrashException {
for(int i = 0; i < trashTypes.size(); i++) {
// Somehow determine the new type
// to create, and create one:
Class tc =
(Class)trashTypes.elementAt(i);
if (tc.getName().indexOf(info.id) != -1) {
try {
// Get the dynamic constructor method
// that takes a double argument:
Constructor ctor =
tc.getConstructor(
new Class[] {double.class});
// Call the constructor to create a
// new object:
return (Trash)ctor.newInstance(
new Object[]{new Double(info.data)});
} catch(Exception ex) {
ex.printStackTrace();
throw new CannotCreateTrashException();
}
}
}
// Class was not in the list. Try to load it,
// but it must be in your class path!
try {
System.out.println("Loading " + info.id);
trashTypes.addElement(
Class.forName(info.id));
} catch(Exception e) {
e.printStackTrace();
throw new PrototypeNotFoundException();
}
// Loaded successfully. Recursive call
// should work this time:
return factory(info);
}
public static class Info {
public String id;
public double data;
public Info(String name, double data) {
id = name;
this.data = data;
}
}
} ///:~
The
basic
Trash
class and
sumValue( )
remain as before. The rest of the class supports the prototyping pattern. You
first see two inner
classes (which are made
static,
so they are inner classes only for code organization purposes) describing
exceptions that can occur. This is followed by a
Vector
trashTypes
,
which is used to hold the
Class
handles.
In
Trash.factory( ),
the
String
inside the
Info
object
id
(a
different version of the
Info
class than that of the prior discussion) contains the type name of the
Trash
to
be created; this
String
is compared to the
Class
names in the list. If there’s a match, then that’s the object to
create. Of course, there are many ways to determine what object you want to
make. This one is used so that information read in from a file can be turned
into objects.
Once
you’ve discovered which kind of
Trash
to create, then the reflection
methods come into play. The getConstructor( )
method takes an argument that’s an array of Class
handles. This array represents the arguments, in their proper order, for the
constructor that you’re looking for. Here, the array
is dynamically created using the Java 1.1
array-creation syntax:
new
Class[] {double.class}
This
code assumes that every
Trash
type has a constructor that takes a
double
(and
notice that
double.class
is distinct from
Double.class).
It’s also possible, for a more flexible solution, to call getConstructors( ),
which returns an array of the possible constructors.
What
comes back from
getConstructor( )
is
a handle to a Constructor
object (part of
java.lang.reflect).
You call the constructor dynamically with the method newInstance( ),
which takes an array of
Object
containing the actual arguments. This array is again created using the Java 1.1
syntax:
new
Object[]{new Double(info.data)}
In
this case, however, the
double
must be placed inside a wrapper class so that it can be part of this array of
objects. The process of calling
newInstance( )
extracts the
double,
but you can see it is a bit confusing – an argument might be a
double
or
a
Double,
but when you make the call you must always pass in a
Double.
Fortunately, this issue exists only for the primitive types.
Once
you understand how to do it, the process of creating a new object given only a
Class
handle is remarkably simple. Reflection also allows you to call methods in this
same dynamic fashion.
Of
course, the appropriate
Class
handle might not be in the
trashTypes
list. In this case, the
return
in the inner loop is never executed and you’ll drop out at the end. Here,
the program tries to rectify the situation by loading the
Class
object dynamically and adding it to the
trashTypes
list. If it still can’t be found something is really wrong, but if the
load is successful then the
factory
method is called recursively
to try again.
As
you’ll see, the beauty of this design is that this code doesn’t
need to be changed, regardless of the different situations it will be used in
(assuming that all
Trash
subclasses contain a constructor that takes a single
double
argument).
Trash
subclasses
To
fit into the prototyping scheme, the only thing that’s required of each
new subclass of
Trash
is that it contain a constructor that takes a
double
argument. Java 1.1
reflection handles everything else.
Here
are the different types of
Trash,
each in their own file but part of the
Trash
package (again, to facilitate reuse within the chapter):
//: Aluminum.java
// The Aluminum class with prototyping
package c16.trash;
public class Aluminum extends Trash {
private static double val = 1.67f;
public Aluminum(double wt) { super(wt); }
public double value() { return val; }
public static void value(double newVal) {
val = newVal;
}
} ///:~
//: Paper.java
// The Paper class with prototyping
package c16.trash;
public class Paper extends Trash {
private static double val = 0.10f;
public Paper(double wt) { super(wt); }
public double value() { return val; }
public static void value(double newVal) {
val = newVal;
}
} ///:~
//: Glass.java
// The Glass class with prototyping
package c16.trash;
public class Glass extends Trash {
private static double val = 0.23f;
public Glass(double wt) { super(wt); }
public double value() { return val; }
public static void value(double newVal) {
val = newVal;
}
} ///:~
And
here’s a new type of
Trash:
//: Cardboard.java
// The Cardboard class with prototyping
package c16.trash;
public class Cardboard extends Trash {
private static double val = 0.23f;
public Cardboard(double wt) { super(wt); }
public double value() { return val; }
public static void value(double newVal) {
val = newVal;
}
} ///:~
You
can see that, other than the constructor, there’s nothing special about
any of these classes.
Parsing
Trash from an external file
The
information about
Trash
objects will be read from an outside file. The file has all of the necessary
information about each piece of trash on a single line in the form
Trash:weight,
such as:
c16.Trash.Glass:54
c16.Trash.Paper:22
c16.Trash.Paper:11
c16.Trash.Glass:17
c16.Trash.Aluminum:89
c16.Trash.Paper:88
c16.Trash.Aluminum:76
c16.Trash.Cardboard:96
c16.Trash.Aluminum:25
c16.Trash.Aluminum:34
c16.Trash.Glass:11
c16.Trash.Glass:68
c16.Trash.Glass:43
c16.Trash.Aluminum:27
c16.Trash.Cardboard:44
c16.Trash.Aluminum:18
c16.Trash.Paper:91
c16.Trash.Glass:63
c16.Trash.Glass:50
c16.Trash.Glass:80
c16.Trash.Aluminum:81
c16.Trash.Cardboard:12
c16.Trash.Glass:12
c16.Trash.Glass:54
c16.Trash.Aluminum:36
c16.Trash.Aluminum:93
c16.Trash.Glass:93
c16.Trash.Paper:80
c16.Trash.Glass:36
c16.Trash.Glass:12
c16.Trash.Glass:60
c16.Trash.Paper:66
c16.Trash.Aluminum:36
c16.Trash.Cardboard:22
Note
that the class path must be included when giving the class names, otherwise the
class will not be found.
To
parse this, the line is read and the String
method
indexOf( )
produces the index of the ‘
:’.
This is first used with the
String
method
substring( )
to
extract the name of the trash type, and next to get the weight that is turned
into a
double
with
the
static
Double.valueOf( )
method.
The trim( )
method removes white space at both ends of a string.
The
Trash
parser
is placed in a separate file since it will be reused throughout this chapter:
//: ParseTrash.java
// Open a file and parse its contents into
// Trash objects, placing each into a Vector
package c16.trash;
import java.util.*;
import java.io.*;
public class ParseTrash {
public static void
fillBin(String filename, Fillable bin) {
try {
BufferedReader data =
new BufferedReader(
new FileReader(filename));
String buf;
while((buf = data.readLine())!= null) {
String type = buf.substring(0,
buf.indexOf(':')).trim();
double weight = Double.valueOf(
buf.substring(buf.indexOf(':') + 1)
.trim()).doubleValue();
bin.addTrash(
Trash.factory(
new Trash.Info(type, weight)));
}
data.close();
} catch(IOException e) {
e.printStackTrace();
} catch(Exception e) {
e.printStackTrace();
}
}
// Special case to handle Vector:
public static void
fillBin(String filename, Vector bin) {
fillBin(filename, new FillableVector(bin));
}
} ///:~
In
RecycleA.java,
a
Vector
was used to hold the
Trash
objects. However, other types of collections can be used as well. To allow for
this, the first version of
fillBin( )
takes a handle to a
Fillable,
which is simply an
interface
that supports a method called
addTrash( ):
//: Fillable.java
// Any object that can be filled with Trash
package c16.trash;
public interface Fillable {
void addTrash(Trash t);
} ///:~
Anything
that supports this interface can be used with
fillBin.
Of course,
Vector
doesn’t implement
Fillable,
so it won’t work. Since
Vector
is used in most of the examples, it makes sense to add a second overloaded
fillBin( )
method that takes a
Vector.
The
Vector
can be used as a
Fillable
object using an adapter class:
//: FillableVector.java
// Adapter that makes a Vector Fillable
package c16.trash;
import java.util.*;
public class FillableVector implements Fillable {
private Vector v;
public FillableVector(Vector vv) { v = vv; }
public void addTrash(Trash t) {
v.addElement(t);
}
} ///:~
You
can see that the only job of this class is to connect
Fillable’s
addTrash( )
method to
Vector’s
addElement( ).
With this class in hand, the overloaded
fillBin( )
method can be used with a
Vector
in
ParseTrash.java:
public static void
fillBin(String filename, Vector bin) {
fillBin(filename, new FillableVector(bin));
}
This
approach works for any collection class that’s used frequently.
Alternatively, the collection class can provide its own adapter that implements
Fillable.
(You’ll see this later, in
DynaTrash.java.)
Recycling
with prototyping
Now
you can see the revised version of
RecycleA.java
using the prototyping
technique:
//: RecycleAP.java
// Recycling with RTTI and Prototypes
package c16.recycleap;
import c16.trash.*;
import java.util.*;
public class RecycleAP {
public static void main(String[] args) {
Vector bin = new Vector();
// Fill up the Trash bin:
ParseTrash.fillBin("Trash.dat", bin);
Vector
glassBin = new Vector(),
paperBin = new Vector(),
alBin = new Vector();
Enumeration sorter = bin.elements();
// Sort the Trash:
while(sorter.hasMoreElements()) {
Object t = sorter.nextElement();
// RTTI to show class membership:
if(t instanceof Aluminum)
alBin.addElement(t);
if(t instanceof Paper)
paperBin.addElement(t);
if(t instanceof Glass)
glassBin.addElement(t);
}
Trash.sumValue(alBin);
Trash.sumValue(paperBin);
Trash.sumValue(glassBin);
Trash.sumValue(bin);
}
} ///:~
All
of the
Trash
objects, as well as the
ParseTrash
and support classes, are now part of the package
c16.trash
so they are simply imported.
The
process of opening the data file containing
Trash
descriptions and the parsing of that file have been wrapped into the
static
method
ParseTrash.fillBin( ),
so now it’s no longer a part of our design focus. You will see that
throughout the rest of the chapter, no matter what new classes are added,
ParseTrash.fillBin( )
will continue to work without change, which indicates a good design.
In
terms of object creation, this design does indeed severely localize the changes
you need to make to add a new type to the system. However, there’s a
significant problem in the use of RTTI that shows up clearly here. The program
seems to run fine, and yet it never detects any cardboard, even though there is
cardboard in the list! This happens
because
of the use of RTTI, which looks for only the types that you tell it to look
for. The clue that RTTI
is being misused is that
every
type in the system
is
being tested, rather than a single type or subset of types. As you will see
later, there are ways to use polymorphism instead when you’re testing for
every type. But if you use RTTI a lot in this fashion, and you add a new type
to your system, you can easily forget to make the necessary changes in your
program and produce a difficult-to-find bug. So it’s worth trying to
eliminate RTTI in this case, not just for aesthetic reasons – it produces
more maintainable code.