Types as Objects Pattern

Problem

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 San Francisco. Suppose each film has attributes indicating when and where it is showing in San Francisco. One of the theaters decides to change the time it will show the movie. The database administrator must update the show time attribute of three objects representing this movie. Failure to update all three will lead to a synchronization error. The database will be inconsistent. People looking for the show time will get different answers depending on if they are looking by genre, quality, or rating.

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!

Solution

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:

Implementation

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.

InstanceTypes as Instances

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:

Example

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:

Advantages of type objects

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.

Disadvantages of types as objects

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.

The Dynamic Object Model (Property Lists)

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);
}

Methods

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.

Lazy Load

Reference

The Types-as-Objects Pattern (TOP) can be found remotely or locally.