Examples

In this section, two examples are presented to give the reader an impression of a typical Omega programming style. The first example shows the implementation of a generic stack, the second example shows part of the implementation of a set of prototypes for persons attending a workshop or conference. In order to cut down the size of the examples, elementary types and existing prototypes will be used where applicable. Both examples are, of course, developed in an object-oriented fashion. However, only some basic design ideas will be discussed here. The reader is therefore expected to have sufficient knowledge of object-oriented design.


Example 1 - A Generic Stack

A typical aspect of programming in Omega (and object-oriented programming in general) is reuse. An Omega programmer would therefore not implement his/her own stack but rather use the one that is already supplied with the Omega library. Stack is a generic prototype that can be parameterized with an arbitrary type. For example, Stack{Integer} and Stack{Person} would be two legal incarnations of Stack. In order to show how a generic stack could be implemented in a simple programming exercise, we will show the development of a new prototype MyStack. We will only implement the absolutely necessary methods and then show how such stack objects can be used.


Step 1 - Finding the proper supertype

The Omega library contains a subtree with the root Collection. Such collections are structured objects that can maintain (references to) several other objects. The interface of Collection already provides operations for adding and removing elements and for iteration over all elements of a collection. It is therefore tempting to derive MyStack from Collection in order to reuse these operations. This is, however, not recommended as clients of a stack should not be allowed to access elements other than the element on top of the stack. A (from the software engineering point of view) better solution therefore is to derive MyStack from Object and add those few methods needed to implement a stack.


Step 2 - Creating the MyStack prototype

In the hierarchy browser, we select the Object prototype, invoke the New Type menu command and complete the resulting dialog as shown in Figure 5.

Figure 5: Creation of the MyStack prototype

When the OK button is pressed, a clone of the Object prototype is created and defined as the new generic prototype MyStack. The default parameter type is Object, which means that MyStack can be parameterized with any type. As a result of this operation, MyStack appears in the hierarchy browser as a direct descendent of Object. At this point, MyStack is already globally known and can be used throughout the entire workspace, although this is not yet meaningful.


Step 3 - Defining the new type's structure

The MyStack prototype is opened in the hierarchy browser for inspection. In the variable view, an instance variable named content of type Array{Parameter} is added (see Figure 6). The location and visibility of the variable are defined as local and heritage, respectively. Parameter is a pseudo-type that represents the formal parameter type with which the current incarnation of MyStack is parameterized. When a new object MyStack{Integer} is created, the instance variable content is given the type Array{Integer}. Figure 6 shows how the object then appears in the object editor. The components map and mark are inherited from Object; they are present in each object and used by the run-time system.

Figure 6: The MyStack prototype with the new instance variable content


Step 4 - Overriding inherited methods

When creating a new prototype, we first have to consider which methods must be overridden. In the case of MyStack, we only have to override the method copyParts which is responsible for copying objects referenced by instance variables when the object receives the message copy. In the case of MyStack, a shallow copy of the array must be created using the message clone.

  copyParts -> Same
  [ content := content clone; self ]


The pseudo-type Same represents the type of the receiver of a message. It is similar to Eiffel's "like Current". One of the uses of Same is to indicate that a method will return an object of the same (static and dynamic) type as the receiver. It is therefore used wherever a message returns the receiver (self).



Step 5 - Defining additional methods

Now it is time to implement the actual methods for manipulating the stack. We simply list the methods first; explanations of certain detail are given below.

clear -> Same
  --initializes the stack with an empty array of elements
  [ content := Array{Parameter} copy; self ]
 
size -> Integer
  --returns the number of elements in the stack
  [ content size ]
 
push: Parameter -> Same
  --appends a new element at the end of the array
  {newElem:Parameter}
  [ content add:newElem; self ]
 
pop -> Parameter
  --returns the top element and removes it from the array
  [ n::=self size; top::=content at: n; content delete:n; top ]


That's all there is to do to define the basic behavior of MyStack. New elements are simply appended to the array referenced by the variable content. This is possible because Omega arrays are not declared with a fixed size; they rather expand and shrink as needed. When the message pop is sent to an empty stack, the variable n will have the value zero, thus leading to a range error when attempting to access the nth element of the array with the message content at:n. The exception handling mechanism of Omega can the be used to perform appropriate corrective actions. If a more descriptive exception is to be generated, the method pop could be implemented as follows:

pop -> Parameter
  --returns the top element and removes it from the array
  [ n::=self size; "pop only works for non-empty stacks" assertion: n>0;
    top::=content at: n; content delete:n; top ]
 
 

 

Step 6 - Using MyStack objects

The following code fragment shows how MyStack objects can be used. It assumes that the variable stk refers to an existing stack and uses a second stack tmp to reverse the order of the elements in stk.

 tmp::=stk copy;  stk clear;
  [tmp size>0] whileTrue:[stk push:tmp pop];


As MyStack inherits from Object, many predefined operations can be applied to MyStack objects without having to implement additional methods. For example, activation and passivation of objects is implemented in a general way in Object. Circular references and so-called unique objects (that are supposed to exist only once) are automatically taken care of by this mechanism. In this way, MyStack objects can be written to and read from files, copied to the clipboard of the Macintosh and sent to remote computers.

