MOODS

Models for Object-Oriented Design of State

by Alexander Ran

Design Decision Trees

Behavior of some objects depends significantly on their states and may thus be qualitatively different at different times. Such objects behave as if they had moods. Specification, design, and implementation of objects with moods may be very complex. MOODS is a family of design patterns that may be used to simplify the design and implementation of objects with complex, state-dependent representation and behavior (i.e., moody objects).

MOODS is presented as a design decision tree (DDT). Design decisions are fine-grained elements of design. A typical design pattern includes a number of design decisions. Design decisions are made one at a time, based on the requirements of the problem at hand and the earlier decisions. A DDT explicitly exposes the sequence and dependence of the design decisions that constitute a particular pattern or design. A DDT may represent a family of patterns or a family of specific designs.

In the mathematical sense, a DDT is a directed graph. The "tree" is symbolic, conveying a sense of ordering, alternatives, and simplicity. (Also, in a lighter spirit, it makes the acronym effective against bugs.)

Figure 1 shows the DDT described in this chapter. Each design decision is represented by a box with three parts: the decision, the specific context or requirements that it addresses and its implications. Links between design decision (i.e., between boxes) are marked with the criteria used to select the appropriate path. Each path on a DDT corresponds to one member of the pattern family represented by the DDT.

Figure 1. Design Decision Tree for Models of Object-Oriented Design with State

A design pattern specifies a solution to a problem that recurs in a certain context; a solution that satisfies the explicit requirements of the problem and those implied by the context.

In different contexts the same problem may require different solutions. Even in the same context a number of solutions to the same problem may exist. If each variation in the problem-context-solution trinity is represented by independent patterns, essential information regarding their commonality and variability is lost and much information that is shared by these patterns is replicated.

Therefore:

Patterns that address similar problems and share important properties or decisions should be grouped into families. Families of patterns should be represented in a form that explicitly exposes their commonality and variability. DDT is one such form.

A DDT incrementally specializes the context, problem and additional requirements on every path, from the root to the leaves. Each node corresponds to a design decision, the requirements it satisfies, and the consequences it implies. Each design decision is taken in the context of all the earlier design decisions. When alternatives exist, they form alternative branches of the DDT. All shared aspects of the alternative design decisions are represented on their shared path from the root of the DDT. Since a DDT supports incremental specification of the problem, the context, and the corresponding design decisions it is suitable to represent a family of patterns.

Besides presenting some useful models for object-oriented (OO) design of systems with complex state-dependent behavior, the goal of this chapter is to demonstrate how architectural knowledge may be organized, explored and gradually refined using DDTs. Note that the MOODS DDT is not complete. Not all of the relevant design decisions are explored with an equal degree of detail, and some are not explored at all. DDTs are rarely complete. Variations in context and requirements are endless, except for very simple problems. However, DDTs are useful at every stage of software development as an effective way to organize existing, incomplete and evolving design knowledge.

Most of the sections in this chapter explain one design decision per section. The names of these sections have two parts. The first part attempts to express the essence of the problem, the context, or the objective, and the second part summarizes the recommended decision. Either part may serve as a reference to the section in the rest of the text.

Here is the list of the design decisions presented in this chapter:

  1. To SIMPLIFY COMPLEX BEHAVIOR use DECOMPOSITION
  2. For OBJECT WITH MOODS use STATE CLASSES
  3. When EVENTS CAUSE MOODS use STATE MACHINE
  4. For STATE MACHINE use TRANSITION METHODS
  5. When STATES ARE CONDITIONS use PREDICATIVE CLASSES
  6. When STATES ARE ASSOCIATIONS use STATE OBJECTS
  7. For COMPLEX MOODS use MOOD ALTERNATOR
  8. For OBJECT WITH MANY MOODS use MOOD CLASSIFIER

Design Decision 1: To Simplify Complex Behavior use Decomposition

In OO design, we are first concerned with finding objects that model essential elements of the problem in the solution space (i.e. in the computer program). After finding the objects we have to design their behavior that is, the way the objects provide their services.

The behavior of some objects is relatively simple and may be expressed by short methods performing the same sequence of actions whenever the methods are called. These objects have few attributes that can always be assigned a meaningful value during the objects' lifetime.

