Design Pattern : Adaptive Role Playing

Name

Adaptive Role Playing

Author

Robert Familiar
bofam@kafka.iii.net

Intent

The Adaptive Role Playing Design Pattern defines an approach to class migration. The goal of this pattern is to allow an object to dynamically play different roles without losing its unique object identity.

For example, if Student and Employee are defined as Role Classes of class Person, and Student and Employee were defined as being in the same Role Group, then the Person object would be able to play the role of one or more Employees or one or more Students and migrate between playing the Role of a Student or an Employee dynamically.

Depending on how you group the Role classes, set the cardinality constraints and define the migration rules will role playing by the Person class be defined.

Motivation

Using this technique, you will be able to define Roles and Role Groups thus allowing for an object to play multiple roles at one time and to be able to migrate between roles that are in the same group.

For example, you could define a Person that playes the Role of either a Student or an Employee. To do this both the Student Role and the Employee Role would be defined as being in the same Role Group. Consider this class dictionary:

	Person = <personRole> PersonRoleGroup
		 <age> Number .
	PersonRoleGroup : Student | Employee
		*common* <playedBy> Person .
	Student = .
	Employee = .
Note the common part shared by Student and Employee, the playedBy member. This is a pointer back to the object that is playing the Role.

Assume that there is a method called PlayedBy() of the PersonRoleGroup that returns the pointer to the Person object and a method called GetAge() of the Person class.

The method PlayedBy() allows access to properties of the Person object from a Role that is being played by the Person. For example, determining the age of a student would be written as:

	pStudent->PlayedBy()->GetAge()
A Person object can also have the role it is playing migrated from one role to the other. This would be written as:
	// create an 18 year old  person who
	// is playing the role of a Student
	Person * pPerson( new Student(), 18 ); 
	...
	// the Person is now employed and no longer a Student
	delete pPerson->rset_personRole( new Employee() );
The method rset_personRole() takes the new role for the Person object to play and returns the previous role. Note the deletion of the return value form rset_personRole(). This is to insure that the Student object is deleted.

Requirements

A Role is always played by another object. We assume that there is a method PlayedBy() such that if R is a role class then R->PlayedBy() returns P, the player. This implies the following constraints:

  1. There is always one player of a Role object
  2. The Role object is existence-dependent on its Player
  3. There may be any number of Roles played by a Player, even if these roles are instances of the same Role class
  4. There may be no cycles in the PlayedBy() relationship, i.e. PlayedBy() may return another Role object but after a finite number of Role objects being returned from successive calls to PlayedBy() a non-Role object is returned and the Role playing ends
Each Role class must have a cardinality constraint. Absence of a cardinality constraint implies unrestricted cardinality. This allows the Player to be related to 0, 1 or more instances of a particular Role. This is a differentiating factor from the cardinality of subclasses which is always 0 or 1.

Role Groups have the following characteristics:

  1. Each Role class can be specialized into one or more Role Groups
  2. A Player can play at most one Role in each of its Role Groups
  3. A Role group is not exhaustive, i.e. there may be cases where the Player does not play any Role
  4. A Role Group may consist of only 1 Role Class
Role classes can be partitioned statically or dynamically. (see Adaptive Dynamic Subclasses Design Pattern)

Implementation

To define the relationship between a Role and its Player, a data member is defined in the Player class for each Role Group that the Player wants to play in. The type of these data members is the superclass of the Role Group or a list of the superclass of the Role Group.

Each Role class defines a data member called playedBy that is a pointer to the Person playing the Role. Consider the following class dictionary that defines a player Person and a Role Group that represents the marriage status of the Person:

	Person = <marrigeRole> MarriageRole .
	MarriageRole : Single |
        	       Married |
	               Divorced |
	               Widowed *common* <playedBy> Person .
	Single = .
	Married = .
	Divorced = .
	Widowed = .
Using this structure, a Person can play only 1 Role from the Role Group MarriageStatus.

Both cardinality and migration rules can not be expressed in the class dictionary. They must be expressed in the Role Playing Propagation Patterns defined by an adaptive program. (See the Demeter book for more information about Demeter.)

Role Classes are stored in a list when it is necessary to allow the Player to play a Role multiple times in an instance allowing for cardinality from 0 to N.

Migration rules define how a Role can migrated to other Roles in the same Role Group. For example, a program would have to make sure that if a Person were playing the Role of a Single Person, then they could not migrate from playing the Single Role to playing the Divorced or Widowed Roles.

An adaptive program, defined as a set of Propagation Patterns, must define the cardinality and migration rules as attributes of the application domain.

Now lets add two more Role Groups, the EmployeeRole Role Group and the StudentRole Role Group. A Person can hold multiple jobs so we must allow for the EmployeeRole to be played multiple times by the same Person.

A Person is either a Student or not so there is no need to store Student Roles in a list. We want to allow a Person to be employed and to go to school so the StudentRole and EmployeeRole must be in different Role Groups.

