The Active Object System


An Extended Concept of Object Types

Active Oberon for .NET propagates a "new computing model" [3] that is based on a dynamic population of communicating active objects or agents . The corresponding language construct is an upgraded variant of an object type declaration with an optional body for the specification of intrinsic behavior. By convention, the body of an object type is run automatically as a separate thread as a side effect of the creation of an instance.

Ordinary (passive) objects are considered as active objects with an empty intrinsic behavior. They often take the role of shared resources. Figure H.1 depicts the new computing model. It shows a collection of self-managing objects. Some objects (like x and y ) are "active"; others (like the shared resource s ) are "passive." It is worth noting that no driver processes are needed in our scenario to remote-control the evolution of objects. Participating objects communicate via method calls as usual.

Figure H.1. Sample scenario of communicating objects

graphics/hfig01.gif

The code fragment in Listing H.1 outlines a scenario of actors in a hypothetic multimedia production scripted in Active Oberon.

Listing H.1
 TYPE   Link = OBJECT     VAR next: Link; x, y: INTEGER   END Link;   Figure = OBJECT     VAR next: Figure; first: Link;     PROCEDURE NEW (path: Link); (* constructor *)     BEGIN first := path     END NEW;     PROCEDURE Display (X, Y: INTEGER); (* method *)       VAR cur: Link;     BEGIN       IF first # NIL THEN cur := first;         WHILE cur.next # NIL DO           DrawLine(X + cur.x, Y + cur.y, X + cur.next.x,                    Y + cur.next.y);           cur := cur.next         END       END     END Display;   END Figure;   Movie = OBJECT     VAR stopped: BOOLEAN; first, cur: Figure; X, Y: INTEGER;     PROCEDURE NEW (orgx, orgy: INTEGER; video: Figure);                   (* creator & initializer *)     BEGIN X := orgx; Y := orgy; first := video     END NEW;     PROCEDURE Stop;     BEGIN stopped := TRUE     END Stop;   BEGIN { ACTIVE } (* modifier *)     stopped := FALSE; cur := first;     WHILE ~stopped DO       cur.Display(X, Y); cur := cur.next (*cyclic list*)       X := (X + 1) MOD Scene.W; Y := (Y + 1) MOD Scene.H     END   END Movie; 

Note first that no explicit pointers appear in the declarations. In Active Oberon, the keyword OBJECT is used to declare object types that are implicitly pointer-based. Further note that three kinds of objects participate in the Movie scenario: instances of type Link are pure "records" or "structures"; instances of type Figure are ordinary "passive" objects; and instances of type Movie are "active" objects.

Although the members of a typical active object scenario are largely autonomous, they are rarely truly independent of one another. As an immediate consequence, synchronization is required that necessarily manifests itself in temporary "passivation" of individual objects. On this low level of abstraction, the exhaustive enumeration of passivation conditions is surprisingly short:

  • An unsatisfied precondition for continuation

  • Competition for a shared resource object

  • A lack of processors

From the perspective of the object concerned , the first two situations necessarily arise synchronously and are governed by Active Oberon language constructs, while the third situation may arise asynchronously and cause preemption. The language constructs governing the synchronous cases are as follows :

  • AWAIT c

  • BEGIN { EXCLUSIVE } END

The AWAIT statement takes a scope-local Boolean condition c and blocks continuation while c is unasserted. The block statement in combination with the EXCLUSIVE modifier automatically guarantees mutual exclusion within the block.

Figure H.2 summarizes our explanations by showing the time evolution of the scenario depicted in Figure H.1. Note that the thread of activity of each participating object in this figure is represented by a distinguished direction. Also note that object y is passivated twice, for different reasons. Its first passivation occurs when it tries to access the shared resource s (which is blocked by object x at that time). Object y is passivated a second time while awaiting condition c, which is finally established by object z. In both cases, the scheduling is managed completely by the system. This is different from notification-oriented synchronization that fully relies on a fair behavior of the involved peer threads.

Figure H.2. Evolution in time of an Active Object scenario

graphics/hfig02.gif

A Unified Concept of Abstractions

Active Oberon introduces an abstract concept called definition. It roughly corresponds to an abstract class in C++, Java, and C# or, in other words, to an interface with optional state space and optional method implementations .

