Lecture
One of the most interesting properties of development environments is that, when successfully implemented, they tend to acquire a certain critical mass of functionality and adaptability. In other words, if we have correctly chosen the fundamental abstractions and endowed the library with a number of well-interacting mechanisms, we will soon discover that clients are using our product to solve problems that the developers of the environment never even suspected. Once the main patterns of using the environment have taken shape, it makes sense to make them a formal part of the library itself. A sign that a development environment is correctly constructed is the possibility of introducing new behaviors by reusing existing features of the product and without violating the principles of its architecture.
One such problem is the problem of object lifetime. One may encounter a client who does not want or does not need to use a full-scale object-oriented database, but plans only to save from time to time the state of such structures as queues and sets, so as to be able to retrieve their state on the next invocation from the same program or from another application. Considering that such requirements may arise quite often, it makes sense to supplement our library with a simple mechanism for persisting objects.
Let us make two assumptions concerning this mechanism. First, the client must provide the streams into which the objects will be written and from which they will be read. Second, the client is obliged to provide the objects with the behavior necessary for directing them to a stream.
There are two alternative approaches to creating such a mechanism. One can build a mixin class providing "long-lived" semantics; this is exactly the approach implemented in many object-oriented databases. As an alternative, one can create a class whose instances act as agents responsible for redirecting various structures to a stream. In order to justify our choice, let us try to assess the advantages and disadvantages of each approach.
As it turned out, a mixin is not quite suitable for the very simple persistence mechanism we chose (whereas it fits very well into the architecture of a real object-oriented database). When a mixin is used, the user must add it to his own class himself, often overriding in the process some of the utility functions of the mixin class. In our case, for such a simple mechanism this would prove inefficient, since it would be easier for the user to develop his own facilities than to rework the library's. Thus, we lean toward the second solution, which will require of the user only the creation of an instance of an already existing class.
Figure 9-14 illustrates the working of such a mechanism, which prolongs the life of objects through the work of a separate agent. The class Persist is a friend of the class Queue; we define this relationship inside the declaration of the class Queue as follows:
friend class Persist<Item, Queue<Item>>;
In this case the classes become friends only at the moment the class Queue is instantiated. By embedding such friendship declarations in every abstract base class, we ensure the possibility of using Persist with any structure of the library.
The parameterized class Persist contains the write and read operations put and get, as well as functions for attaching the data exchange streams. We can define this abstraction as follows:
template<class Item, class Structure>
class Persist {
public:
Persist();
Persist(iostream& input, iostream& output);
virtual ~Persist();
virtual void setInputStream(iostream&);
virtual void setOutputStream(iostream&);
virtual void put(Structure&);
virtual void get(Structure&);
protected:
iostream* inStreain;
iostream* outStream;
};

Figure 9-14. Providing persistence by means of an agent.
The implementation of this class depends on its being a friend of the class Structure that figures as a template argument. In particular, Persist depends on the presence in the structure of the helper functions purge, cardinality, itemAt, lock, and unlock. Here the uniformity of our library comes into play: since every base class Structure has such functions, persist can be used without any changes to work with all the structures available in the library.
Consider as an example the implementation of the function Persist::put:
template<class Item, class Structure>
void Persist<Item, Structure>::put(Structure& s)
{
s.lock();
unsigned int count = s.cardinality();
(*outStream) << count << endl;for (unsigned int index = 0; index < count; index++)
(*outStream) << s.itemAt(index);s.unlock();
}
This operation uses the locking mechanism we developed earlier, so it will work for both guarded and synchronized forms. The function's algorithm is straightforward: first the number of elements of the structure is written to the stream, and then, one after another, all its elements. The implementation of persist::get analogously performs the reverse action:
template<class Item, class Structure>
void Persist<Item, Structure>::get(Structure& s)
{
s.lock();
unsigned int count;
Item item;
if (! inStream->eof()) {(*inStream) >> count;}
s.purge();
for (unsigned int index = 0; (index < count) && (! inStream->eof());
index++)
{(*inStream) >> item;}
s.add(item);
s.unlock();
}
In order to use this simple data persistence mechanism, the client need only instantiate one additional class for each structure.
The task of building a development environment is quite difficult. In constructing the fundamental class hierarchies one must take into account various, often contradictory requirements on the system. Try to make your library as flexible as possible: one can never predict just how a developer will try to use it. It is also very important to make it as independent of the programming environment as possible - it will then be easier to use it together with other libraries. The abstractions offered should be as simple, efficient, and understandable to the developer as possible. The most elegant solutions will never be used if the time needed to master them exceeds the time a programmer needs to solve the problem on his own. One will be able to say that the effect has been achieved only when it becomes evident that your abstractions are being reused many times over. That is, when the developer has felt the advantages of using them and is not reinventing the wheel, but is concentrating his attention on those aspects of the problem that no one has yet solved.
Comments