Learning by example


introduction teaching contracts patterns events examples foundations conclusions references
Even though teaching concepts is the aim, providing students with appropriate examples definitely facilitates their learning. For each of the topics discussed previously, a simple example will be given demonstrating a possible realization.



slide: Examples

The account contract


introduction teaching contracts patterns events examples foundations conclusions references
An ordinary account supports methods to deposit and withdraw money and allows for inquiring after the balance as well. As an invariant of an ordinary account we may assume that the balance must be equal to or greater than zero. The account class below gives an example of how to realize a contract in C++ code.

The account class

  class account {
  public:
  account() { _balance = 0; promise(account::invariant()); }
   
  virtual float balance() const { return _balance; }
   
   
  void deposit(float x) {
          require( x >= 0 );  
check precondition

hold();
to save the old state

_balance += x; promise( balance() == old_balance + x && invariant() ); } void withdraw(float x) { require( x <= balance() );
check precondition

hold();
to save the old state

_balance -= x; promise( balance() == old_balance - x && invariant() ); } protected: virtual bool invariant() { return balance() >= 0; } virtual void hold() { old_balance = balance(); } private: float _balance; float old_balance;
additional variable

};

slide: The account class

Pre- and post-conditions are realized by calling respectively the require and promise macros, which are simply variants of the C assert macro. To check whether the post-conditions for (state-changing) methods are satisfied, a virtual function hold is introduced that copies the value of (selected parts of) the state of the object. We use the prefix old to indicate these additional instance variables. To check whether a contract is satisfied runtime, we must test that the invariant is satisfied when the object is created. Further, when a method is invoked, we must test its pre-condition and establish that its post-condition is satisfied and that the object invariance is not violated.

A credit_account may be considered to be an improvement over an ordinary account since it allows us to draw more money from it. One possible realization is given below.


  class credit_account : public account {
  public:
  credit_account(float x) { _maxcredit = x; _credit = 0; }
  
  float balance() { return _balance + _credit; }
  
  float credit(float x) { 
  	require( x >= 0 && x + _credit <= _maxcredit );
  	hold();
  	_credit += x;
  	promise( _credit = old_credit + x );
  	promise( balance() == old_balance + x && invariant() );
  	}
  
  
  protected:
    bool invariant() {
  	  return _credit <= _maxcredit && account::invariant();
  	  }
    void hold() { old_credit = _credit; account::hold(); }
  private:
    float _maxcredit, _credit;
    float old_credit;
  };
  
We merely introduce one additional method to raise the credit, and extend the function hold to keep track of the _credit variable. Note that the invariant is indeed strengthened and that the pre-condition of withdraw has (implicitly) been weakened. An alternative solution would consist of redefining deposit and withdraw. This is left as an exercise. <h4>A dynamic role switching pattern</h4> For many applications static type hierarchies do not provide the flexibility needed to model dynamically changing roles. For example we may wish to consider a person as an actor capable of various roles during his lifetime, some of which may even coexist concurrently. The characteristic feature of the dynamic role switching pattern is that it allows us regard a particular entity from multiple perspectives and that the behavior of that entity changes accordingly. We will look at a possible realization of the pattern below.

Taking our view of a person as an actor as a starting point, we need first to establish the repertoire of possible behavior.

The Actor Class

  enum Role { Person = 0 , Student, Employer, Final };
  
  class actor {                      
defines the repertoire

public: actor() { } virtual void walk() { if (exists()) self()->walk(); } virtual void talk() { if (exists()) self()->talk(); } virtual void think() { if (exists()) self()->think(); } virtual void act() { if (exists()) self()->act(); } virtual void become(Role) { }
only for a person

virtual void become(actor*) { } virtual actor* self() { return this; }
an empty self

int exists() { return self() != this; }
who ami

};
Apart from the repertoire of possible behavior, which consists of the ability to walk, talk, think and act, an actor has the ability to establish its own identity (self) and to check whether it exists as an actor, which is true only if it has become another self. However, an actor is not able to assume a different role or to become another self. We need a person for that!

Next, we may wish to refine the behavior of an actor for certain roles, such as for example the student and employer roles, which are among the many roles a person can play.

The Student Actor

  class student : public actor {
  public:
  void talk() { cout << "OOP" << endl; }
  void think() { cout << "Z" << endl; }
  };
  
  class employer : public actor {
  public:
  void talk() { cout << "$$" << endl; }
  void act()  { cout << "business" << endl; }
  };
Only a person has the ability to assume a different role or to assume a different identity. Apart from becoming a Student or Employer, a person may for example become an adult_person and in that capacity again assume a variety of roles.