Note that, unlike the languages mentioned above, Active Oberon syntactically separates the notions corresponding to concrete and abstract classes because, in spite of their formal similarity, they are embodiments of radically different principles. Abstract classes (definitions) are descriptions of abstract concepts to be processed at compile time, while concrete classes (object types) are descriptions of objects to be instantiated at runtime.

Two relations apply to definitions:

  • IMPLEMENTS

  • REFINES

If an object type implements a definition, it commits itself to implement all previously unimplemented methods of this definition and to either accept, complement, or override existing implementations. Every definition implemented by an object type represents a facet or role of this type, and the entirety of implemented definitions constitutes the type's API (application programming interface).

Viewed from the client/server perspective taken in Figure H.3, definitions expose units of service to clients . For example, D is an abstraction of a service provided by object s, and E is an abstraction of a service provided by both objects s and t, with potentially different implementations. Object x is a client of s, making use of service units D and E. Objects y and z are clients of s and t, respectively, using the same service unit E, albeit with potentially different interpretations.

Figure H.3. Client/server scenario in Active Oberon

graphics/hfig03.gif

While definitions in Active Oberon appear as coherent facets or units of use to clients, their specification need not be self-contained but can be derived from a base definition by refinement. For example, Figure H.3 reveals that definition D is actually a refinement of base definition D0.

The full semantics of refinement are intricate and may well include topics such as weakening preconditions and strengthening postconditions in the future. For the moment, it suffices to think of refinement as adding variables , methods, and implementations to an existing definition.

Perhaps the most obvious advantage of our object model over traditional object-oriented systems is symmetry. Facets, roles, service units, and so on can uniformly be modeled as definitions in Active Oberon, ready for implementation in any combination by any concrete object type. In a traditional object-oriented language such as Java, two different ways exist for a concrete class to build on an abstraction A:

  • Subclassing A and inheriting from it

  • Implementing A, possibly by delegating standard work to some "helper class"

Of course, unless multiple inheritance is supported, the superclass option is applicable to at most one "main" abstraction per class.

While in some cases the distinction of a main abstraction is quite natural, it may be artificial in others. To give an example: Assume that some GUI framework supports two abstractions Figure and Sensor ( mouse-sensitive area) and an object type Control . Then the question arises of whether a control is primarily a figure that in addition happens to be a mouse-sensitive area or primarily a sensitive area that happens to be represented as a figure. Both Figure and Sensor qualify equally well as the main abstraction and can equally well serve as the base class of the class Control . If Figure is chosen as the base class, then Sensor must be modeled as an interface to be implemented by Control , possibly assisted by some helper class SensorImplementation . If Sensor is chosen as the base class, then Figure must be modeled as an interface, possibly plus a helper class FigureImplementation . Figures H.4(a) and (b) illustrate the two asymmetric design patterns. Both are definitely inferior to the symmetric Active Oberon pattern depicted in Figure H.5, which consists of two definitions Figure and Sensor plus an object type Control that implements those definitions.

Figure H.4. (a) Asymmetric design pattern number 1 for class Control ; (b) Asymmetric design pattern number 2 for class Control

graphics/hfig04a.gif

graphics/hfig04b.gif

Figure H.5. Symmetric design pattern for class Control

graphics/hfig05.gif

The most negative aspect of enforced asymmetry is an inherent ambiguity and lack of a "canonical form" for the presentation and use of abstractions. This can easily be confirmed when reading, for example, any tutorial on concurrent programming in Java. The controversial choice is between (1) extending the class Thread and (2) implementing the interface Runnable . Even the most sophisticated explanations of a conceptual difference between options 1 and 2 are much less convincing than the sheer pragmatic reason of using variant 2 whenever the class to be made concurrent already extends some other base class such as Applet .

A second significant advantage of our object model is the "lightweight" way of object usage. Clients need not be aware of the full "type" of a server object, but merely need to know the signatures of the services they actually use. Obviously, such a minimum-knowledge model is particularly beneficial in cases that involve remote-service objects whose full specifications are locally unavailable.

