How we represent objects in Active Object Models is driven by several forces, such as:
In this paper we explore some of these forces and show how they influence several design issues of the Argo framework. In particular, we view dynamic, hybrid strategies as a natural phenomenon.
As a basis for the following discussion, we present a simple, but typical scenario when using our framework. The user logs on to the system and selects an application. In this application we expose that part of the object model we are interested in, in particular the object types and properties to be used. In an application, the user may execute a query, list some or all of the instances, display the results in a list view and open a form on a selection of objects to get a more detailed view.
In practice there are many object types that are used in only one or two applications; other object types are more common, but then again, not all of their properties (for instance associations) are relevant in all applications. Depending on the object type, the number of instances may vary considerably. Since users are free to define their own queries and list and form layouts, there is in general no way to specify at development time which properties we need at given time for a particular object or group of objects.
The most obvious object representation in an object-oriented environment is by means of classes. Object properties are represented by means of instance variables; access is handled by get- and set-accessors. In the remainder of this paper we use properties in a broad sense: they denote basic attributes (e.g. strings and dates) as well as references to other objects (arbitrary relationships, type-subtype relationships, part-whole relationships, ....).
Analysis of the characteristics of this approach depends obviously on the semantics of the underlying implementation language, such as typing and reflective facilities, but in general we make the following observations:
Using the well-known variable state pattern (often used in combination with the type object pattern) and a generic accessor protocol, we can set up a much more dynamic environment, with appropriate support for the various generic aspects. A typical implementation will use a property list, i.e. a dictionary mapping properties to the values for these properties. In its simplest form the dictionary keys are names representing properties. Often, when we add typing, when we want to re-use property and type definitions or when we model meta-information explicitly, it makes more sense to use the actual properties as keys and add a parallel mapping: at the type level, properties basically map names onto types, at the object-level, the property lists map properties onto actual values.
Using dictionaries throughout instead of a slot-based approach has it drawbacks: in general we pay a rather hefty space penalty, and performance is likewise affected. This is particularly important if, as in our case, meta-level information is represented in the same way as instance-level objects, stored in the database using the same persistence component, and if the meta-information needs to be consulted at run-time (as is usual in Active Object Models).
Not all properties play the same role. This becomes clear when looking at our meta-model:
We could handle each of these different property roles in different ways. For instance, using regular instance variables and code to represent and access key properties. The price we pay is loss in uniformity and additional adaptor (or glue) code to make meta-level objects behave as regular objects, e.g. when opening a form on an object type, when using them in queries and authorization rules, .... The solution we opted for was simple: cache references to key properties, add optimized behavior in specific classes representing these objects, while adhering to the generic object representation. Removing these optimizations will not break the system, it will only run slower. This transparency is an important asset in keeping a framework design clean.
Defining an object type starts with placing the new object type in the type hierarchy, specifying its basic attribute properties and its relationships to the existing object types. Additional 'virtual' properties may be derived from these 'real' properties, for instance by representing chains of associations as a logical property that may essentially be used as any other property. We distinguish two types of usage:
Clearly, the second type of usage is more 'volatile' than the first, and certainly more dynamic than 'real' properties. Hence it makes sense to find out if the representation of object properties can be fine-tuned using these observations.
When retrieving and displaying lists of objects, the user is initially only interested in a limited set of properties. When he / she has located the objects of interest, more information is needed. Similarly to properties, we can view this as a sort of implicit object type 'definition'.
In our framework we conceptually represent objects in the database as objects with two instance variables:
We use the variable state pattern to represent volatile properties, such as virtual properties defined on the fly in list layouts. The dictionary keys are symbols and uniquely identity the virtual property. The virtual property itself does not exist as an object.
Other properties are mapped onto slots in an array. The number of slots varies in the following cases:
The need for an extra indirection (and the array instance variable) when accessing the array vanishes if we use the Smalltalk reflective facilities: combining indexed instances variables with the "become:" operator yields the desired behavior. This is, in fact, how objects are implemented in our framework.
As we already hinted, we use caching throughout our framework, not only to cache objects retrieved from the database, but also to optimize usage of critical parts of the Active Object Model.
The techniques described above have enabled us to achieve a good compromise between space and time requirements. Even so, some types of applications may require us to go one step further, for instance when we need to support complex computations.
Just-in-time generation of the necessary (and only these) classes and accessor protocols would enable us to achieve these goals. As already mentioned, dynamic languages as Smalltalk make implementation of this approach feasible. The (somewhat) harder part will be to achieve the right degree of transparency, i.e. the generation process must be totally unobtrusive, just like caching techniques. After all, we are interested in instances of our Active Object Model(s), not in instances of classes.
The techniques we have been applying in our framework our fairly simple, and well-known, but they seem to work well in our context. Even so, several variations and hybrid combinations on the themes presented here can further optimize our framework and broaden its scope.