Dynamic Behaviors in Java

Dynamically adapt program behavior at runtime

Imagine you are creating a game that contains many different classes of things; there are players, monsters, and objects that can be picked up. In addition, magic spells can change the classification of things for a set time period. A monster might transform into an object that can be carried, or an object might pick itself up and walk around like a monster.

Implementing this sort of arrangement with a traditional inheritance hierarchy and data-driven attributes is a difficult housekeeping task, as things must transform back to their initial characteristics as soon as the spell ends or is dispelled. If every new spell implementation required the developer to modify all of the methods of the game's physics model to check for the existence of each enchantment, the application would quickly become unmaintainable. It would also prove difficult to find all of the source code associated with any existing spell, as said code would be scattered throughout the application. A better paradigm is needed that allows the common effects of each spell to exist together in one class that can be used to change the effect of many diverse methods throughout the application.

Dynamic Behaviors, a design pattern similar to the Chain of Responsibility pattern, is ideally suited for applications that must change the class of objects fluidly at runtime. MacApp 3.0 used behaviors to implement dynamic "adorners" that could be used to change the way objects in the user interface were drawn. The C++ implementation relied on multiple inheritance and pointer fields inside the behavior objects themselves. This article presents a more flexible alternative that allows the same behavior to modify multiple objects without relying on multiple inheritance or other features unavailable in Java.

This implementation's design puts a high premium on the simplicity of defining new behavior classes; applications that need behaviors may require numerous classes, so they should be as easy as possible to write.

Flow of control in behaviors

Java is not a dynamic language; to simulate a dynamic language's features in Java, you must add a certain amount of "glue" to each class method that may be modified by appending behaviors to that class's object instance. To do this, first split method foo() into two methods: foo() and fooDefaultBehavior(). foo() is called the dispatch method and fooDefaultBehavior() is the default behavior method (naturally enough).

Figure 1 shows the flow of control involved. When a caller invokes the method foo(), conceptually, she expects to execute the code in the default behavior method. If behaviors are attached to the object, though, they are each allowed to override foo()'s behavior; they are able to perform different operations, invoke the default behavior, and modify the returned result.

Figure 1. Dynamic Behavior flow of control

The foo() method first creates a behavior iterator and then dispatches to the first object in the behavior chain. The objects in the behavior chain contain methods that all take the behavior iterator as their first parameter and use it to obtain a reference to the next behavior in the call chain. When the foo() method creates the iterator via getInheritanceChain, it initializes the iterator such that fooDefaultBehavior() is the last behavior in the chain. There is always a terminating default behavior, even if it does nothing. Behavior objects can rely on this invariant and always call the next behavior without checking for null pointers, which simplifies the code.

The magic in the Dynamic Behaviors pattern lies in the implementation of the behavior iterator. The use pattern employed in each method is relatively simple, which is a highly desirable characteristic of a pattern that will be employed frequently in applications that use it.

Using and defining behaviors

This section describes those requirements for making a method that can be modified by Dynamic Behaviors and for defining a Java object that can be used as a behavior. Special capabilities of full-class behaviors (as explained below) are also described.

Set up your project

The behaviors distribution comes with everything you need to use behaviors in your project; see Resources for the download link. Add all of the .jar files in the lib directory to your CLASSPATH, and put the bin directory in your PATH environment variable.

If you'd like to rebuild the project, just use the make clean all test-all command and the .jar file and Javadocs will regenerate. (Someday I'll make an Ant build.xml file.) There are two test directories: one called test and the other called test-withgenerator. The latter uses the code generator, whereas the former uses handwritten dispatch code. Both techniques are described below.

Requirement for dynamic behavior use in Java

The requirements for classes that use behaviors are minimal. A BehaviorList that contains the object's behaviors must be maintained. The behavior list is used as follows:

Listing 1. Dynamic behavior dispatch example

 public int foo(int value)
{
   Iterator chain = fBehaviorList.getInheritanceChain(FooInterface.class, 
      new FooInterface()
      {
         public int foo(Iterator chain, int value)
         {
            return fooDefaultBehavior(value);
         }
      }
   );
   return ((FooInterface) chain.next()).foo(chain, value);
}
public int fooDefaultBehavior(int value)
{
   return value + 17;
}

FooInterface must be defined, of course. Usually, the most convenient way is to declare the interface inside the class that has the behavior dispatch method, naming the interface after the behavior method it implements, and defining but one method per behavior interface. External clients would then refer to FooInterface as MyClass.FooInterface. Of course, you may also define FooInterface in FooInterface.java, but, if you follow this route, beware of an explosion of interface files in an application that uses behaviors extensively.

If the behavior dispatch code looks a bit confusing, not to worry—a useful code generator can keep your class simple and still allow the use of Dynamic Behaviors. Skip ahead to Listing 2 if you don't care how the dynamic behavior dispatch mechanism works.

Note in Listing 1 the use of anonymous classes in the dispatch method. This allows for the easy creation of new behavior classes. Note also that the behavior iterator's first parameter is always the class object of the behavior interface being iterated. This additional magic simplifies behavior dispatching, as the behavior iterator skips any behavior in the chain that does not implement the named interface. The anonymous class implementation, also provided to the behavior iterator as its second parameter, is required, even if the method's default behavior does nothing. This constraint ensures that a terminating object is always at the end of the behavior chain, an invariant that simplifies the code in each behavior class (as described below).

