Interaction

How do entities interact with each other when they meet? Since we are now talking about the implementation of custom classes rather than framework classes, it is important to explore different possibilities. Different users of the framework might well make different choices.

One approach is to represent entities using a single class, with each entity object containing a character code or enumerated constant to specify its type. When an entity accesses another entity, the type of the second entity can be used to decide what to do, e.g.:

void act() {
    Entity other = find(...);
    switch (other.type()) {
        case Space: ... break;
        case Star: ... break;
        default: ... break;
    }
}

A simple example of this is in the maze package. Two classes, Item (which is the equivalent of Entity) and Maze implement a maze game, which is a simple cut-down version of wanderer. There are disadvantages of this approach in larger projects:

Visitors

Suppose instead that each type of entity is represented by a separate class, with Entity as a base class. Then it is easy to imagine trying to use switch statements like this:

void act() {
    Entity other = find(...);
    switch (other.getClass()) {
        case Space: ... break;
        case Star: ... break;
        default: ... break;
    }
}

This doesn't work because you can't switch on classes in Java. Dynamic dispatch is the preferred way of deciding which class an object has within a family. The visitor pattern is designed to be used when dynamic dispatch is desired on method arguments but, as with Java, the language doesn't support that. In this case, only a fairly simple version of the visitor pattern is needed. Two methods can be set up, meet and isMetBy, say, which can be used like this:

void act() {
    Entity other = find(...);
    other.isMetBy(this);
}

void meet(Space s) { ... }

void meet(Star s) { ... }

void meet(Entity e) { ... }

The isMetBy function acts like a switch, and the overloaded variants of the meet function form the cases, with the Entity version acting as a default. Comparing this with the usual notation in descriptions of the visitor pattern, isMetBy is the equivalent of accept, and meet is the equivalent of visit.

Some extra code is required to set this up. Each entity class needs to include a one-line definition of isMetBy:

void isMetBy(Entity e) { e.meet(this); }

Despite the definition being the same in every class, it has to be overridden in each class rather than being defined just once in the base Entity class, because it is important that the compiler should know that the variable this has the desired specific class, not just class Entity.

In addition, in the base Entity class, the meet method variants need to be given default implementations:

void meet(Space e) { meet((Entity) e); }
void meet(Star e) { meet((Entity) e); }
...
void meet(Entity e) {}

These definitions ensure that, in each entity class, not all the variants need to be overridden, and the ones that aren't end up calling the Entity variant.

This use of the visitor pattern does have some drawbacks.

A Compromise

A compromise is possible in which the code in each entity class looks like this:

void act() {
    Entity other = find(...);
    meet(other);
}

void meetSpace(Entity s) { ... }

void meetStar(Entity s) { ... }

void meetEntity(Entity e) { ... }

The meet function is defined in the Entity class, using a switch on the type of its argument. But there is only one switch shared by all the classes, instead of one switch per class. The methods meetSpace etc. are given default implementations in the Entity class as before. The code in each of the meet variants only knows that the other entity has class Entity, not its specific class, but that is a minor drawback. It is worth it to be able to compile the entity classes one by one.

This compromise approach is illustrated in the wanderer package. Many different inert types of entity are handled by a simgle Thing class.