Objects that correspond to entities in the problem domain, reactive objects, controllers, and managers often have complex behavior that depends on the object's history and environment. The sequence of actions performed by the methods of these objects may depend on the values of their attributes or even on their environment. The attributes of such objects are often numerous, and they do not always have a meaningful value.

How can one make specification of complex behavior simpler?

Use decomposition to reduce complexity. Consider these questions carefully:

The main elements of the OO paradigm are objects, classes and methods. These provide the three main dimensions for decomposition:

Design Decision 2: For Objects with Moods use State Classes

Complexity in the behavior of some objects is due to various conditions that determine how the objects provide (or deny) their services. When the same conditions affect a number of services provided by an object, identifying these conditions with abstract states may help simplify specification of the object's behavior.

Consider a service point manager (SPM) that assigns requests to available servers, using possibly complex logic for scheduling and matching, to control the load, optimize service availability, maintain some degree of fairness, and avoid frustrating clients. It is an integral entity that centralizes control and maintains and monitors the relevant information. However behavior of the SPM object may be very complex. Some information maintained by the SPM is meaningful only under specific conditions. For example, when the SPM is overloaded, it must notify its clients to prevent repeated requests. This notification should provide clients with information regarding when the service will be available. This kind of processing, and the associated representation, are only needed and meaningful when the SPM is overloaded.

Abstract states are conditions that guarantee more specific behavior of an object. When an object in an abstract state has complicated behavior that depends on additional conditions, substates of the abstract state may be identified that will guarantee simpler behavior of the object in each substate. This process of incremental specialization of conditions and the corresponding behavior will continue, until sufficiently simple behavior characterizes each abstract state.

How does one specify the representation and behavior of entities with a complex hierarchy of abstract states?

The OO paradigm does not provide a dedicated construct for representing abstract states. Thus, abstract states must be modeled using the basic building blocks of OO design: objects, methods, and classes.

Abstract states are often important concepts in the problem domain. The OO approach uses classes to model important concepts. Since abstract states are chosen to guarantee more specific behavior by objects, they specialize classes of behavior. Thus hierarchies of abstract states may be mapped onto generalization / specialization hierarchies of classes. Classes that model abstract states may be called state classes.

Therefore:

To simplify the modeling of objects with abstract states, use a cluster of state classes. The base class of the cluster specifies the interface and implements the state-independent representation and behavior. Each derived class represents an abstract state and implements state-specific representation and behavior. Substates are represented by the corresponding subclasses.

Note that clusters of state classes are tightly bound organizations. The principles for design of state classes differ from the general OO strategies used to design independent, reusable classes. In clusters of state classes, inheritance is used to establish a hierarchical scope that controls the applicability of its methods as well as visibility and sharing of the structure and values of its attributes.

A very simple SPM is similar to an assigner that assigns clerks to clients on "first come first served" basis. An assigner's public interface includes two messages: ClientComes(Client &) or ClerkGetsFree(Clerk &). The behavior of the assigner in response to these messages, depends on whether there are free clerks or not when a client comes, and whether there are clients waiting or not when a clerk gets free. These conditions identify important abstract states of the system.

Figure 2. Assigner with State Classes.

The class diagram on Figure 2 shows how an assigners' behavior may be specified by a cluster of state classes. Note that the queue of clients is only accessible and visible in the states in which this queue has a meaningful existence when there are no free clerks. The same is true about the pool of clerks. The subclasses on this diagram are also true subtypes. This example demonstrates how state classes help to decompose and incrementally specify state-dependent representation and behavior.

Modeling abstract states as classes achieves several important objectives:

How to keep track of the changing mood of an object?

We have used State Classes to decompose and incrementally specify the behavior of objects with moods. Now we have to make decisions regarding mood management.

How can one determine the current mood of a moody object?

The current mood of an object is a result of its history and, indirectly, the history of objects related to it. The history of an object is partly reflected by the values of its attributes. There are two possibilities:

How to support mood-sensitive method selection?

The actual methods selected by a moody object to perform a service request depend on its mood.

How to program mood-sensitive method selection in commonly used OO languages?

Design Decision 3: When Events Cause Moods use State Machine