The following is the complete legal class dictionary:

        Person = <marriageRole> MarriageRole ";"
                 <employmentRole> EmploymentRole ";"
                 <studentRole> StudentRole.
        MarriageRole : Single |
                       Married |
                       Divorced |
                       Widowed *common* [ <playedBy> Person ] .
        Single = "single" .
        Married = "married" .
        Divorced = "divorced".
        Widowed = "widowed" .

        EmploymentRole : Employed |
                         UnEmployed *common* [ <playedBy> Person ] .
        UnEmployed = "unemployed" .
        Employed = <jobTypeList> List( JobType ) .
        JobType : PartTimeJob |
                  FullTimeJob *common* "shift=" <shift> DemNumber .
        PartTimeJob = "part-time-job" .
        FullTimeJob = "full-time-job" .

        StudentRole : Student |
                      NonStudent *common* [ <playedBy> Person ] .
        Student = "student" .
        NonStudent = "non-student" .

        List(S) ~ S { "," S } .
Note the use of Adaptive Dynamic Subclasses to define the Employed Role. This allows a Person who is playing the Role of a part-time employee to migrate that job to a full-time position dynamically or vice versa.

Now if we were developing an adaptive program using this class dictionary, we first would want to set the cardinality constraints for the role playing and then define the migration rules within the role groups.

The cardinality constraints are:

  1. Marriage Role 1:1, a Person must play 1 Marriage Role
  2. Employment Role 0:3, a Person can play 0 to 3 Employment Roles
  3. Student Role 0:1, a Person can play 0 or 1 Student Roles
Also, an Employement Role can not conflict with another Employment Role with respect to its shift (1, 2 or 3).

The migration rules for the MarriageRole are:

  1. Single can migrate to Married
  2. Married can migrate to Widowed or Divorced
  3. Divorced can migrate to Married.
  4. Widowed can migrate to Married.
The migration rules for EmploymentRole are:

  1. UnEmployed can migrate to Employed.
  2. Employed can migrate to UnEmployed.
The migration rules for StudentRole are:

  1. NonStudent can migrate to Student.
  2. Student can migrate to NonStudent.
Employed can have an internal representation of FullTime or Partime.

The following are the Role Playing Propagation Patterns that an adpative program would include as a part of its domain for enforcing the cardinality constraints and migration rules that we have set up:

*operation* void PlaySingleRole()

  *wrapper* Person
    (@
        if ((strcmp(marriageRole->get_type(),"Married") == 0) ||
            (strcmp(marriageRole->get_type(),"Divorced") == 0) ||
            (strcmp(marriageRole->get_type(),"Widowed") == 0))
        {
            cout << "Can't change role from married, divorced or widowed to single" << endl;
            return;
        }
        else
        {
            delete rset_marriageRole( new Single() );
            marriageRole->set_playedBy(this);
            cout << "Playing single role now" << endl;
        }
    @)

*operation* void PlayMarriedRole()

  *wrapper* Person
    (@
        delete rset_marriageRole( new Married() );
        marriageRole->set_playedBy(this);
        cout << "Playing married role now" << endl;
    @)

*operation* void PlayDivorcedRole()

  *wrapper* Person
    (@
        if ((strcmp(marriageRole->get_type(),"Widowed") == 0) ||
            (strcmp(marriageRole->get_type(),"Single") == 0))
        {
            cout << "Can't change role from single or widowed to divorced" << endl;
            return;
        }
        else
        {
            delete rset_marriageRole( new Divorced() );
            marriageRole->set_playedBy(this);
            cout << "Playing divorced role now" << endl;
        }
    @)

*operation* void PlayWidowedRole()

  *wrapper* Person
    (@
        if (!(strcmp(marriageRole->get_type(),"Married") == 0))
        {
            cout << "Can't change role from single or divorced to widowed" << endl;
            return;
        }
        else
        {
            delete rset_marriageRole( new Divorced() );
            marriageRole->set_playedBy(this);
            cout << "Playing widowed role now" << endl;
        }
    @)

*operation* void PlayUnEmployedRole()

  *wrapper* Person
    (@
        delete rset_employmentRole( new UnEmployed() );
        employmentRole->set_playedBy(this);
        cout<< "Playing unemployed role now" << endl;
    @)

*operation* void PlayEmployedRole( JobType * pJobType )

  *wrapper* Person
    (@
        if (strcmp(employmentRole->get_type(),"Employed") == 0)
        {
            cout << "Checking employment status..." << endl;

            DemNumber * pNumJobs = new DemNumber(0);
            employmentRole->NumberOfJobs( pNumJobs );
            cout << "Number Of Jobs = " << pNumJobs << endl;
            if (pNumJobs->get_val() == 3)
            {
                cout << "Maximum number of jobs reached, can not work another!" << endl;
                return;
            }

            DemNumber * bConflict = new DemNumber(0);
            employmentRole->ShiftConflict( pJobType, bConflict );
            if (bConflict->get_val() == 1)
            {
                cout << "There is a shift conflict with this new job!" << endl;
                return;
            }

            employmentRole->AddJob( pJobType );
            cout << "Playing employed role now" << endl;
        }
        else
        {
            delete rset_employmentRole( new Employed() );
            employmentRole->set_playedBy(this);
            employmentRole->AddJob( pJobType );
            cout << "Playing employed role now" << endl;
        }
    @)

