Concrete realizations
class N : public R { \fbox{N}
public:
N(int i) : n(i) { }
void action() { cout << n << endl; }
protected:
int n;
};
A::A(int n) { rep = new N(n); } // constructor for A
class S : public R { \fbox{S}
public:
S(char* p) : s(p) { }
void action() { cout << s << endl; }
protected:
char* s;
};
A::A(char* s) { rep = new S(s); } // constructor for A
slide: Concrete realizations
Note that the creation of actual instances of
N and S is hidden in the constructors
of A.
This technique is characterized in [Coplien92] as the
virtual constructor idiom,
since the actual type of the body instance of A is
determined dynamically.
Since these are simple types, the destructor may safely be omitted.
As an example of using the heterogeneous container type
A, look at the following fragment.
First, an (integer) element is created (which becomes
the first element of the chain) and then elements are
appended to the chain
A e1(2),e2("hi"),e3(1);
e1->insert(e2); e1->insert(e3);
e1->process();
In the solution presented
we rely on the implicit dispatching of
the virtual function mechanism to process
the elements according to their types.
An advantage of using classes (instead of tag fields)
to define the various element types
is that the collection of element types
can be easily extended.
Nevertheless, when adding a new element type
the collection of constructors for the
abstract interface A must still be extended.
Dynamic typing {\em -- multiple roles}
An often heard criticism of statically
typed object-oriented languages
is that they do not allow for the flexibility
necessary to characterize objects that change
their type dynamically.
Actually, few dynamically typed languages
allow for changing the type of an object as well.
However, the scheme employed to implement
heterogeneous container types may be easily
extended to allow for changing types (or
roles) dynamically as well.
The construction relies (again) on the
definition of an abstract interface comprising
the interfaces of the (role) realization classes.
In addition, we need a control class
(containing a tag field) to switch
between (realization) types dynamically.
Consider the abstract interface class A, shown in
slide [7-rol-A].
class A { \fbox{A}
public:
virtual void talk() { cout << "*talk*"; }
virtual void think() { cout << "*think*"; }
virtual void act() { cout << "*act*"; }
};
slide: Abstract interface
The class A provides a so-called fat interface,
combining all possible behaviors allowed by the
role realization classes.
Next, we need a control class C, which distinguishes
between the roles of a PERSON and a STUDENT.
See slide [7-rol-C].
Control
-- dispatching
class C : public A { \fbox{C}
public:
enum { PERSON = 0 , STUDENT };
C() {
roles[PERSON]=0;
roles[STUDENT]=0;
role(0); default role
}
A* operator->() { return roles [role]; }
void role( int r ); role switching
protected:
int _role;
A* roles[2];
};
slide: Control
Apart from a constructor and an overloaded de-reference operator,
the class C contains a member function role
(to switch between roles) and an array of roles
(containing so to speak the repertoire of an instance
of A).
The default role is set to PERSON in the constructor.
The realization classes P (for PERSON)
and S (for STUDENT) give the actual definition
for the virtual member functions declared
in the interface class A.
See slide [7-rol-R].
class P : public A { \fbox{P}
public:
void talk() { cout << "beach"; }
void act() { cout << "..."; }
};
class S : public A { \fbox{S}
public:
void talk() { cout << "OOP"; }
void think() { cout << "Z"; }
};
slide: Realizations
Note that each of the role (realization) classes
allows only for a strict subset of the behavior
allowed by the abstract interface class A.
See slide [7-rol-switch].
void C::role( int r ) { \fbox{}
require( r == PERSON || r == STUDENT );
_role = r;
if (roles [role] == 0) {
switch (r) {
case PERSON : roles [role] = new P(); break;
case STUDENT : roles [role] = new S(); break;
};
}
}
slide: Switching between roles
Switching between roles employs the technique of
virtual constructors, which is introduced
in [Coplien92] as a means to enhance the functionality of the
envelope/letter idiom.
For each possible role, an instance of the role
(realization) class is created,
to which requests addressed to the instance of C
(figuring as a representative for each of the roles)
are delegated.
An example of using an instance of C is given below
C s;
s.role(S::STUDENT);
s->think(); s->talk(); s->act();
s.role(S::PERSON);
s->think(); s->talk(); s->act();
The disadvantage of the approach sketched here
(and of fat interfaces in general) is that
the compiler cannot ensure that only the functions
relevant to a particular role are invoked.
Obviously, the price that must be paid for
flexibility of this kind is diminished compiler
support and increased reliance on the programmer's
discipline.