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