The Person is an Actor

  class person : public actor {
  public:
  
  person();                         
to create a person

void become(Role r);
to become a ...

void become(actor* p);
change identity

int exists() { return role[Person] != this; } actor* self() { return exists()?role[Person]->self():role [role]; } private: int _role; actor* role[Final+1];
the repertoire

};
A person may check whether he exists as a Person, that is whether the Person role differs from the person's own identity. A person's self may be characterized as the actor belonging to the role the person is playing, taking a possible change of identity into account. When a person is created, his repertoire is still empy.

The Repertoire

  person::person() {
     for (int i = Person; i <= Final ; i++ ) role[i] = this;
     become( Person );
     }
Only when a person changes identity by becoming a different actor (or person) or by assuming one of his (fixed) roles, he is capable of displaying actual behavior.

Changing Roles

  void person::become(actor* p) { role[Person] = p; } 
permanent

void person::become(Role r) { require( Person <= r && r <= Final ); if (exists()) self()->become(r); else { _role = r; if ( role [role] == this ) { switch (_role) { case Person: break;
nothing changes

case Student: role [role] = new student; break; case Employer: role [role] = new employer; break; case Final: role [role] = new actor; break; }; } } }
Assuming or 'becoming' a role results in creating a role instance if none exists and setting the _role instance variable to that particular role. When a person's identity has been changed, assuming a role takes effect for the actor that replaced the person's original identity. (However, only a person can change roles!) The ability to become an actor allows us to model the various phases of a person's lifetime by different classes, as illustrated by the adult_person class.

Becoming adult

  class adult_person : public person {
  public:
    void talk() { cout << "interesting" << endl; }
  };
In the example code below we have a person talking while assuming different roles. Note that the person's identity may be restored by letting the person become its original self.
  person p; p.talk();                    
empty

p.become(Student); p.talk();
OOP

p.become(Employer); p.talk();
$$

p.become(new adult_person); p.talk();
interesting

p.become(Student); p.talk();
OOP (new student)

p.become(&p); p.talk();
$$ (old role)

p.become(Person); // initial state
The dynamic role switching pattern can be used in any situation where we wish to change the functionality of an object dynamically. It may for example be used to incorporate a variety of tools in a drawing editor, as illustrated in  [Hush]. <h4>Propagating update events</h4>

The Model-View or Observer pattern  [GOF] provides a general way to deal with information updates that may affect multiple dependent objects. Similar functionality can be obtained by employing events. An event class can be defined as follows:

The event class

  class event {
  public:
  	virtual void operator()() {}      
activate

void dependent(event* e); void update();
to process dependent events

private: set _dep;
initially empty

};
The event class must provide for a function to activate the event, and in addition it must allow for defining dependent events, that may in turn be activated by calling update.

In the example below, which is adapted from  [Henderson], events are used to maintain and display the value of a collection of thermometers in a consistent fashion. An abstract thermometer may be defined as:

The thermometer class

  class thermometer {
  friend class Reset;
  public:
  	virtual void set(float v);
  	virtual float get();
  protected:
  	float _temp;                 
absolute temperature

};
The thermometer keeps its value in degrees Kelvin, and may be refined into a Centigrade and Fahrenheit thermometer using standard conversion rules.

For realizing a user interface, that allows us to set and display the value of a thermometer, we need classes like prompter and displayer:

The promper class

  class prompter : public widget {
  public:
  	prompter(char* msg);
  	float get();
  };
  
  class displayer : public widget {
  public:
  	displayer(char* msg);
  	void put( float v);
  };
These classes may be refined to allow for textual as well as graphical input and display.

To manage temperature updates, we introduce the Get, Reset and Show events:

  class Get : public event {
  public:
    Get(thermometer* th, prompter* p ) : _th(th), _p(p) {} 
    void operator()() {
  	_th->set ( _p->get() );
  	event::update();
  	}
  private:
    thermometer* _th; prompter* _p;
  };
Activating the Get event results in displaying a prompter and setting the thermometer's value with the value obtained from it. Then its dependent events are activated.

Reset events are needed to update the value of other thermometers. To allow the Reset event access to the stored temperature value, the class Reset has been made a friend of thermometer. Alternatively, the _temp instance variable could have been made a static (class) variable.

The Show event simply puts the value of the thermometer in a displayer. In it defined in a similar way as the Get events.

When a Get event occurs for a particular thermometer, first the other thermometers must be reset, and then the displays for the various thermometers must be changed accordingly. That means that we must create at least one Get event for each thermometer, and sufficient Reset events to update the value of every other thermometer. Also, we must have one or more Show events for each thermometer. Both the appropriate Reset and Show events must be declared to be dependent upon the occurrence of a Get event. Both Get and Show events may be made available as entries in a menu. When defining dependencies take care not to create cycles. Alternatively you may check dynamically for cycles.


introduction teaching contracts patterns events examples foundations conclusions references