When the current mood of an object cannot be unambiguously determined from the values of its attributes, the mood must be explicitly represented and specifically tracked.

Use a finite state machine (FSM) to specify the mood-tracking logic when the frequency of events is comparable to the frequency of mood changes and the change of mood depends only on the current mood and event. The FSM paradigm may be described as follows:

OO implementation of state machines requires design decisions regarding the mapping of states, events, actions and transitions onto the constructs of the OO programming. There is a wide range of choices for this mapping. Here I only address the choices compatible with our earlier decision to use State Classes for modeling abstract states. Also OO modeling of events requires a number of design decisions that are not discussed here.

Design Decision 4: For State Machine use Transition Methods

In response to events, an event-dispatch mechanism invokes event-response methods of reactive objects. It is common to assign response methods responsibility to perform the action of the response and update the state of the reactive object. It is often an effective solution, since both actions and transitions are state-dependent. However when action methods of one state class explicitly refer to sibling state classes for transitions, they are more difficult to reuse or extend.

How can one preserve the separation between the actions and the transitions? How can one track state transitions without introducing unnecessary binding between the state classes?

A transition is a function of the current state and an event. A guarded transition also depends on a special guarding condition. State classes are good for implementing actions because the actions of an object with moods are mood-dependent. State transitions depend on the mood as well. and it is possible to allocate transition logic to state classes. However, it should be separated from the method implementing the actions. Dedicated transition methods can also implement the guarding condition.

Therefore:

To manage the complexity of state dependency and allow for independent refinement of actions and transitions, implement State Machine using dedicated transition methods of state classes.

Transition methods may be invoked independently of action methods (by the same mechanism that invokes action methods on an event) or they may be called by the corresponding action methods. If transition methods are directly called by action methods, separation of actions from transitions would further require dynamic binding of transition methods.

Design Decision 5: When States are Conditions use Predicative State Classes

Explicitly implementing state transitions may be inappropriate in a number of situations. Such is the case when events that cause transitions are significantly more frequent than events that cause state-dependent actions. Also, when objects need to adjust their behavior in correspondence with changes in their environment, explicit modeling of state transitions may not be a good choice. Though change notification may be used to make State Machine applicable in such situations, it may result in a rather inefficient model.

How can the object-to-state relation be maintained without explicitly modeling state transitions?

Figure 3. Predicative state classes for the assigner.

In many situations, the mood of an object may be determined from the values of its attributes or the state of its environment. Since an object's moods correspond to conditions for different types of behavior, these conditions may be explicitly specified by predicate functions. This would allow to determine the abstract state of an object on demand.

Therefore:

Use Predicative State Classes to explicitly specify the conditions for the different moods of an object.

A predicative state classes is a specialization of a state class. Each predicative state class must include in its definition a predicate that ensures that the object is in the abstract state represented by the state class.

Usually only the concrete state classes (i.e., the leaves of a state hierarchy) must be predicative. This is so because an object is always in a concrete state. The superstates exist for factoring common features of their substates. Since leaf states are mutually exclusive one of the leaf states may be left without a predicate. This state will become the default state of the object. The class diagram of the assigner with predicative state classes is shown in the Figure 3.

The Predicative State Classes may serve the Alternator to classify its instance into the appropriate state class. Generic implementation of dynamic state classification is explained in the State Classifier.

Design Decision 6: When States are Associations use State Objects

After we have applied State Classes, the representation and behavior of the modeled entity is jointly specified by a cluster of classes. It is an unusual situation. Normally, each object is identified with one class that determines the representation and behavior of this object at all times.

How can an object follow the behavior specified by different classes when in different states?

One possible approach is for the object, while it is in the different states, to delegate the responsibility for the actual service to different objects. When the change in the object's state can be conceptualized as a change in its association to some collaborator object, State Objects offers a simple and effective solution [Beck+94, Gamma+94].

The State Objects pattern is based on a collaboration between the server object, that appears to provide all the services to the clients, and the state objects that are instances of the state classes. When the server object receives a service request from a client, it delegates the request to the appropriate state object. In a typical implementation of the State Objects the server object holds a pointer whose current value determines the current state of the object. When state transitions occur the value of this pointer should be updated. Thus, behavior of the server object, as seen by the clients, depends on its state.

