Modular Programming Problems
The previous section shows, that you already program with some object-oriented
concepts in mind. However, the example implies some problems which we will outline
now.
2.5.1 Explicit Creation and Destruction
In the example every time you want to use a list, you explicitly have to declare a handle
and perform a call to list_create() to obtain a valid one. After the use of the list you must
explicitly call list_destroy() with the handle of the list you want to be destroyed. If you
want to use a list within a procedure, say, foo() you use the following code frame:
PROCEDURE foo() BEGIN
list_handle_t myList;
myList <- list_create();
/* Do something with myList */
...
list_destroy(myList);
END
Let's compare the list with other data types, for example an integer. Integers are declared
within a particular scope (for example within a procedure). Once you've defined them,
you can use them. Once you leave the scope (for example the procedure where the integer
was defined) the integer is lost. It is automatically created and destroyed. Some compilers
even initialize newly created integers to a specific value, typically 0 (zero).
Where is the difference to list ``objects''? The lifetime of a list is also defined by its
scope, hence, it must be created once the scope is entered and destroyed once it is left. On
creation time a list should be initialized to be empty. Therefore we would like to be able
to define a list similar to the definition of an integer. A code frame for this would look
like this:
PROCEDURE foo() BEGIN
list_handle_t myList; /* List is created and initialized */
/* Do something with the myList */
...
END /* myList is destroyed */
The advantage is, that now the compiler takes care of calling initialization and
termination procedures as appropriate. For example, this ensures that the list is correctly
deleted, returning resources to the program.
2.5.2 Decoupled Data and Operations
Decoupling of data and operations leads usually to a structure based on the operations
rather than the data: Modules group common operations (such as those list_...()
operations) together. You then use these operations by providing explicitly the data to
them on which they should operate. The resulting module structure is therefore oriented
on the operations rather than the actual data. One could say that the defined operations
specify the data to be used.
In object-orientation, structure is organized by the data. You choose the data
representations which best fit your requirements. Consequently, your programs get
structured by the data rather than operations. Thus, it is exactly the other way around:
Data specifies valid operations. Now modules group data representations together.
2.5.3 Missing Type Safety
In our list example we have to use the special type ANY to allow the list to carry any
data we like. This implies, that the compiler cannot guarantee for type safety. Consider
the following example which the compiler cannot check for correctness:
PROCEDURE foo() BEGIN
SomeDataType data1;
SomeOtherType data2;
list_handle_t myList;
myList <- list_create();
list_append(myList, data1);
list_append(myList, data2); /* Oops */
...
list_destroy(myList);
END
It is in your responsibility to ensure that your list is used consistently. A possible solution
is to additionally add information about the type to each list element. However, this
implies more overhead and does not prevent you from knowing what you are doing.
What we would like to have is a mechanism which allows us to specify on which data
type the list should be defined. The overall function of the list is always the same,
whether we store apples, numbers, cars or even lists. Therefore it would be nice to
declare a new list with something like:
list_handle_t<Apple> list1; /* a list of apples */
list_handle_t<Car> list2; /* a list of cars */
The corresponding list routines should then automatically return the correct data types.
The compiler should be able to check for type consistency.
2.5.4 Strategies and Representation
The list example implies operations to traverse through the list. Typically a cursor is used
for that purpose which points to the current element. This implies a traversing strategy
which defines the order in which the elements of the data structure are to be visited.
For a simple data structure like the singly linked list one can think of only one traversing
strategy. Starting with the leftmost element one successively visits the right neighbours
until one reaches the last element. However, more complex data structures such as trees
can be traversed using different strategies. Even worse, sometimes traversing strategies
depend on the particular context in which a data structure is used. Consequently, it makes
sense to separate the actual representation or shape of the data structure from its
traversing strategy..
What we have shown with the traversing strategy applies to other strategies as well. For
example insertion might be done such that an order over the elements is achieved or not.