CATEGORII DOCUMENTE |
Asp | Autocad | C | Dot net | Excel | Fox pro | Html | Java |
Linux | Mathcad | Photoshop | Php | Sql | Visual studio | Windows | Xml |
The nature of this problem is that the trash is thrown unclassified into a single bin, so the specific type information is lost. But later, the specific type information must be recovered to properly sort the trash. In the initial solution, RTTI (described in Chapter 11) is used.
This is not a trivial design because it has an added constraint. That's what makes it interesting - it's more like the messy problems you're likely to encounter in your work. The extra constraint is that the trash arrives at the trash recycling plant all mixed together. The program must model the sorting of that trash. This is where RTTI comes in: you have a bunch of anonymous pieces of trash, and the program figures out exactly what type they are.
//: RecycleA.java
// Recycling with RTTI
package c16.recyclea;
import java.util.*;
import java.io.*;
abstract class Trash
abstract double value();
double weight()
// Sums the value of Trash in a bin:
static void sumValue(Vector bin)
System.out.println('Total value = ' + val);
}
}
class Aluminum extends Trash
double value()
static void value(double newval)
}
class Paper extends Trash
double value()
static void value(double newval)
}
class Glass extends Trash
double value()
static void value(double newval)
}
public class RecycleA
Vector
glassBin = new Vector(),
paperBin = new Vector(),
alBin = new Vector();
Enumeration sorter = bin.elements();
// Sort the Trash:
while(sorter.hasMoreElements())
Trash.sumValue(alBin);
Trash.sumValue(paperBin);
Trash.sumValue(glassBin);
Trash.sumValue(bin);
}
} ///:~
The first thing you'll notice is the package statement:
package c16.recyclea;
This means that in the source code listings available for the book, this file will be placed in the subdirectory recyclea that branches off from the subdirectory c16 (for Chapter 16). The unpacking tool in Chapter 17 takes care of placing it into the correct subdirectory. The reason for doing this is that this chapter rewrites this particular example a number of times and by putting each version in its own package the class names will not clash.
Several Vector objects are created to hold Trash handles. Of course, Vectors actually hold Objects so they'll hold anything at all. The reason they hold Trash (or something derived from Trash) is only because you've been careful to not put in anything except Trash. If you do put something "wrong" into the Vector, you won't get any compile-time warnings or errors - you'll find out only via an exception at run-time.
When the Trash handles are added, they lose their specific identities and become simply Object handles (they are upcast). However, because of polymorphism the proper behavior still occurs when the dynamically-bound methods are called through the Enumeration sorter, once the resulting Object has been cast back to Trash. sumValue( ) also uses an Enumeration to perform operations on every object in the Vector.
It looks silly to upcast the types of Trash into a collection holding base type handles, and then turn around and downcast. Why not just put the trash into the appropriate receptacle in the first place? (Indeed, this is the whole enigma of recycling). In this program it would be easy to repair, but sometimes a system's structure and flexibility can benefit greatly from downcasting.
The program satisfies the design requirements: it works. This might be fine as long as it's a one-shot solution. However, a useful program tends to evolve over time, so you must ask, "What if the situation changes?" For example, cardboard is now a valuable recyclable commodity, so how will that be integrated into the system (especially if the program is large and complicated). Since the above type-check coding in the switch statement could be scattered throughout the program, you must go find all that code every time a new type is added, and if you miss one the compiler won't give you any help by pointing out an error.
The key to the misuse of RTTI here is that every type is tested. If you're looking for only a subset of types because that subset needs special treatment, that's probably fine. But if you're hunting for every type inside a switch statement, then you're probably missing an important point, and definitely making your code less maintainable. In the next section we'll look at how this program evolved over several stages to become much more flexible. This should prove a valuable example in program 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.
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))
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
}
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)
}
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 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
Trash()
public abstract double value();
public double weight()
// Sums the value of Trash in a bin:
public static void sumValue(Vector bin)
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 );
// Call the constructor to create a
// new object:
return (Trash)ctor.newInstance(
new Object[]);
} catch(Exception ex)
}
}
// Class was not in the list. Try to load it,
// but it must be in your class path!
try catch(Exception e)
// Loaded successfully. Recursive call
// should work this time:
return factory(info);
}
public static class Info
}
} ///:~
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[]
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[]
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).
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
public double value()
public static void value(double newVal)
} ///:~
//: Paper.java
// The Paper class with prototyping
package c16.trash;
public class Paper extends Trash
public double value()
public static void value(double newVal)
} ///:~
//: Glass.java
// The Glass class with prototyping
package c16.trash;
public class Glass extends Trash
public double value()
public static void value(double newVal)
} ///:~
And here's a new type of Trash:
//: Cardboard.java
// The Cardboard class with prototyping
package c16.trash;
public class Cardboard extends Trash
public double value()
public static void value(double newVal)
} ///:~
You can see that, other than the constructor, there's nothing special about any of these classes.
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
data.close();
} catch(IOException e) catch(Exception e)
}
// Special case to handle Vector:
public static void
fillBin(String filename, Vector 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 ///:~
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
public void addTrash(Trash 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)
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.)
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())
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.
Politica de confidentialitate | Termeni si conditii de utilizare |
Vizualizari: 912
Importanta:
Termeni si conditii de utilizare | Contact
© SCRIGROUP 2024 . All rights reserved