To make MyStack attractive for future reuse, a couple of additional methods would be needed. It would, for example, be convenient to concatenate stacks and to inspect their contents in a window. For this reason, the Stack prototype in the Omega library contains sixteen methods instead of the five methods shown above.


Example 2 - Conference Attendees

As a second example, we will show the initial steps of the development of a set of prototypes for maintaining persons at a conference. This example is a small exercise in object-oriented design rather than a typical example of Omega programming style. Once adequate design decisions have been made, the implementation is quite simple and straightforward.


Step 1 - Developing the prototype hierarchy

The most important issue to settle is how to deal with attendees who have more than one role at the conference. For example, a single person could be a presenter in one session, chair in another session and also an exhibitor. It would be tempting to use multiple inheritance to model multiple roles, but such a solution would cause more problems than it solves. Besides that, multiple inheritance cannot cope with changing roles (for example, when a speaker decides to present a poster during the conference). Fortunately, Omega has single inheritance only, so we have to look for another solution, anyway. We decide to develop a single prototype Person (with all unique attributes of a person) and a hierarchy of roles with an abstract root prototype Role. Figure 7 shows a subset of the resulting hierarchy.

Figure 7: Persons and roles in the prototype hierarchy

Instead of saying that a person is a presenter we rather say that a person has the role of a presenter. As a single person may have several roles, we include an instance variable roles of type Set{Role} in Person. An empty set is associated with a person who only attends the conference.


Step 2 - Defining the prototypes' structure

Next, we have to determine the interfaces and the structure of the prototypes. To simplify matters, only the most relevant instance variables and methods are listed in the sequel. In order to be practically usable (and reusable), finer granularity and more types (for example, Address with subtypes for countries with different conventions) would be necessary.

prototype

instance variable

type

initial content

Person

name

String

"unknown"

organisation

String

"unknown"

address

String

"unknown"

roles

Set {Role}

empty set

Role

no additional instance variables

SessionRole

session

Session

Nil

Chair

no additional instance variables

Presenter

title

String

"unknown"

timeSlot

Integer

0

As the table above shows, a new type Session would be convenient. All roles relating to a particular session could then refer to the same Session object. This makes it easy to make global changes to a session (for example, to change a session's title or to shift it in the timetable).

The above definitions can all be made without implementing a single line of code. The interactive "declaration" and initialization of these five prototypes takes no more than two or three minutes.

According to the principles of information hiding, all instance variables were defined with the visibility attribute heritage. This guarantees, for example, that clients cannot inadvertently access and modify the session of a SessionRole, but the subtypes Chair and Presenter have unlimited access to this instance variable.


Step 3 - Implementing the basic methods

What methods are to be provided depends on the context in which our objects will be used. It is certainly advisable to implement simple access methods to examine and change the instance variables. It would, of course, be easier to simply define the instance variables as public, but implementing methods for this purpose is more flexible with respect to future changes of the objects' structure. In order to avoid external references to the objects referred to by instance variables, access methods often create copies of the objects in question. The following example shows typical implementations of methods for accessing a person's name. By convention, the method with the same name as the instance variable returns the requested value, and a method ending with a colon modifies the instance variable.

name -> String
  --returns the name of a person
  [ name copy ]
 
name: String -> Same
  --changes the name of a person
  {newName:String}
  [ name:=newName copy; self changed ]

The message self changed at the end of the name: method indicates that a significant change had been made to the receiver. Omega's change propagation mechanism then informs objects depending on the receiver (such as windows that display the contents of an object) of the change and gives this objects a chance to update themselves accordingly.

The access methods for the remaining instance variables would have a similar structure. They are not listed here for the sake of brevity.


Step 4 - Implementing additional methods

What remains to do is to implement other methods as needed. Such methods are typically rather small and straightforward. The following example shows how a textual representation of a person (which may then be printed or displayed on the screen) can be produced.

asString -> String
  --returns a textual representation of the receiver
  [ str::=name+String.eol+organization+String.eol+address+String.eol;
    roles forAll:{r:Role}[str add:r asString+String.eol];
    str ]

The message asString is implemented in Object with a default behavior (it returns just the name of the object's type). It should be overridden if an object has something more meaningful to return. String.eol is a public shared read-only variable (i.e., a global constant) that contains the end-of-line character(s). The asString method of Person simply concatenates the individual components and then appends the textual representations of all roles (again using the message asString). The following example shows what the result could look like.

Guenther Blaschek
Johannes Kepler University
Altenbergerstrasse 69, A-4040 Linz, Austria
presenter ("Omega" in Session 3, 10:00-10:30)

 

Step 5 - Using the objects

Once the prototypes have been defined, they can easily be used to create, say, an array of conference attendees. The following code fragment shows how such an array can be created, filled, and printed.

  attendees::=Array{Person} copy;
  mySelf::=Person copy
    name:"Guenther Blaschek",
    organization:"Johannes Kepler University",
    address:"Altenbergerstrasse 69, A-4040 Linz, Austria",
    addRole:(Presenter copy session:3, title:"Omega", timeSlot:3);
  attendees add:mySelf;
  ...
  attendees print;



Previous Sections:

Following Sections:


This page has been visited times.