After the iterator of behavior interfaces is obtained, we use it to acquire a reference to the first matching interface in the list with chain.next(), typecast it to FooInterface (which is guaranteed to always work), and call the appropriate method of the first behavior found. If no behaviors are associated with the specified interface in the behavior list, then the behavior inheritance chain iterator will return the anonymous class instance created on the previous line, which does nothing but call the default behavior function below.

You might think that the above pattern could be simplified by moving the implementation of fooDefaultBehavior() inside the anonymous new FooInterface(); however, doing so is not recommended. Keeping the behavior dispatch method separate from the default behavior makes the code easier to read. Once you understand the pattern, there is no need to read the dispatch method, as all of the logic appears in the default behavior.

You can keep the redundant dispatch methods out of your code completely if you use a code generator to create them for you. To do so, insert some special directives in your Javadoc comment sections similar to those used in XDoclet (see Resources). An example is shown in Listing 2.

Listing 2. Build the dispatch method with a code generator

 /**
 * @INgen.behaviordispatch
 *      behaviorList="fBehaviorList"
 *      behaviorInterface="FooInterface"
 *      behaviorMethod="foo"
 */
public int foo(int value)
{
   return value + 17;
}

XDoclet is an extremely useful, easily extensible code generator, but it does not support the insertion of the generated code back into the original source file, so it is not useful for our purposes. Instead, we use a similar but much simpler code generator called INgen that does insert the generated code directly in the Java source stream that it was created from. If we wanted to depend on J2SE 5, we could use the annotation processing tool to build an annotation-based generator (and someday will); however, the provided solution works with J2SE 1.4 and amounts to only about a page of code.

INgen looks for the directive @INgen.behaviordispatch (case-sensitive) in Javadoc comments that come immediately before method declarations in your Java source code. INgen is an extremely simple and limited code generator with several restrictions on its use. Javadoc sections must begin on lines that contain only white space before the /** start marker, and nothing may appear in the source code between the end of the comment section and the method declaration it modifies.

After the behaviordispatch directive, INgen expects to find three parameters. The parameter behaviorList contains the name of the BehaviorList variable that contains the behavior objects that will override this method. The parameter behaviorInterface names the interface that behavior objects must implement to be called by this method's behavior dispatch code. Finally, the parameter behaviorMethod names the method of that interface to be called. The behavior method parameter is optional; if omitted, its value defaults to the method name being modified. Parameter keywords are case-insensitive, but their values are case-sensitive, since they are inserted directly into the generated source code.

INgen uses these parameters to produce the dispatch code shown in Listing 1. The method being modified is renamed from foo() to fooDefaultBehavior(), and a new foo() method containing the behavior dispatcher is inserted in the code ahead of it.

To use INgen in your project, run the code generator with a command similar to the following:

 ingen src/packagepath/MyCode.java > objects/generated-src/packagepath/MyCode.java

To use INgen, you must be sure to put behaviors.jar on your CLASSPATH, because the code generator .class file is packaged there. Of course, you must also modify your build rules to compile the Java code in objects/generated-src instead of in src. Doing this complicates your build file a bit, but the benefit is simplified Java source code—a good trade off. If you'd rather not introduce the code generator into your build process, you can always add the Behavior Dispatch pattern from Listing 1 directly to every method you'd like to override. That will be more work in the long run, though.

Requirement for new behavior objects

In short, behavior objects have no requirements, other than their implementation of an interface that contains a method (or methods) with the correct parameters, as shown in the previous section. "Correct" parameters means that the behavior method should take an iterator as its first parameter, and the subsequent parameters should match those in the behavior dispatch method. The behavior's implementation should call the iterator's next() method (again, as shown above) to invoke the default behavior of the function being modified. Code can be added before and after the point where the inherited functionality is called.

Although not required, it is useful if a behavior extends the class Behavior. Doing this gives each behavior object additional functionality beneficial to implementing the kinds of functions that applications using behaviors typically need. I describe the provided functionality in the sections below.

Listing 3 shows an example behavior called DoublingBehavior that doubles the result returned from the method foo(int) in any object it is attached to:

Listing 3. A simple behavior

 class DoublingBehavior extends Behavior implements FooInterface
{
   DoublingBehavior() {}
   public int foo(Iterator chain, int value)
   {
      int inheritedResult = ((FooInterface) chain.next() ).foo(chain, value);
      return inheritedResult * 2;
   }
}

To be a full-class behavior, a class should extend Behavior and implement the behavior interface used by the method it will override. It is possible to make a single object that implements multiple behavior interfaces; if this is done, the behavior will be called any time any of the associated methods of the object it is attached to are invoked. Note in Listing 3 how the code that determines the inherited result uses the behavior chain iterator to call the inherited function. The behavior chain iterator is guaranteed to always return an object of the appropriate interface (as specified by its constructor, as called in the dispatch method; see Listings 1 and 2). If no further behaviors are in the chain, the chain iterator will return a behavior object that calls the default behavior method of the object this behavior is attached to.

1 2 3 Page 1
Page 1 of 3