We should note at this point that Active Oberon embodies an object model that in many ways resembles that of the original COM. In particular, the two models coincide in their interface-oriented object view and in the nonuse of polymorphic typing and subclassing. However, they differ in the form of implementation support. While COM carefully avoids any form of implementation inheritance and refers to "hand-knitted" methods as delegation and aggregation, Active Oberon inherently supports multiple inheritance of predefined standard functionality at compile time (in contrast to languages such as C++ and Eiffel, which support multiple inheritance at runtime). As a matter of fact, we could roughly characterize Active Oberon as "COM plus built-in aggregation."

It is also worth noting that the concept of definition could well be generalized. A more progressive version of this concept would, for example, allow HTML documents and XML tags to be viewed as definitions and thereby provide a cleaner and more powerful way of scripting.

Let us now get more technical and assume a configuration as caricatured in the Active Oberon code in Listing H.2.

Listing H.2
 DEFINITION I; (* pure interface *)   PROCEDURE { ABSTRACT }  f (..); (*abstract method *) END I; DEFINITION D;   VAR x: X; (* public state variable *)   VAR { PROTECTED } y: Y; (* protected state variable *)   PROCEDURE { ABSTRACT } e (..); (* abstract method *)   PROCEDURE { ABSTRACT } f (..);   PROCEDURE g (..): ..; (* standard implementation *)     VAR u, v: U;   BEGIN.. RETURN ..   END g;   PROCEDURE { FINAL } h (..); (* final implementation *)   BEGIN .. f(..); ..   END h; END D; DEFINITION D1 REFINES D;   VAR { PROTECTED } z: Z; (* new protected state variable *)   PROCEDURE e (..); (* standard implementation *)   BEGIN .. x ..   END e;   PROCEDURE g (..); (* new standard implementation *)   BEGIN .. RETURN D.g(..) + ..   END g;   PROCEDURE { ABSTRACT } k (..);(* new abstract method *) END D1; TYPE A = OBJECT IMPLEMENTS I, D1;   ..   PROCEDURE f (..); IMPLEMENTS I.f, D1.e;   BEGIN .. D1.x ..   END f;   PROCEDURE f1 (..); IMPLEMENTS D1.f;   BEGIN .. D1.e(..); ..   END f1;   PROCEDURE k (..); IMPLEMENTS D1.k;   BEGIN   END k;   .. END A; 

We immediately observe the following:

  • x is the only public state variable in D and D1 . y and z are protected variables whose access is restricted to refining definitions and implementing object types.

  • A meets its obligations by providing implementations for I.f , D1.f , and D1.k .

  • A customizes the implementation of method D1.e .

  • Within D1 and A , qualification is used to identify members of a different scope. For example, D1.x and D1.e in A refer to the variable x and to the standard implementation of e in D1 , respectively.

  • In this example, A IMPLEMENTS D1 implicitly implies A IMPLEMENTS D .

Let us now assume that a second refinement D2 of definition D exists and that A is supposed to implement both D1 and D2 . Then we are faced with a more complex case of intersecting chains of refinement (see Listing H.3).

Listing H.3
 DEFINITION D2 REFINES D;    PROCEDURE k (..);(* new method *)   BEGIN ..   END k; END D2; TYPE A = OBJECT IMPLEMENTS I, D1, D2;   ..   PROCEDURE f (..); IMPLEMENTS I.f, D1.e;   BEGIN ..   END f;   PROCEDURE f1 (..); IMPLEMENTS D1.f;   BEGIN ..   END f1;   PROCEDURE f2 (..); IMPLEMENTS D2.f;   BEGIN ..   END f2;   PROCEDURE p (..); IMPLEMENTS D2.g;   BEGIN ..   END p;   PROCEDURE k (..); IMPLEMENTS D1.k, D2.k;   BEGIN ..   END k;   .. END A; 

In this case, the definitions implemented by A ( D1 and D2 ) have a common root ( D ), and some semantic clarification is needed. First, we declare D 's state space to be shared by D1 and D2 ; that is, x refers to the same instance of the variable wherever it occurs in either D1 or D2 . Second, we point out that the implication A IMPLEMENTS D1 A IMPLEMENTS D is no longer unconditionally true. By convention, this implication holds if and only if, for each method of D , the identification of its implementation is unambiguous. In our example, A does not implement D because the identification of f 's implementation is ambiguous. We note in passing that this convention is compatible with the more general semantic model based on "dominants" [4, 5] and is adapted by C++, for example.

