Working with C++ classes from different sources can be difficult. Often an application may wish to ``project'' common behavior on such classes, but is restricted by the classes' existing design. If only the class interface requires adaptation an obvious solution is to apply an object structural pattern such as Adapter or Decorator [1]. Occasionally there are more complex requirements, such as the need to change both underlying interface and implementation. In such cases, classes may need to behave as if they had a common ancestor.
For instance, consider the case where we are debugging an application constructed using classes from various C++ libraries. It would be convenient to be able to ask any instance to ``dump'' its state in a human-readable format to a file or console display. It would be even more convenient to gather all live class instances into a collection and iterate over that collection asking each instance to dump itself.
Since collections are homogeneous, a common base class must exist to maintain a single collection. Since classes are already designed, implemented and in use, however, modifying the inheritance tree to introduce a common base class is not an option - we may not have access to the source, either! In addition, classes in OO languages like C++ may be concrete data types [2], which require strict storage layouts that could be compromised by hidden pointers (such as C++'s virtual table pointer). Re-implementing these classes with a common, polymorphic, base class is not feasible.
Thus, projecting common behavior on unrelated classes requires the resolution of the following forces constraining the solution:
Consider the following example using classes from the ACE network programming framework [3]:
0=6 =0.850 .55 -0 =.9
0
1. SOCK_Acceptor acceptor; // Global storage
2.
3. int main (void) {
4. SOCK_Stream stream; // Automatic storage
5. INET_Addr *addr =
6. new INET_Addr // Dynamic storage.
7. ...
The SOCK_Stream, SOCK_Acceptor, and INET_Addr classes are all concrete data types since they don't all inherit from a common ancestor and/or they don't contain virtual functions. If during a debugging session an application wanted to examine the state of all live ACE objects at line 7, we might get the following output:
0=6 =0.850 .55 -0 =.9
0
Sock_Stream::this = 0x47c393ab, handle_ = {-1}
SOCK_Acceptor::this = 0x2c49a45b, handle_ = {-1}
INET_Addr::this = 0x3c48a432,
port_ = {0}, addr_ = {0.0.0.0}
An effective way to project the capability to dump state onto these class without modifying their binary layout is to use the External Polymorphism pattern. This pattern constructs a parallel, external inheritance hierarchy that projects polymorphic behavior onto a set of concrete class that need not be related by inheritance. The following OMT diagram illustrates how the External Polymorphism pattern can be used to create the external, parallel hierarchy of classes:
bbllxfalse bbllyfalse bburxfalse bburyfalse heightfalse widthfalse rheightfalse rwidthfalse p@sbbllxp@sbblly p@sbburxp@sbbury p@sheightp@swidth p@srheightp@srwidth p@sfile p@scost10 sc prologfilefalse postlogfilefalse clipfalse psdopsfiga:=figure=graphics/epmodel.eps,width=8.5cm setparmspsfiga, @bbfalse @bbllx @bbtrue @bblly @bbtrue @bburx @bbtrue @bbury @bbtrue @bb @stream=p@sfile @bbtrue @eoftrue `%=12
@stream to @in 200=@in @stream @eoffalse bbtest200 @bbmatch@eoffalse@cull200 @eof `%=14 @bb -bb-error
203=p@sbburx 204=p@sbbury 203 by -p@sbbllx 204 by -p@sbblly bbw203 bbh204 @height @width
240=bbw 241=bbh 100=240 100 by 241 101=100 101 by 241 240 by -101 240 by 10 101=240 101 by 241 102=101 102 by 241 240 by -102 240 by 10 102=240 102 by 241 200=p@sheight205=0 201=200 201 by 100 205 by 201 201=200 201 by 10 201 by 101 205 by 201 201=200 201 by 100 201 by 102 205 by 201 result205 p@swidthresult
@width 240=bbh 241=bbw 100=240 100 by 241 101=100 101 by 241 240 by -101 240 by 10 101=240 101 by 241 102=101 102 by 241 240 by -102 240 by 10 102=240 102 by 241 200=p@swidth205=0 201=200 201 by 100 205 by 201 201=200 201 by 10 201 by 101 205 by 201 201=200 201 by 100 201 by 102 205 by 201 result205 p@sheightresult
p@sheightbbh p@swidthbbw
@rheight p@srheightp@sheight @rwidth p@srwidthp@swidth p@scost<psdraft
@clip
@prologfile
@postlogfile
to p@srheight true sp to p@srwidth true sp
to p@srheight true sp
to p@srwidth true sp
p@sfile
As shown in the figure, we define an abstract class (Dumpable) having the desired dump interface. The parameterized class ConcreteDumpable<> inherits from Dumpable and contains a pointer to an instance of its parameter class, e.g., SOCK_Stream. In addition, it defines a body for dump that delegates to the dump<> template function (shown in the figure as the SignatureAdapter<> pseudo-class and parameterized over the concrete class). The dump template function calls the corresponding implementation method on the concrete class, e.g., SOCK_Stream::dump or INET_Addr::printTo.
Using the External Polymorphism pattern, it is now possible to collect Dumpable instances and iterate over them, calling the dump method uniformly on each instance. Note that the original ACE concrete data types need not change.