*operation* void PlayStudentRole()
 
  *wrapper* Person
    (@
        delete rset_studentRole( new Student() );
        studentRole->set_playedBy(this);
        cout << "Playing student role now" << endl;
    @)

*operation* void PlayNonStudentRole()

  *wrapper* Person
    (@
        delete rset_studentRole( new NonStudent() );
        studentRole->set_playedBy(this);
        cout << "Playing non-student role now" << endl;
    @)

*operation* void AddJob( JobType * pJobType )

  *traverse*
    *from* EmploymentRole
      *bypassing* -> *,playedBy,*
    *to-stop* Employed 

  *wrapper* Employed
    (@
        cout << "Adding a job" << endl;

        if (jobTypeList == NULL)
        {
            set_jobTypeList( new JobType_List() );
        }

        (get_jobTypeList())->append( pJobType );
    @)

*operation* void NumberOfJobs( DemNumber * pCount )

  *traverse*
    *from* EmploymentRole
      *bypassing* -> *,playedBy,*
    *to-stop* Employed 

  *wrapper* Employed
    (@
        cout << jobTypeList->list_length() << endl;
        pCount->set_val(jobTypeList->list_length());
    @)

*operation* void ShiftConflict( JobType * pJobType, DemNumber * bConflict )

  *traverse*
    *from* EmploymentRole
      *bypassing* -> *,playedBy,*
    *to-stop* JobType

  *wrapper* JobType
    (@
        if (bConflict->get_val() == 1)
        {
            cout << "Conflict found" << endl;
            return;
        }
        else
        {
            bConflict->set_val((this->get_shift() == pJobType->get_shift()));
        }
    @)

Testing

The following code fragment is used to test the role-playing propagation patterns:
  cout << "\n*** Testing Role Playing Propagation Patterns ***" << endl;

  cout << "Change marriage role from single to married." << endl;

  iPerson->PlayMarriedRole();

  cout << "Change student role to non-student role." << endl;

  iPerson->PlayNonStudentRole();

  cout << "Change marriage role from married to divorced." << endl;

  iPerson->PlayDivorcedRole();

  cout << "Change marriage role from divorced to single." << endl;
  cout << "This should produce an error message.\n" << endl;

  iPerson->PlaySingleRole();

  cout << "Change unemployed role to employed with 3 jobs." << endl;

  FullTimeJob * pFullTimeJob = new FullTimeJob();
  pFullTimeJob->set_shift(new DemNumber(1));
  iPerson->PlayEmployedRole(pFullTimeJob);
  
  PartTimeJob * pPartTimeJob1 = new PartTimeJob();
  pPartTimeJob1->set_shift(new DemNumber(2));
  iPerson->PlayEmployedRole(pPartTimeJob1);

  PartTimeJob * pPartTimeJob2 = new PartTimeJob();
  pPartTimeJob2->set_shift(new DemNumber(3));
  iPerson->PlayEmployedRole(pPartTimeJob2);

  cout << "Now try to add a 4th job." << endl;
  cout << "This should produce an error message." << endl;
  
  PartTimeJob * pPartTimeJob3 = new PartTimeJob();
  pPartTimeJob3->set_shift(new DemNumber(2));
  iPerson->PlayEmployedRole(pPartTimeJob3);

This is the output produced by the test code:
*** Testing Role Playing Propagation Patterns ***

Change marriage role from single to married.

	Playing married role now

Change student role to non-student role.

	Playing non-student role now

Change marriage role from married to divorced.

	Playing divorced role now

Change marriage role from divorced to single.
This should produce an error message.

	Can't change role from married, divorced or widowed to single

Change unemployed role to employed with 3 jobs.

	Adding a job
	Playing employed role now

	Checking employment status...
	Number Of Jobs = 1 
	Adding a job
	Playing employed role now

	Checking employment status...
	Number Of Jobs = 2 
	Adding a job
	Playing employed role now

Now try to add a 4th job.
This should produce an error message.

	Checking employment status...
	Number Of Jobs = 3 
	Maximum number of jobs reached, can not work another!

Theoretical Background

The theoretical underpinings of Role Classes can be found in the paper 'Using Dynamic Classes and Role Classes to Model Object Migration' by Wieringa, Jonge and Spruit.

Related Patterns

The Adaptive Dynamic Subclassing Design Pattern outlines another approach to class migration. It defines a mechanism by which an object can have its internal representation dynamically changed from one subclass to another at run time.