We can best illustrate this rule by an example. Assume that our\animtext4 Sensor abstraction features a method called Handle . Further assume that\animtext4 ButtonSensor and PenSensor are refinements of Sensor providing\animtext4 individual implementations of Handle . Finally, assume that ButtonPenControl implements both refinements simultaneously . Then, a necessary and sufficient condition for ButtonPenControl to also implement the abstract Sensor definition is a reimplementation of Handle (most probably based on ButtonSensor.Handle and PenSensor.Handle ).

Let us now briefly change our perspective and turn to the client's site. In accordance with the "lightweight" view of objects by clients are polymorphic declarations that emphasize the desired facets of servers rather than their full "type". For example,

 a: OBJECT { D1, D2 } 

defines an object variable or parameter a with guaranteed implementations of the definitions D1 and D2 . The following would then be a possible scenario of use:

 .. D1(a).f(..); D2(a).k(..); .. := D1(a).x; .. 

Alternatively, a could be declared completely generically and be used under the safeguard of an IMPLEMENTS clause:

 a: OBJECT  IF a IMPLEMENTS D1, D2 THEN   .. D1(a).f(..); D2(a).k(..); .. := D1(a).x; .. END; 

Note that type-specific object variable declarations are still possible in Active Oberon. However, they are essentially used only in combination with abstract data types and external operators. For example,

 TYPE T = OBJECT ... END;  PROCEDURE "@" (x: T): T ; PROCEDURE "#" (x, y: T): T; 

defines an abstract data type T featuring two operations @ and # .

A Concept of Static Modules

In combination with a static import relation, the module construct is a simple but extremely useful tool for structuring large software systems according to the principle of separation of concerns. If augmented by guaranteed version compatibility between importing and imported modules, the module/import combination is in addition an ideal framework for maintaining large software systems.

In Active Oberon for .NET, modules have dual semantics of package and static object. More precisely, an Active Oberon module is both

  • A package representing simultaneously a namespace and a compilation unit

  • A static object together with the following conventions of instantiation:

    • The object initializer is represented by the module body.

    • As a precondition of instantiation, all imported modules (that is, their static objects) must be created and initialized in a compatible version.

For example, the module declaration in Listing H.4 defines the following items:

  • A package M consisting of three object types M.A , M.X , and M.Y .

  • A static object M whose state variables are a , s , and t , whose methods are F and G , and whose initializer is the BEGIN END block at the end of the module text. In addition, the import clause requires modules L and N to be initialized and ready for delegation before M is instantiated.

Listing H.4
 MODULE M;   IMPORT L, N;   TYPE     A = OBJECT { PUBLIC } END A;     X = OBJECT { PUBLIC }       VAR { PUBLIC } a: A;       VAR x: X;       ...   : END X;     Y = OBJECT ... END Y;   VAR { PUBLIC } a: A;   PROCEDURE { PUBLIC } F (t: T): A;   VAR s, t: T;   BEGIN ...   END F;   PROCEDURE G (a: A);   BEGIN ...   END G; BEGIN (* module initialization *)     ... N ... (* delegate work to module N *) END M. 

By default, the elements of a module scope are private to the module. However, the PUBLIC modifier may be used to declare any module element as "exported" ”that is, as visible from the outside. In Listing H.4, the object types A and X are public, while Y is private. Further, the members a and F of the static object M are public, while s , t , and G are private.

By convention, the members of object type scopes are private to the enclosing module. However, public object types may declare public members again by simply adding a PUBLIC modifier. For example, in object type X , state variable a is public, while x is private.

It is worth noting at this point that "static" module objects distinguish themselves from "ordinary" Active Oberon objects in only one point: They are created and managed by the system rather than by application programs. As a consequence, Active Oberon modules can implement definitions exactly as object types can and, correspondingly, they can equally be used by clients via definitions.

In concluding these explanations, we emphasize that the system is expected to guarantee version compatibility between L , M , and N at both compile time and runtime.



Programming in the .NET Environment
Programming in the .NET Environment
ISBN: 0201770180
EAN: 2147483647
Year: 2002
Pages: 146

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net