Improving the design

Bruce Eckel’s Thinking in Java Contents | Prev | Next

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”

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.

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.

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;
    }
  }
} ///:~ 

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.

new
Class[] {double.class}

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.

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

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.

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

//: 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.

More by Author

Must Read