A number of further design decisions affect significantly applicability and implications of the State Objects. These decisions should address the following questions:

The class diagram in Figure 4 is for the assigner with state objects. In the last section of this chapter you will find C++ source code for this example. It is instructive to compare it with the source code for the Mood Alternator, also found in the last section.

Figure 4. An assigner using State Objects.

In this example, the following design decisions were made:

When using the State Objects, one must be aware of the following:

When modeling an object with a complex hierarchy of abstract states, or when having to tune the application for performance or memory usage you may use an appropriate specialization of MOOD Alternator.

Design Decision 7: For Complex Moods use Mood Alternator

After we have applied State Classes to simplify the design of a complex entity, its behavior and representation are specified by a cluster of classes. An integral entity is best modeled as one object.

How can one object, when it is in different states, follow the behavior and have the attributes specified by different state classes?

To follow the behavior and have the attributes specified by different state classes, the object must be an instance of all these classes. This is possible if it is an instance of a class derived from every state class in the cluster. This is the Mood Alternator class. Its instances include the attributes needed in all states; however, the visibility of an attribute is controlled by the state class that defines the attribute. Each method of the Mood Alternator calls the corresponding method of the state class, selected based on the mood of the alternator's instance.

Mood Alternator uses multiple inheritance in an unconventional way. Normally, classes used for multiple derivation provide complementary services and attributes, combined by the derived class. Alternator derives from classes that provide alternative services, and explicitly selects the right alternative at run time.

Figure 5. Assigner using Mood Alternator and Predicative State classes.

A mood alternator may be designed as a state machine. In order to delegate the service request to the appropriate state class, the alternator object must know its mood. When delegating requests for actions, the Alternator can also implement the logic for tracking state transitions.

The assigner class diagram in Figure 5 is designed using Mood Alternator and Predicative State Classes. The last section of this chapter also contains the C++ source code for this example.

Since Mood Alternator is an alternative to State Objects, they may be compared in terms of their requirements and consequences. To be concrete, let us assume that implementation is done in C++. The following differences may be pointed out:

In order to delegate a service request to the appropriate state class, an Alternator object must be able to determine the mood of its instance. A common way to track the abstract state is to explicitly program state transitions, as in State Machine. In State Machine, the object-to-state relation is represented by memory and is "eagerly" maintained. It is also possible to represent the object-to-state relation by computation and to use "lazy evaluation" on demand. This can be achieved by using a combination of Predicative State Classes and State Classifier. The code for classification of moods is often trivial and repetitive. For design of an object with many moods, consider using the generic mood classification mechanisms in Mood Classifier.

Design Decision 8: For Object With Many Moods use Mood Classifier

While it is always possible to hand-craft the mood classification logic in the methods of Mood Alternator, a generic classification mechanism offers some advantages with respect to reuse, readability and ease of creation.

How can one create generic a classification mechanism that can be used with different, complex, and changing hierarchies of predicative state classes?

Generic classification mechanisms may be based on a set of standard meta structures that represent the lattice of predicative state classes, their methods, and state predicates.

Since an object is always in one leaf state, the classification may be performed by sequentially applying the predicates of the leaf states to the object. Use this scheme when predicates are only associated with leaf states; when the superstates do not have associated conditions and only exist to factor common actions, transitions, and attributes of their substates.

Therefore:

When conditions common to substates are explicitly factored as the conditions of their superstates, use the hierarchy of state classes and the corresponding predicates as a decision network. An object is classified as being in some abstract state if it is in all the superstates of this state and it satisfies the predicate of the state. Mood Classifier can traverse this decision network, testing the state of the classified object against the predicates, and eventually delegating the method to the most specific state class of the object.

Let us consider extending the simple Assigner with overload control. When clients are waiting, such an Assigner should consider the current processing load whenever new clients come or clerks get free. If load does not exceed the preassigned maximum value, new clients are accepted and put on the queue. Otherwise clients are politely denied the service and advised when to try again.

