Objects are organized into types. For example: a film can be typed according to its genre: comedy, drama, SciFi, etc; its rating: G, PG, R, X; or it's quality: excellent, good, fair, poor, etc.
It's natural to identify the type of an object with its class, but this can lead to unwieldy inheritance hierarchies or inheritance hierarchies with multiple representations of the same object. This, in turn, can lead to synchronization problems.
For example, we can introduce subclasses of Film for each of the above types:
But where in this hierarchy might one find an object representing a film like The Matrix?
Certainly there would be instances of this film in the SciFi, Excellent, and R classes.
Now suppose this diagram represents a database used by an
online event calendar for
We could attempt to fix this problem with the following design:
Now The Matrix is represented by a single object, which is an instance of SciFiRExcellent.
But this hierarchy is unwieldy. If there are three genres, four ratings, and five qualities, then there will be a total of:
1 + 3 + 3 * 4 + 3 * 4 * 5 = 76 classes
Adding a fourth genre, Romance for example, will force us to add 25 more classes!
If the type of an object is only used as a classification scheme, if a difference in type doesn't imply a difference in attributes or behavior, then consider representing the type of an object by another object.
Here's the general pattern:
A more interesting version of the pattern allows types to be instances:
With this in mind, we sketch generic implementations of Instance and InstanceType below:
class Instance {
private InstanceType type;
public Instance(InstanceType type) {
this.type = type;
}
public InstanceType getType() {
return type;
}
}
class InstanceType {
private String name;
public InstanceType(String typeName) {
name = typeName;
}
public Instance makeInstance() {
return new Instance(this);
}
}
Note that one of the duties a type object can perform is to construct appropriately typed instances.
Note that this pattern provides a measure of runtime type identification.
In a more advanced implementation InstanceType extends Instance:
class InstanceType extends Instance {
private String name;
public InstanceType(String typeName) {
super(???);
name = typeName;
}
public Instance makeInstance() {
return new Instance(this);
}
}
This means InstanceType inherits the type field from Instance. What should the value of this field be? What should the value of ??? be?
There are two choices: simple and complicated. In the simple option we define a special instance type that serves as the type of all types:
class InstanceType extends Instance {
public static final InstanceType
TYPE_TYPE =
new
InstanceType("TypeType");
private String name;
public InstanceType(String typeName) {
super(TYPE_TYPE);
name = typeName;
}
// etc.
}
In the complex variation we define different types of types:
Of course Instance and InstanceType are template parameters. Instantiating the Types as Object Pattern involves substituting domain classes and interfaces for these parameters. For example:
Instance InstanceType
Employee Manager, Worker, Programmer,
etc.
Film Genre, Rating
Product Book, CD, DVD, etc.
In the Video Rental domain there are two opportunities to use the Types as Objects Pattern:
We can solve the problem of multiple representations of the same film and unwieldy inheritance hierarchies by associating with each film object three different type objects:
In this example we are using enumeration classes (classes with a small fixed number of instances) as our types.
Adding a new genre, Romance, say, only involves creating a new instance of the Genre class. This can be done at runtime, without changing the design.
Suppose that in addition to altitude and air speed, military planes had a payload attribute (i.e., how many bombs can it carry). If types are objects, then we can simply add payload to the attribute list for military planes:
But if we use type tags, then here's what a typical military plane looks like:
Where are we supposed to put payload?
Suppose also that the implementation of the takeoff operation depends on the type of airplane. If types are objects, then we will need a multi-way conditional to implement takeoff:
class Airplane {
AirplaneType type;
void takeoff() {
if (type == AirplaneType.MILITARY) {
/* algorithm 1 */ }
else if (type ==
AirplaneType.COMMERCIAL) { /* algorithm 2 */ }
else if (type == AirplaneType.PRIVATE)
{ /* algorithm 3 */ }
else error("Unknown airplane
type");
}
// etc.
}
Object-oriented programmers dislike this style of coding because it centralizes intelligence. It's better to distribute intelligence by allowing each subclass of Airplane to redefine the takeoff method.
We can overcome some of the disadvantages of the Types as Object Pattern by associating property lists with our Instance and InstanceType classes. Here's the design:
The implementation (sketch) is a little less complicated:
class Instance {
protected InstanceType type;
private
Map<String, Instance> properties;
public void put(String name, Instance
value) { ... }
public void set(String name, Instance
value) throws Exception {
...
}
public Instance get(String name) {
return properties.get(name); // =
null if no such property
}
public Instance(InstanceType type)
{
this.type = type;
properties
= new Hashtable<String, Instance>();
}
public InstanceType getType() {
return type;
}
}
class InstanceType {
private String name;
private
Map<String, InstanceType> propertyTypes;
public void put(String name,
InstanceType propertyType) {
propertyTypes.put(name,
propertyType);
}
public InstanceType get(String name) {
return propertyTypes.get(name); // =
null if no such property
}
public InstanceType(String
typeName) {
name = typeName;
propertyTypes
= new Hashtable<String, InstanceType>();
}
public Instance makeInstance() {
return new Instance(this);
}
}
When adding a new property to
an instance, the type of this instance must be added to the corresponding
property types map:
public void put(String name, Instance value) {
properties.put(name, value);
type.put(name,
value.getType());
}
Setting the value of an
existing property provides an opportunity for type checking:
public void set(String name, Instance value) throws Exception {
InstanceType propType = type.get(name);
if
(!propType.equals(value.getType())) {
throw new
Exception("Incompatable Types!");
}
properties.put(name, value);
}
Property lists become even more interesting if we allow methods to be instances:
abstract class Method extends Instance {
public Method() {
super(InstanceType.METHOD_TYPE);
}
public abstract Instance apply(Instance
self, Instance[] args);
}
We need some predefined types:
class InstanceType {
public static final InstanceType
METHOD_TYPE =
new
InstanceType("MethodType");
public static final InstanceType
VOID_TYPE =
new
InstanceType("VoidType");
etc.
}
It's also useful to have a special void value:
class Instance {
public static final Instance VOID =
InstanceType.VOID_TYPE.makeInstance();
// etc.
}
We can define procedures as methods that return void.
The Types-as-Objects Pattern (TOP) can be found remotely or locally.