Create a storable object type to share objects between applications
Scott is a free-lance writer; he has more than 15 years of experience in a variety of programming languages. Scott can be reached at 705 W. Virginia, Gunnison, CO 81230.
There are four fundamental features of any true object-oriented programming language: data abstraction, encapsulation, inheritance, and polymorphism. These are the core concepts necessary for object-oriented programming. However, they aren't the only concepts that are useful to the object-oriented programmer.
With most programming languages, objects are created, manipulated, and destroyed within the context of a single program. Each program is a community of objects that do not communicate with other communities of objects. Put yourself in the place of an object. Think of inter-object communication as a network of roads and each program as a city. You can go anywhere inside your own city, but there are no roads leading to other cities. This is frustrating, because it limits the ability of different programs within a system to share objects.
Supporting Persistence
Turbo Pascal, C++, and Smalltalk do not directly support the saving of objects from one invocation of a program to another. This is like building a car every morning, driving it to work, and then having it disintegrate when you get back home at night. Every time you commute to work, you need to build a new car. Similarly, whenever you create an object in a program, it exists only until the program's work is done. Every time the program starts, it has to begin by building new objects.
Bertrand Meyer incorporated the concept of "persistent objects" into his Eiffel programming language. A persistent object can be stored external to a program, usually in a file. More than one program can share the same persistent object, and a program can save its objects for future executions.
Eiffel implements persistent objects via a special base class called STORABLE. Objects of a class derived from STORABLE have store and retrieve methods. When an object is stored, its entire data structure is written to a specified file. Later, that entire structure can be loaded into an object of the same class.
Does having built-in support for persistent objects make Eiffel a superior object-oriented programming language? In one sense, the answer is yes: Anything a programming language can provide for you is a definite advantage over having to develop a facility on your own. However, persistence can be accomplished in other programming languages with a minimal amount of work and the knowledge of object formats.
Persistence with Turbo Pascal
The Turbo Pascal object implementation is relatively simple when compared to Smalltalk and C++. Listing One (page 84) shows an example of a simple Turbo Pascal class hierarchy. Shape is an abstract base class, which defines the common characteristics of all Shape classes. Derived from Shape are two classes, Box and Ring. Box defines a rectangular shape, and Ring defines a circle. The Draw method is polymorphic, by virtue of the VIRTUAL keyword.
Listing Two (page 84) is a short program which uses the polymorphic nature of Draw to implement a generic procedure called DrawShape. The argument to DrawShape is a pointer to the common base class (Shape) of Box and Ring. The call to the Draw method in DrawShape invokes the implementation of Draw for the specific class of the object whose pointer was used as an argument. In other words, passing a pointer to a Box to DrawShape will cause the Box.Draw method to be called.
Making Turbo Pascal objects persistent requires that we understand how objects are stored in memory. A Turbo Pascal object is basically the same thing as a record: The instance variables of an object are stored contiguously in memory, aligned on 16-bit word boundaries. A derived object class will contain all of the members defined for its base class, plus all of the members unique to itself. Shape defines a single instance variable, Color. Because Box is derived from Shape, it inherits Color. You can think of Box, for persistence purposes, as the Pascal RECORD structure shown in Example 1.
Example 1: Representing the Box object as a Turbo Pascal record.
BoxRec = RECORD BEGIN Color : INTEGER; UpperLeftX, UpperLeftY: INTEGER; LowerRightX, LowerRightY: INTEGER; END;
The obvious approach to writing an object to disk is to use the same method used for writing a record. Were Box a record as shown in Example 1, a FILE OF Box could be declared to hold Box objects. Alas, a file component type in Turbo Pascal cannot be an object type; FILE OF Box generates an error to this effect.
An untyped file allows us to solve our problem. Listing Three (page 84) employs an untyped file and the BlockWrite and BlockRead functions to store and retrieve Ring and Box objects. The Reset and Rewrite functions declare the file record length to be one, letting us write any number of bytes to the untyped file. When we read and write an object, the SizeOf function is used to get the size of the object.
An object that has virtual methods must be initialized with a CONSTRUCT0R call before it can be loaded with an object value from disk. The CONSTRUCTOR assigns the address to virtual function pointers for the object. The values of the instance variables can then be updated via a call to BlockRead.
Using individual calls to BlockWrite and BlockRead will store the instance variables of an object in a disk file. This isn't the best method, though. For example, let's say that we wanted to create a function which writes any type of Shape to a file. An obvious first attempt at such a function might look like that shown in Example 2.
Example 2: A first attempt at writing a procedure to store a persistent object.
PROCEDURE WriteShape(f : FILE; s : ^Shape); BEGIN BlockWrite (f, s, SizeOf(s^)); END;
It looks good, but it won't do what you might expect. A Shape is defined with only one integer instance variable, so SizeOf(Shape) is only two. A Box has 10 bytes of instance variables, and a Ring has 8 bytes. So, passing the address of a Box (for example) to WriteShape will result in having only the first 2 bytes of the Box stored. Obviously, this is not going to work. A better system is to use methods to store and retrieve objects in files. By making these methods polymorphic across a class hierarchy, generic functions such as WriteShape can be created.
Listing Four (page 84) shows new versions of the Shape, Ring, and Box classes. The Shape object type defines a pair of virtual methods called FWrite and FRead. The Box and Ring classes define specific versions of these methods, which write and read the corresponding object type into the specified file. Listing Five (page 86) shows how these functions can be used in place of the BlockRead and BlockWrite calls used in Listing Three. Using these new methods, a generic WriteShape procedure would look like that shown in Example 3.
Example 3: A generic WriteShape procedure to store persistent objects.
PROCEDURE WriteShape(f : FILE; s : ^Shape); BEGIN s->EWrite (f); END;
What about building a Turbo Pascal class (object type) that emulates the function of the STORABLE class included with Eiffel? Because Turbo Pascal supports only single-inheritance -- where a class may have only one base class -- STORABLE would have to be the only base class. Using a STORABLE base class with the Shape classes would change their definitions only slightly.
Consider the Storable object type defined in Example 4. The Shape object type and the object types derived from it inherit the basic I/O functions from Storable. Every object type for whom Storable is an ancestor defines its own implementation of the FWrite and FRead methods.
Example 4: A storable base class (object type) that emulates the features of the Eiffel storable class.
Storable = OBJECT BEGIN PROCEDURE FWrite(f : FILE); PROCEDURE FRead(f : FILE); END; Shape = OBJECT (Storable) BEGIN Color : INTEGER; PROCEDURE Draw; VIRTUAL; END;
Another advantage of read/write methods customized to an object type can be seen with objects that represent dynamic data structures. Let's say that you've created a linked-list object type: Each linked-list object will have a pointer to the first node in the list; the other pointers and the actual data will be stored in dynamic memory. Simply writing a linked-list object directly with a BlockWrite statement will store the pointer only to the first element in the list.
Obviously, this pointer will be meaningless in another execution of the program (because addresses will change). Even worse, the other nodes in the list will be lost when the program terminates. Class-specific read/write methods can solve this problem by writing the data in the nodes of the linked list to the disk file in the same order as they appear in the list. Then a linked list is read from disk, and the dynamic tree can be recreated from the node data.
Conclusion
Persistent objects are useful in restoring the state of objects within a program from one run to the next. This is particularly important for objects that are created on-the-fly. In addition, persistent objects allow different programs to share common objects.
As object-oriented technology unfolds, persistent objects will play an increasingly important role. The benefits will be seen not only in programming, but in applications such as object-oriented databases and, ultimately, in operating systems.
The greatest benefits to programmers will come when persistent objects can be shared between programs written in different languages. Wouldn't it be nice to grab a Pascal object from C++?
A common specification for the creation and management of persistent objects, such as that being proposed by Microsoft, will be needed. As you have seen in this article, it is relatively easy to implement persistent objects in Turbo Pascal. With slight modifications, the ideas presented here can be applied to both Smalltalk and C++.
_PERSISTENT OBJECTS IN TURBO PASCAL_ by Scott Robert Ladd
[LISTING ONE]<a name="01d4_000c">
UNIT Shapes;
INTERFACE
USES Graph, Crt;
TYPE
{---------- Shape class ----------}
Shape = OBJECT
Color : BYTE;
PROCEDURE Draw; VIRTUAL;
END;
ShapePtr = ^Shape;
{---------- Box class ----------}
Box = OBJECT (Shape)
LowerRightX, LowerRightY : INTEGER;
UpperLeftX, UpperLeftY : INTEGER;
CONSTRUCTOR Create(ulx, uly, lrx, lry, c : INTEGER);
PROCEDURE Draw; VIRTUAL;
END;
{---------- Ring class ----------}
Ring = OBJECT (Shape)
Xcenter, Ycenter : INTEGER;
Radius : INTEGER;
CONSTRUCTOR Create(x, y, rad, c : INTEGER);
PROCEDURE Draw; VIRTUAL;
END;
IMPLEMENTATION
{---------- Shape methods ----------}
PROCEDURE Shape.Draw;
BEGIN
{ Does nothing; it's a virtual method place-holder }
END;
{---------- Box methods ----------}
CONSTRUCTOR Box.Create(ulx, uly, lrx, lry, c : INTEGER);
BEGIN
UpperLeftX := ulx;
UpperLeftY := uly;
LowerRightX := lrx;
LowerRightY := lry;
Color := c;
END;
PROCEDURE Box.Draw;
BEGIN
SetColor(Color);
Rectangle(UpperLeftX, UpperLeftY, LowerRightX, LowerRightY);
END;
{---------- Ring methods ----------}
CONSTRUCTOR Ring.Create(x, y, rad, c : INTEGER);
BEGIN
Xcenter := x;
Ycenter := y;
Radius := rad;
Color := c;
END;
PROCEDURE Ring.Draw;
BEGIN
SetColor(Color);
Circle(Xcenter, Ycenter, Radius);
END;
END. { UNIT Shapes }
<a name="01d4_000d"><a name="01d4_000d">
<a name="01d4_000e">
[LISTING TWO]<a name="01d4_000e">
PROGRAM Program1;
USES Graph, Crt, Shapes;
VAR
R : Ring;
B : Box;
GrDriver, GrMode, GrResult : INTEGER;
ch : CHAR;
PROCEDURE DrawShape(s : ShapePtr);
BEGIN
s^.Draw;
END;
BEGIN
R.Create(100,100,90,2);
B.Create(25,10,175,175,4);
GrDriver := DETECT;
InitGraph(GrDriver,GrMode,'d:\tp');
GrResult := GraphResult;
IF GrResult <> GrOK THEN
BEGIN
WriteLn('Unable to initialize BGI: error ',grResult);
Halt(1);
END;
DrawShape(@R);
DrawShape(@B);
REPEAT UNTIL KeyPressed;
Ch := ReadKey;
CloseGraph;
END.
<a name="01d4_000f"><a name="01d4_000f">
<a name="01d4_0010">
[LISTING THREE]<a name="01d4_0010">
PROGRAM Program2;
USES Graph, Crt, Shapes;
VAR
R1, R2 : Ring;
B1, B2 : Box;
F : FILE;
GrDriver, GrMode, GrResult : INTEGER;
ch : CHAR;
BEGIN
R1.Create(100,100,90,2);
R2.Create(0,0,0,0);
B1.Create(25,10,175,150,4);
B2.Create(0,0,0,0,0);
GrDriver := DETECT;
InitGraph(GrDriver,GrMode,'d:\tp');
GrResult := GraphResult;
IF GrResult <> GrOK THEN
BEGIN
WriteLn('Unable to initialize BGI: error ',grResult);
Halt(1);
END;
Assign(F,'shapes.fil');
Rewrite(F,1);
BlockWrite(F,R1,SizeOf(R1));
BlockWrite(F,B1,SizeOf(B1));
Close(F);
Assign(F,'shapes.fil');
Reset(F,1);
BlockRead(F,R2,SizeOf(R2));
BlockRead(F,B2,SizeOf(B2));
R2.Draw;
B2.Draw;
REPEAT UNTIL KeyPressed;
Ch := ReadKey;
Close(F);
CloseGraph;
END.
<a name="01d4_0011"><a name="01d4_0011">
<a name="01d4_0012">
[LISTING FOUR]<a name="01d4_0012">
UNIT Shapes;
INTERFACE
USES Graph, Crt;
TYPE
{---------- Shape class ----------}
Shape = OBJECT
Color : BYTE;
PROCEDURE Draw; VIRTUAL;
PROCEDURE FWrite(VAR f : FILE); VIRTUAL;
PROCEDURE FRead(VAR f : FILE); VIRTUAL;
END;
ShapePtr = ^Shape;
{---------- Box class ----------}
Box = OBJECT (Shape)
UpperLeftX, UpperLeftY : INTEGER;
LowerRightX, LowerRightY : INTEGER;
CONSTRUCTOR Create(ulx, uly, lrx, lry, c : INTEGER);
PROCEDURE Draw; VIRTUAL;
PROCEDURE FWrite(VAR f : FILE); VIRTUAL;
PROCEDURE FRead(VAR f : FILE); VIRTUAL;
END;
{---------- Ring class ----------}
Ring = OBJECT (Shape)
Xcenter, Ycenter : INTEGER;
Radius : INTEGER;
CONSTRUCTOR Create(x, y, rad, c : INTEGER);
PROCEDURE Draw; VIRTUAL;
PROCEDURE FWrite(VAR f : FILE); VIRTUAL;
PROCEDURE FRead(VAR f : FILE); VIRTUAL;
END;
IMPLEMENTATION
{---------- Shape methods ----------}
PROCEDURE Shape.Draw;
BEGIN
END;
PROCEDURE Shape.FWrite(VAR f : FILE);
BEGIN
END;
PROCEDURE Shape.FRead(VAR f : FILE);
BEGIN
END;
{---------- Box methods ----------}
CONSTRUCTOR Box.Create(ulx, uly, lrx, lry, c : INTEGER);
BEGIN
UpperLeftX := ulx;
UpperLeftY := uly;
LowerRightX := lrx;
LowerRightY := lry;
Color := c;
END;
PROCEDURE Box.Draw;
BEGIN
SetColor(Color);
Rectangle(UpperLeftX, UpperLeftY, LowerRightX, LowerRightY);
END;
PROCEDURE Box.FWrite(VAR f : FILE);
BEGIN
BlockWrite(f,Color, SizeOf(INTEGER));
BlockWrite(f,UpperLeftX, SizeOf(INTEGER));
BlockWrite(f,UpperLeftY, SizeOf(INTEGER));
BlockWrite(f,LowerRightX,SizeOf(INTEGER));
BlockWrite(f,LowerRightY,SizeOf(INTEGER));
END;
PROCEDURE Box.FRead(VAR f : FILE);
BEGIN
BlockRead(f,Color, SizeOf(INTEGER));
BlockRead(f,UpperLeftX, SizeOf(INTEGER));
BlockRead(f,UpperLeftY, SizeOf(INTEGER));
BlockRead(f,LowerRightX,SizeOf(INTEGER));
BlockRead(f,LowerRightY,SizeOf(INTEGER));
END;
{---------- Ring methods ----------}
CONSTRUCTOR Ring.Create(x, y, rad, c : INTEGER);
BEGIN
Xcenter := x;
Ycenter := y;
Radius := rad;
Color := c;
END;
PROCEDURE Ring.Draw;
BEGIN
SetColor(Color);
Circle(Xcenter, Ycenter, Radius);
END;
PROCEDURE Ring.FWrite(VAR f : FILE);
BEGIN
BlockWrite(f,Color, SizeOf(INTEGER));
BlockWrite(f,Xcenter, SizeOf(INTEGER));
BlockWrite(f,Ycenter, SizeOf(INTEGER));
BlockWrite(f,Radius, SizeOf(INTEGER));
END;
PROCEDURE Ring.FRead(VAR f : FILE);
BEGIN
BlockRead(f,Color, SizeOf(INTEGER));
BlockRead(f,Xcenter, SizeOf(INTEGER));
BlockRead(f,Ycenter, SizeOf(INTEGER));
BlockRead(f,Radius, SizeOf(INTEGER));
END;
END. { UNIT Shapes }
<a name="01d4_0013"><a name="01d4_0013">
<a name="01d4_0014">
[LISTING FIVE]<a name="01d4_0014">
PROGRAM Program3;
USES Graph, Crt, Shapes;
VAR
R1, R2 : Ring;
B1, B2 : Box;
F : FILE;
GrDriver, GrMode, GrResult : INTEGER;
ch : CHAR;
BEGIN
R1.Create(100,100,90,2);
R2.Create(0,0,0,0);
B1.Create(25,10,175,150,4);
B2.Create(0,0,0,0,0);
GrDriver := DETECT;
InitGraph(GrDriver,GrMode,'d:\tp');
GrResult := GraphResult;
IF GrResult <> GrOK THEN
BEGIN
WriteLn('Unable to initialize BGI: error ',grResult);
Halt(1);
END;
Assign(F,'shapes.fil');
Rewrite(F,1);
R1.FWrite(F);
B1.FWrite(F);
Close(F);
Assign(F,'shapes.fil');
Reset(F,1);
R2.FRead(F);
B2.FRead(F);
R2.Draw;
B2.Draw;
REPEAT UNTIL KeyPressed;
Ch := ReadKey;
Close(F);
CloseGraph;
END.
[Example 1: Representing the Box object as a Turbo Pascal record.]
BoxRec = RECORD
BEGIN
Color : INTEGER;
UpperLeftX, UpperLeftY : INTEGER;
LowerRightX, LowerRightY : INTEGER;
END;
[Example 2: A first attempt at writing a procedure to store a
persistent object.]
PROCEDURE WriteShape(f : FILE; s : ^Shape);
BEGIN
BlockWrite(f,s,SizeOf(s^));
END;
[Example 3: A generic WriteShape procedure to store persistent
objects.]
PROCEDURE WriteShape(f : FILE; s : ^Shape);
BEGIN
s->FWrite(f);
END;
[Example 4: A Storable base class (object type) that emulates the
features of the Eiffel STORABLE class.]
Storable = OBJECT
BEGIN
PROCEDURE FWrite(f : FILE);
PROCEDURE FRead(f : FILE);
END;
Shape = OBJECT (Storable)
BEGIN
Color : INTEGER;
PROCEDURE Draw; VIRTUAL;
END;