The class diagram in Figure 6 shows the hierarchy of moods for an assigner with overload control using Mood Classifier. The example demonstrates reuse of earlier defined moods and incremental specialization. Note that the methods of the assigner, and the corresponding meta structures (tables of member functions), may be trivially generated. C++ code for this example is included in the next section of this chapter.

Figure 6. The hierarchy of moods for an assigner with overload control.

Using mood classifiers is half way between designing a program and designing a language. Further design decisions are required to make the mood classification mechanism easier to use, to make it applicable in a wider category of situations, and to improve its performance and memory requirements.

C++ source code used in the examples

Assigner with State Objects

class Assigner {

friend class AssignerInterface;

friend class NoClerks;

friend class NoClients;

friend class ClientWaiting;

friend class ClerkFree;

friend class AssignerDream;

public:

  Assigner();

  void ClientComes(Client& c);

  void ClerkGetsFree(Clerk& c);

private:

  Clients clients;

  Clerks clerks;

  AssignerInterface *state;

};
class AssignerInterface {

public:

  virtual AssignerInterface *ClientComes(Assigner * a, Client& c) = 0;

  virtual AssignerInterface *ClerkGetsFree(Assigner * a, Clerk& c) = 0;

};
class NoClerks : public virtual AssignerInterface {

public:

  virtual AssignerInterface *ClientComes(Assigner * a, Client& c);

};                      
class NoClients : public virtual AssignerInterface {

public:

  virtual AssignerInterface *ClerkGetsFree(Assigner* a, Clerk& c);

};
class ClerkFree : public NoClients {

public:

  virtual AssignerInterface* ClientComes(Assigner* a, Client& c);

  static AssignerInterface* Instance();

};
class ClientWaiting : public virtual NoClerks {

public:

  virtual AssignerInterface *ClerkGetsFree(Assigner* a, Clerk& c);

  static AssignerInterface* Instance();

};
class AssignerDream : public NoClerks, public NoClients { 

public:

  static AssignerInterface* Instance();

};
Assigner::Assigner() : state(AssignerDream::Instance()){

}
void Assigner::ClientComes(Client& c){

  state = state->ClientComes(this, c);

}
void Assigner::ClerkGetsFree(Clerk& c){

  state = state->ClerkGetsFree(this, c);

}
AssignerInterface* NoClients::ClerkGetsFree(Assigner* a, Clerk& c) {

  a->clerks.add(c);

  return ClerkFree::Instance();

}
AssignerInterface* NoClerks::ClientComes(Assigner* a, Client& c) {

  a->clients.add(c);

  return ClientWaiting::Instance();

}
AssignerInterface* ClientWaiting::ClerkGetsFree(Assigner* a, Clerk& clerk) {

  clerk.serve(a->clients.next());

  return (a->clients.IsAnybodyThere()) ?  ClientWaiting::Instance() :

                                            AssignerDream::Instance();

}
AssignerInterface* ClerkFree::ClientComes(Assigner* a, Client& client) {

  a->clerks.next().serve(client);

  return (a->clerks.IsAnybodyThere()) ? ClerkFree::Instance() :

                                          AssignerDream::Instance();

}
AssignerInterface * ClientWaiting::Instance() {

  static ClientWaiting instance;

  return &instance;

}
AssignerInterface * ClerkFree::Instance() {

  static ClerkFree instance;

  return &instance;

}
AssignerInterface * AssignerDream::Instance() {

  static AssignerDream instance;

  return &instance;

}

Assigner as Alternator with predicative state classes

class AssignerInterface {

public:

  void ClientComes(Client& c);

  void ClerkGetsFree(Clerk& c);

};
class NoClerks : public virtual AssignerInterface {

public:

  void ClientComes(Client& c);

protected:

  Clients clients;

};                      
class NoClients : public virtual AssignerInterface {

public:

  void ClerkGetsFree(Clerk& c);

protected:

  Clerks clerks;

};
class ClerkFree : public virtual  NoClients {

public:

  void ClientComes(Client& c);

protected:

  bool Is() { return clerks.IsAnybodyThere(); }

};
class ClientWaiting : public virtual NoClerks {

public:

  void ClerkGetsFree(Clerk& c);

protected:

  bool Is() { return clients.IsAnybodyThere(); }

};
class AssignerDream : public virtual NoClerks,

                      public virtual NoClients {

};
class Assigner :  private ClerkFree,

                  private ClientWaiting,

                  private AssignerDream {

public:

  Assigner () {}

  void ClientComes(Client& c);

  void ClerkGetsFree(Clerk& c);

};
void NoClients::ClerkGetsFree(Clerk& c) {

  clerks.add(c);                    

}
void NoClerks::ClientComes(Client& c) {

  clients.add(c);

}
void ClientWaiting::ClerkGetsFree(Clerk& clerk) {

  clerk.serve(clients.next());

}
void ClerkFree::ClientComes(Client& client) {

  clerks.next().serve(client);

}

void Assigner::ClientComes(Client& client){

  if (ClerkFree::Is()){

    ClerkFree::ClientComes(client);

  }

  else {

    NoClerks::ClientComes(client);

  }

}
void Assigner::ClerkGetsFree(Clerk& clerk) {

  if (ClientWaiting::Is()) {

    ClientWaiting::ClerkGetsFree(clerk);

  }

  else {

    NoClients::ClerkGetsFree(clerk);

  }

}

Assigner as Mood Classifier

// classes AssignerInterface, NoClerks, NoClients, ClientWaiting

// AssignerDream, ClerkFree as in the example 2.



class LoadControl : public virtual ClientWaiting {

public:

  void ClerkGetsFree(Clerk& c);

protected:

  static const int MaxLoad;

  LoadControl() : load(0) {}

  void UpdateLoad(Client&) { load++; }

  void UpdateLoad(Clerk&) { load--; }

  int load;

};



const int LoadControl::MaxLoad = 1;



class Overloaded : public virtual LoadControl {

public:

  void ClientComes(Client& c);

protected:

  bool Is() { return load > MaxLoad; }

};



class ManageableLoad : public virtual LoadControl {

public:

  void ClientComes(Client& c);

};



void LoadControl::ClerkGetsFree(Clerk& clerk) {

  ClientWaiting::ClerkGetsFree(clerk);

  UpdateLoad(clerk);

}



void Overloaded::ClientComes(Client& client) {

  client.ComeLater();

}



void ManageableLoad::ClientComes(Client& client) {

  ClientWaiting::ClientComes(client);

  UpdateLoad(client);

}



// the rest is trivially generated



class Assigner :  private ClerkFree,

                  private Overloaded,

                  private ManageableLoad,

                  private AssignerDream {



public:

  Assigner () {}

  void ClientComes(Client& c) {

    (this->*_client_comes_[_mood_()])(c); }

  void ClerkGetsFree(Clerk& c) {

    (this->*_clerk_gets_free_[_mood_()])(c); }

private:

  int _mood_();

  typedef void (Assigner::*_ClientComes_)(Client&);

  typedef void (Assigner::*_ClerkGetsFree_)(Clerk&);

  static _ClientComes_ _client_comes_[];

  static _ClerkGetsFree_ _clerk_gets_free_[];

};



Assigner::_ClientComes_ Assigner::_client_comes_[] = {

  &ClerkFree::ClientComes,

  &Overloaded::ClientComes,

  &ManageableLoad::ClientComes,

  &AssignerDream::ClientComes

};



Assigner::_ClerkGetsFree_ Assigner::_clerk_gets_free_[] = {

  &ClerkFree::ClerkGetsFree,

  &Overloaded::ClerkGetsFree,

  &ManageableLoad::ClerkGetsFree,

  &AssignerDream::ClerkGetsFree

};



int Assigner::_mood_() {

  enum { _ClerkFree=0, _Overloaded, _ManageableLoad, _AssignerDream };

  if (ClerkFree::Is()) return _ClerkFree;

  else if (ClientWaiting::Is()) {

    if (Overloaded::Is()) return _Overloaded;

    else return _ManageableLoad;

  }

  else return _AssignerDream;

}

References

[Beck+94] K. Beck and R. Johnson. "Patterns Generate Architectures." In M. Tokoro and R. Pareschi (Eds.), Object-Oriented Programming, LNCS 821, Berlin: Springer-Verlag.

[Gamma+94] E. Gamma, R. Helm, R. Johnson, and J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley.