amiga-e/amigae33a/E_v3.3a/Docs/BeginnersGuide/OOE.guide

930 lines
38 KiB
Plaintext

@database beginner.guide
@Master beginner
@Width 75
This is the AmigaGuide® file beginner.guide, produced by Makeinfo-1.55 from
the input file beginner.
@NODE "main" "Object Oriented E"
@Next "Examples.guide/main"
@Prev "Recursion.guide/main"
@Toc "Contents.guide/main"
Object Oriented E
*****************
The Object Oriented Programming (OOP) aspects of E are covered in this
chapter. Don't worry if you don't know the OOP buzz words like `object',
`method' and `inheritance': these terms are explained in the OOP
introduction, below. (For some reason, computer science uses strange
words to cloak simple concepts in secrecy.)
@{" OOP Introduction " Link "OOP Introduction" }
@{" Objects in E " Link "Objects in E" }
@{" Methods in E " Link "Methods in E" }
@{" Inheritance in E " Link "Inheritance in E" }
@{" Data-Hiding in E " Link "Data-Hiding in E" }
@ENDNODE
@NODE "OOP Introduction" "OOP Introduction"
@Next "Objects in E"
@Toc "main"
OOP Introduction
================
`Object Oriented Programming' is the name given to a collection of
programming techniques that are meant to speed up development and ease
maintenance of large programs. These techniques have been around for a
long time, but it is only recently that languages that explicitly support
them have become popular. You do not @{i }need@{ui } to use a language that supports
OOP to program in an Object Oriented way; it's just a bit simpler if you
do!
@{" Classes and methods " Link "Classes and methods" }
@{" Example class " Link "Example class" }
@{" Inheritance " Link "Inheritance" }
@ENDNODE
@NODE "Classes and methods" "Classes and methods"
@Next "Example class"
@Toc "OOP Introduction"
Classes and methods
-------------------
The heart of OOP is the `Black Box' approach to programming. The kind
of black box in question is one where the contents are unknown but there
is a number of wires on the outside which give you some way of interacting
with the stuff on the inside. The black boxes of OOP are actually
collections of data (just like the idea of variables that we've already
met) and they are called @{fg shine }objects@{fg text } (this is the general term, which is,
coincidentally, connected with the @{b }OBJECT@{ub } type in E). Objects can be
grouped together in @{fg shine }classes@{fg text }, like the types for variables, except that a
class also defines what different kinds of wires protrude from the black
box. This extra bit (the wires) is known as the @{fg shine }interface@{fg text } to the
object, and is made up of a number of @{fg shine }methods@{fg text } (so a method is analogous
to a wire). Each method is actually just like a procedure. With a real
black box, the wires are the only way of interacting with the box, so the
methods of an object ought to be the @{i }only@{ui } way of creating and using the
object. Of course, the methods themselves normally need to know the
internal workings of the object, just like the way the wires are normally
connected to something inside the black box.
There are two special kinds of methods: @{fg shine }constructors@{fg text } and
@{fg shine }destructors@{fg text }. A @{fg shine }constructor@{fg text } is a method which is used to
initialise the data in an object, and a class may have several different
constructors (allowing for different kinds of initialisation) or it may
have none if no special initialisation is necessary. Constructors are
normally used to allocate the resources (such as memory) that an object
needs. The deallocation of such resources is done by the @{fg shine }destructor@{fg text }, of
which there is at most one for each class.
Protecting the contents of an object in the `black box' way is known as
@{fg shine }data-hiding@{fg text } (the data in the object is visible only to its methods), and
only allowing the contents of an object to be manipulated via its
interface is known as @{fg shine }data abstraction@{fg text }. By using this approach, only
the methods know the structure of the data in an object and so this
structure can be changed without affecting the whole of a program: only
the methods would potentially need recoding. As you might be able to
tell, this simplifies maintenance quite considerably.
@ENDNODE
@NODE "Example class" "Example class"
@Next "Inheritance"
@Prev "Classes and methods"
@Toc "OOP Introduction"
Example class
-------------
A good example of a class is the mathematical notion of a set (of
integers). A particular object from this class would represent a
particular set of integers. The interface for the class would probably
include the following methods:
1. @{b }Add@{ub } -- adds an integer to a set object.
2. @{b }Member@{ub } -- tests for membership of an integer in a set object.
3. @{b }Empty@{ub } -- tests for emptiness of a set object.
4. @{b }Union@{ub } -- unions a set object with a set object.
A more complete class would also contain methods for removing elements,
intersecting sets etc. The important thing to notice is that to use this
class you need to know only how to use the methods. The black box
approach means that we don't (and shouldn't) know how the set class is
actually implemented, i.e., how data is structured within a set object.
Only the methods themselves need to know how to manipulate the data that
represents a set object.
The benefit of OOP comes when you actually use the classes, so suppose
you implement this set class and then use it in your code for some
database program. If you found that the set implementation was a bit
inefficient (in terms of memory or speed), then, since you programmed in
this OOP way, you wouldn't have to recode the whole database program, just
the set class! You can change the way the set data is structured in an
object as much and as often as you like, so long as each implementation
has the same interface (and gives the same results!).
@ENDNODE
@NODE "Inheritance" "Inheritance"
@Prev "Example class"
@Toc "OOP Introduction"
Inheritance
-----------
The remaining OOP concept of interest is @{fg shine }inheritance@{fg text }. This is a
grand name for a way of building on classes that enables the @{fg shine }derived@{fg text }
(i.e., bigger) class to be used as if its objects were really members of
the inherited, or @{fg shine }base@{fg text }, class. For example, suppose class @{b }D@{ub } were
derived from class @{b }B@{ub }, so @{b }D@{ub } is the derived class and @{b }B@{ub } is the base class.
In this case, class @{b }D@{ub } inherits the data structure of class @{b }B@{ub }, and may add
extra data to it. It also inherits all the methods of class @{b }B@{ub }, and
objects of class @{b }D@{ub } may be treated as if they were really objects of class
@{b }B@{ub }.
Of course, an inherited method cannot affect the extra data in class @{b }D@{ub },
only the inherited data. To affect the extra data, class @{b }D@{ub } can have extra
methods defined, or it can make new definitions for the inherited methods. The
latter approach is only really useful if the new definition of an
inherited method is pretty similar to the inherited method, differing only
in how it affects the extra data in class @{b }D@{ub }. This overriding of methods
does not affect the methods in class @{b }B@{ub } (nor those of other classes derived
from @{b }B@{ub }), but only those in class @{b }D@{ub } and the classes derived from @{b }D@{ub }.
@ENDNODE
@NODE "Objects in E" "Objects in E"
@Next "Methods in E"
@Prev "OOP Introduction"
@Toc "main"
Objects in E
============
Classes are defined using @{b }OBJECT@{ub } in the same way that we've seen before
(see @{"OBJECT Type" Link "Types.guide/OBJECT Type" }). So, in E, the terms `object declaration' and `class'
may be used interchangeably. However, referring to an @{b }OBJECT@{ub } type as a
`class' signals the presence of methods in an object.
The following example @{b }OBJECT@{ub } is the basis of a set class, as described
above (see @{"Example class" Link "Example class" }). This set implementation is going to be quite
simple and it will be limited to a maximum of 100 elements.
OBJECT set
elts[100]:ARRAY OF LONG
size
ENDOBJECT
Currently, the only way to allocate an OOP object is to use @{b }NEW@{ub } with an
appropriately typed pointer. The following sections of code all allocate
memory for the data of @{b }set@{ub }, but only the last one allocates an OOP @{b }set@{ub }
object. Each one may use and access the @{b }set@{ub } data, but only the last one
may call the methods of @{b }set@{ub }.
DEF s:set
DEF s:PTR TO set
s:=NewR(SIZEOF set)
DEF s:PTR TO set
s:=NEW s
OOP objects can, of course, be deallocated using @{b }END@{ub }, in which case the
destructor for the corresponding class is also called. Leaving an OOP
object to be deallocated automatically at the end of the program is not
quite as safe as normal, since in this case the destructor will not be
called. Also, when using @{b }END@{ub } to deallocate an object you do not need to
use a pointer of exactly the same type as the object (like you would for
normal @{b }NEW@{ub } allocations). Instead you can use a pointer of any of the base
classes' types. Constructors and destructors are described in more detail
below.
@ENDNODE
@NODE "Methods in E" "Methods in E"
@Next "Inheritance in E"
@Prev "Objects in E"
@Toc "main"
Methods in E
============
The @{fg shine }methods@{fg text } of E are very similar to normal procedures, but there is
one, big difference: a method is part of a class, so must somehow be
identified with the other parts of the class. In E this identification is
done by relating all methods to the corresponding @{b }OBJECT@{ub } type for the
class, using the @{b }OF@{ub } keyword after the description of the method's
parameters. So, the methods of the simple set class would be defined as
outlined below (of course, these examples have omitted the code of
methods).
PROC add(x) OF set
/* code for add method */
ENDPROC
PROC member(x) OF set
/* code for member method */
ENDPROC
PROC empty() OF set
/* code for empty method */
ENDPROC
PROC union(s:PTR TO set) OF set
/* code for union method */
ENDPROC
At first sight it might seem that the particular @{b }set@{ub } object which would
be manipulated by these methods is missing from the parameters. For
instance, it appears that the @{b }empty@{ub } method should need an extra @{b }PTR TO set@{ub }
parameter, and that would be the @{b }set@{ub } object it tested for emptiness.
However, methods are called in a slightly different way to normal
procedures. A method is a part of a class, and is called in a similar way
to accessing the data elements of the class. That is, the method is
selected using @{b }.@{ub } and acts (implicitly) on the object from which it was
selected. The following example shows the allocation of a set object and
the use of some of the above methods.
DEF s:PTR TO set
NEW s -> Allocate an OOP object
s.add(17)
s.add(-34)
IF s.empty()
WriteF('Error: the set s should not be empty!\\n')
ELSE
WriteF('OK: not empty\\n')
ENDIF
IF s.member(0)
WriteF('Error: how did 0 get in there?\\n')
ELSE
WriteF('OK: 0 is not a member\\n')
ENDIF
IF s.member(-34)
WriteF('OK: -34 is a member\\n')
ELSE
WriteF('Error: where has -34 gone?\\n')
ENDIF
END s -> Finished with s now
This is why the methods do not take that extra @{b }PTR TO set@{ub } argument. If a
method is called then it has been selected from an appropriate object, and
so this must be the object which it affects. The slightly complicated
method is @{b }union@{ub } which adds another @{b }set@{ub } object by unioning it. In this
case, the argument to the method is a @{b }PTR TO set@{ub }, but this is the set to
be added, not the set which is being expanded.
So, how do you refer to the object which is being affected? In other
words, how do you affect it? Well, this is the remaining difference from
normal procedures: every method has a special local variable, @{b }self@{ub }, which
is of type @{b }PTR TO @{ub }@{fg shine }class@{fg text }@{b }@{ub } and is initialised to point to the object from
which the method was selected. Using this variable, the data and methods
of object can be accessed and used as normal. For instance, the @{b }empty@{ub }
method has a @{b }self@{ub } local variable of type @{b }PTR TO set@{ub }, and can be defined as
below:
PROC empty() OF set IS self.size=0
@{fg shine }Constructors@{fg text } are simply methods which initialise the data of an
object. For this reason they should normally be called only when the
object is allocated. The @{b }NEW@{ub } operator allows OOP objects to call a
constructor at the point at which they are allocated, to make this easier
and more explicit. The constructor will be called after @{b }NEW@{ub } has allocated
the memory for the object. It is wise to give constructors suggestive
names like @{b }create@{ub } and @{b }copy@{ub }, or the same name as the class. The following
constructors might be defined for the set class:
/* Create empty set */
PROC create() OF set
self.size=0
ENDPROC
/* Copy existing set */
PROC copy(oldset:PTR TO set) OF set
DEF i
FOR i:=0 TO oldset.size-1
self.elements[i]:=oldset.elements[i]
ENDFOR
self.size:=oldset.size
ENDPROC
They would be used as in the code below. Notice that the @{b }create@{ub }
constructor is, in this case, redundant since @{b }NEW@{ub } will initialise the data
elements to zero. If @{b }NEW@{ub } does sufficient initialisation then you do not
have to define any constructors, and even if you do have constructors you
don't @{i }have@{ui } to use them when allocating objects.
DEF s:PTR TO set, t:PTR TO set, u:PTR TO set
NEW s.create()
IF s.empty THEN WriteF('s is empty\\n')
END s
NEW t /* This happens to be the same as using create */
IF t.empty THEN WriteF('t is empty\\n')
t.add(10)
NEW u.copy(t)
IF u.member(10) THEN WriteF('10 is in u\\n')
END t, u
For each class there is at most one @{fg shine }destructor@{fg text }, and this is
responsible for clearing up and deallocating resources. If one is needed
then it must be called @{b }end@{ub }, and (as this might suggest) it is called
automatically when an OOP object is deallocated using @{b }END@{ub }. So, for OOP
objects with a destructor, the (roughly) equivalent code to @{b }END@{ub } using
@{b }Dispose@{ub } is a bit different. Take care to note that the destructor is @{i }not@{ui }
called if @{b }END@{ub } is not used to deallocate an OOP object (i.e., if
deallocation is left to be done automatically at the end of the program).
END p
IF p
p.end() -> Call destructor
Dispose(p)
p:=NIL
ENDIF
The simple implementation of the set class needs no destructor. If,
however, the @{b }elements@{ub } data were a pointer (to @{b }LONG@{ub }), and the array were
allocated based on some size parameter to a constructor, then a destructor
would be useful. In this case the set class would also need a @{b }maxsize@{ub }
data element, which records the maximum, allocated size of the @{b }elements@{ub }
array.
OBJECT set
elements:PTR TO LONG
size
maxsize
ENDOBJECT
PROC create(sz=100) OF set -> Default to 100
DEF p:PTR TO LONG
self.maxsize:=IF (sz>0) AND (sz<100000) THEN sz ELSE 100
self.elements:=NEW p[self.maxsize]
ENDPROC
PROC end() OF set
DEF p:PTR TO LONG
IF self.maxsize=0
WriteF('Error: did not create() the set\\n')
ELSE
p:=self.elements
END p[self.maxsize]
ENDIF
ENDPROC
Without the destructor @{b }end@{ub }, the memory allocated for @{b }elements@{ub } would not be
deallocated when @{b }END@{ub } is used, although it would get deallocated at the end
of the program (in this case). However, if @{b }AllocMem@{ub } were used instead of
@{b }NEW@{ub } to allocate the array, then the memory would have to be deallocated
using @{b }FreeMem@{ub }, and this would best be done in the destructor, as above.
(The memory would not be deallocated automatically at the end of the
program if @{b }AllocMem@{ub } is used.) Another solution to this kind of problem
would be to have a special method which called @{b }FreeMem@{ub }, and to remember to
call this method just before deallocating one of these objects, so you can
see that the interaction of @{b }END@{ub } with destructors is quite useful.
Already, the above re-definition of @{b }set@{ub } begins to show the power of OOP.
The actual implementation of the set class is very different, but the
interface can remain the same. The code for the methods would need to
change to take into account the new @{b }maxsize@{ub } element (where before the
fixed size of 100 was used), and also to deal with the possibility the
@{b }create@{ub } constructor had not been used (in which case @{b }elements@{ub } would be @{b }NIL@{ub }
and @{b }maxsize@{ub } zero). But the code which used the set class would not need
to change, except maybe to allocate more sensibly sized sets!
Yet another, different implementation of a set was outlined above (see
@{"Binary Trees" Link "Recursion.guide/Binary Trees" }). In fact, remarkably few changes would be needed to convert
the code from that section into another implementation of the set class.
The @{b }new_set@{ub } procedure is like a set constructor which initialises the set
to be a singleton (i.e., to contain one element), and the @{b }add@{ub } procedure is
just like the @{b }add@{ub } method of the set class. The only slight problem is
that empty sets are not modelled by the binary tree implementation, so it
wouldn't, as it stands, be a complete implementation. It would be
straight-forward (but unduly complicated at this point) to add support for
empty sets to this particular implementation.
@ENDNODE
@NODE "Inheritance in E" "Inheritance in E"
@Next "Data-Hiding in E"
@Prev "Methods in E"
@Toc "main"
Inheritance in E
================
One class is @{fg shine }derived@{fg text } from another using the @{b }OF@{ub } keyword in the
definition of the derived class @{b }OBJECT@{ub }, in a similar way that @{b }OF@{ub } is used
with methods. For instance, the following code shows how to define the
class @{b }d@{ub } to be derived from class @{b }b@{ub }. The class @{b }b@{ub } is then said to be
@{fg shine }inherited@{fg text } by the class @{b }d@{ub }.
OBJECT b
b_data
ENDOBJECT
OBJECT d OF b
extra_d_data
ENDOBJECT
The names @{b }b@{ub } and @{b }d@{ub } have been chosen to be somewhat suggestive, since the
class which is inherited (i.e., @{b }b@{ub }) is known as the @{fg shine }base@{fg text } class, whilst
the inheriting class (i.e., @{b }d@{ub }) is known as the @{fg shine }derived@{fg text } class.
The definition of @{b }d@{ub } is the same as the following definition of @{b }duff@{ub },
except for one major difference: with the above derivation the methods of
@{b }b@{ub } are also inherited by @{b }d@{ub } and they become methods of class @{b }d@{ub }. The
definition of @{b }duff@{ub } relates it in no way to @{b }b@{ub }, except at best accidentally
(since any changes to @{b }b@{ub } do not affect @{b }duff@{ub }, whereas they would affect @{b }d@{ub }).
OBJECT duff
b_data
extra_d_data
ENDOBJECT
One property of this derivation applies to the data records built by
@{b }OBJECT@{ub } as well as the OOP classes. The data records of type @{b }d@{ub } or @{b }duff@{ub } may
be used wherever a data record of type @{b }b@{ub } were required (e.g., the argument
to some procedure), and they are, in fact, indistinguishable from records
of type @{b }b@{ub }. Although, if the definition of @{b }b@{ub } were changed (e.g., by
changing the name of the @{b }b_data@{ub } element) then data records of type @{b }duff@{ub }
would not be usable in this way, but those of type @{b }d@{ub } still would.
Therefore, it is wise to use inheritance to show the relationships between
classes or data of @{b }OBJECT@{ub } types. The following example shows how
procedure @{b }print_b_data@{ub } can validly be called in several ways, given the
definitions of @{b }b@{ub }, @{b }d@{ub } and @{b }duff@{ub } above.
PROC print_b_data(p:PTR TO b)
WriteF('b_data = \\d\\n', p.b_data)
ENDPROC
PROC main()
DEF p_b:PTR TO b, p_d:PTR TO d, p_duff:PTR TO duff
NEW p_b, p_d, p_duff
p_b.b_data:=11
p_d.b_data:=-3
p_duff.b_data:=27
WriteF('Printing p_b: ')
print_b_data(p_b)
WriteF('Printing p_d: ')
print_b_data(p_d)
WriteF('Printing p_duff: ')
print_b_data(p_duff)
ENDPROC
So far, no methods have been defined for @{b }b@{ub }, which means that it is just
an @{b }OBJECT@{ub } type. The procedure @{b }print_b_data@{ub } suggests a useful method of @{b }b@{ub },
which will be called @{b }print@{ub }.
PROC print() OF b
WriteF('b_data = \\d\\n', self.b_data)
ENDPROC
This definition would also define a @{b }print@{ub } method for @{b }d@{ub }, since @{b }d@{ub } is derived
from @{b }b@{ub } and it inherits all the methods of @{b }b@{ub }. However, @{b }duff@{ub } would, of
course, still be just an @{b }OBJECT@{ub } type, although it could have a similar
@{b }print@{ub } method explicitly defined for it. If @{b }b@{ub } has any methods defined for
it (i.e., if it is a class) then data records of type @{b }duff@{ub } cannot be used
as if they were @{i }objects@{ui } of the class @{b }b@{ub }, and it is not safe to try! In
this case, only objects of derived class @{b }d@{ub } can be used in this manner.
(If @{b }b@{ub } is a class then @{b }d@{ub } is a class, due to inheritance.)
PROC main()
DEF p_b:PTR TO b, p_d:PTR TO d, p_duff:PTR TO duff
NEW p_b, p_d, p_duff
p_b.b_data:=11
p_d.b_data:=-3; p_d.extra_d_data:=3
p_duff.b_data:=7; p_duff.extra_d_data:=-7
WriteF('Printing p_b: ')
/* b explicitly has print method */
p_b.print()
WriteF('Printing p_d: ')
/* d inherits print method from b */
p_d.print()
WriteF('No print method for p_duff\\n')
/* Do not try to print p_duff in this way */
/* p_duff.print() */
ENDPROC
Unfortunately, the @{b }print@{ub } method inherited by @{b }d@{ub } only prints the @{b }b_data@{ub }
element (since it is really a method of @{b }b@{ub }, so cannot access the extra data
added in @{b }d@{ub }). However, any inherited method can be overridden by defining
it again, this time for the derived class.
PROC print() OF d
WriteF('extra_d_data = \\d, ', self.extra_d_data)
WriteF('b_data = \\d\\n', self.b_data)
ENDPROC
With this extra definition, the same @{b }main@{ub } procedure above would now print
all the data of @{b }d@{ub }, but only the @{b }b_data@{ub } element of @{b }b@{ub }. This is because the
new definition of @{b }print@{ub } affects only class @{b }d@{ub } (and classes derived from @{b }d@{ub }).
Inherited methods are often overridden just to add extra functionality,
as in the case above where we wanted the extra data to be printed as well
as the data derived from @{b }b@{ub }. For this purpose, the @{b }SUPER@{ub } operator can be
used on a method call to force the base class method to be used, where
normally the derived class method would be used. So, the definition of
the @{b }print@{ub } method for class @{b }d@{ub } could call the @{b }print@{ub } method of class @{b }b@{ub }.
PROC print() OF d
WriteF('extra_d_data = \\d, ', self.extra_d_data)
SUPER self.print()
ENDPROC
Be careful, though, because without the @{b }SUPER@{ub } operator this would involve
a recursive call to the @{b }print@{ub } method of class @{b }d@{ub }, rather than a call to the
base class method.
Just as data records of type @{b }d@{ub } can be used wherever data records of
type @{b }b@{ub } were required, objects of class @{b }d@{ub } can used in place of objects of
class @{b }b@{ub }. The following procedure prints a message and the object data,
using the @{b }print@{ub } method of @{b }b@{ub }. (Of course, only the methods named by class
@{b }b@{ub } can be used in such a procedure, since the pointer @{b }p@{ub } is of type @{b }PTR
TO b@{ub }.)
PROC msg_print(msg, p:PTR TO b)
WriteF('Printing \\s: ', msg)
p.print()
ENDPROC
PROC main()
DEF p_b:PTR TO b, p_d:PTR TO d
NEW p_b, p_d
p_b.b_data:=11
p_d.b_data:=-3; p_d.extra_d_data:=3
msg_print('p_b', p_b)
msg_print('p_d', p_d)
ENDPROC
You can't use @{b }duff@{ub } now, since it is not a class and @{b }b@{ub } is, and @{b }msg_print@{ub }
expects a pointer to class @{b }b@{ub }. The only other objects that can be passed
to @{b }msg_print@{ub } are objects from classes derived from @{b }b@{ub }, and this is why @{b }p_d@{ub }
can be printed using @{b }msg_print@{ub }. If you collect together the code and run
the example you will see that the call to @{b }print@{ub } in @{b }msg_print@{ub } uses the
overridden @{b }print@{ub } method when @{b }msg_print@{ub } is called with @{b }p_d@{ub } as a parameter.
That is, the correct method is called even though the pointer @{b }p@{ub } is not of
type @{b }PTR TO d@{ub }. This is called @{fg shine }polymorphism@{fg text }: different implementations
of @{b }print@{ub } may be called depending on the real, @{fg shine }dynamic@{fg text } type of @{b }p@{ub }. Here's
what should be printed:
Printing p_b: b_data = 11
Printing p_d: extra_d_data = 3, b_data = -3
Inheritance is not limited to a single layer: you can derive other
classes from @{b }b@{ub }, you can derive classes from @{b }d@{ub }, and so on. For instance,
if class @{b }e@{ub } is derived from class @{b }d@{ub } then it would inherit all the data of @{b }d@{ub }
and all the methods of @{b }d@{ub }. This means that @{b }e@{ub } would inherit the richer
version of @{b }print@{ub }, and may even override it yet again. In this case, class
@{b }e@{ub } would have two base classes, @{b }b@{ub } and @{b }d@{ub }, but would be derived directly from
@{b }d@{ub } (and indirectly from @{b }b@{ub }, via @{b }d@{ub }). Class @{b }d@{ub } would therefore be known as the
@{fg shine }super@{fg text } class of @{b }e@{ub }, since @{b }e@{ub } is derived directly from @{b }d@{ub }. (The super class
of @{b }d@{ub } is its only base class, @{b }b@{ub }.) So, the @{b }SUPER@{ub } operator is actually used
to call the methods in the super class. In this example, the @{b }SUPER@{ub }
operator can be used in the methods of @{b }e@{ub } to call methods of @{b }d@{ub }.
The binary tree implementation above (see @{"Binary Trees" Link "Recursion.guide/Binary Trees" }) suggests a good
example for a @{fg shine }class hierarchy@{fg text } (a collection of classes related by
inheritance). A basic tree structure can be encapsulated in a base class
definition, and then specific kinds of tree (with different data at the
nodes) can be derived from this. In fact, the base class @{b }tree@{ub } defined
below is only useful for inheriting, since a tree is pretty useless
without some data attached to the nodes. Since it is very likely that
objects of class @{b }tree@{ub } will never be useful (but objects of classes derived
from @{b }tree@{ub } would be), the @{b }tree@{ub } class is called an @{fg shine }abstract@{fg text } class.
OBJECT tree
left:PTR TO tree, right:PTR TO tree
ENDOBJECT
PROC nodes() OF tree
DEF tot=1
IF self.left THEN tot:=tot+self.left.nodes()
IF self.right THEN tot:=tot+self.right.nodes()
ENDPROC tot
PROC leaves(show=FALSE) OF tree
DEF tot=0
IF self.left
tot:=tot+self.left.leaves(show)
ENDIF
IF self.right
tot:=tot+self.right.leaves(show)
ELSEIF self.left=NIL
IF show THEN self.print_node()
tot++
ENDIF
ENDPROC tot
PROC print_node() OF tree
WriteF('<NULL> ')
ENDPROC
PROC print() OF tree
IF self.left THEN self.left.print()
self.print_node()
IF self.right THEN self.right.print()
ENDPROC
The @{b }nodes@{ub } and @{b }leaves@{ub } methods return the number of nodes and leaves of the
tree, respectively, with the @{b }leaves@{ub } method taking a flag to specify
whether the leaves should also be printed. These methods should never
need overriding in a class derived from @{b }tree@{ub }, and neither should @{b }print@{ub },
which traverses the tree, printing the nodes from left to right. However,
the @{b }print_node@{ub } method probably should be overridden, as is the case in the
integer tree defined below.
OBJECT integer_tree OF tree
int
ENDOBJECT
PROC create(i) OF integer_tree
self.int:=i
ENDPROC
PROC add(i) OF integer_tree
DEF p:PTR TO integer_tree
IF i < self.int
IF self.left
p:=self.left
p.add(i)
ELSE
self.left:=NEW p.create(i)
ENDIF
ELSEIF i > self.int
IF self.right
p:=self.right
p.add(i)
ELSE
self.right:=NEW p.create(i)
ENDIF
ENDIF
ENDPROC
PROC print_node() OF integer_tree
WriteF('\\d ', self.int)
ENDPROC
This is a nice example of polymorphism at work: we can implement a @{b }tree@{ub }
which works with integers simply by defining the appropriate methods. The
@{b }leaves@{ub } method (of the @{b }tree@{ub } class) will then automatically call the
@{b }integer_tree@{ub } version of @{b }print_node@{ub } whenever we pass it an @{b }integer_tree@{ub }
object. The definitions of @{b }tree@{ub } and @{b }integer_tree@{ub } can even be in different
modules (see @{"Data-Hiding in E" Link "Data-Hiding in E" }), and, using these OOP techniques, the
module containing @{b }tree@{ub } would not need to be recompiled even if a class
like @{b }integer_tree@{ub } is added or changed. This shows why OOP is good for
code-reuse and extensibility: with traditional programming techniques we
would have to adapt the binary tree functions to account for integers, and
again for each new datatype.
Notice that the recursive use of the new method @{b }add@{ub } must be called via
an auxiliary pointer, @{b }p@{ub }, of the derived class. This is because the @{b }left@{ub }
and @{b }right@{ub } elements of @{b }tree@{ub } are pointers to @{b }tree@{ub } objects and @{b }add@{ub } is not a
method of @{b }tree@{ub } (the compiler would reject the code as a syntax error if
you tried to directly access @{b }add@{ub } under these circumstances). Of course,
if the @{b }tree@{ub } class had an @{b }add@{ub } method there would not be this problem, but
what would the code be for such a method?
An @{b }add@{ub } method does not really make sense for @{b }tree@{ub }, but if almost all
classes derived from @{b }tree@{ub } are going to need such a method it might be nice
to include it in the @{b }tree@{ub } base class. This is the purpose of @{fg shine }abstract@{fg text }
methods. An @{fg shine }abstract@{fg text } method is one which exists in a base class solely
so that it can be overridden in some derived class. Normally, such
methods have no sensible definition in the base class, so there is a
special keyword, @{b }EMPTY@{ub }, which can be used to define them. For example,
the @{b }add@{ub } method in @{b }tree@{ub } would be defined as below.
PROC add(x) OF tree IS EMPTY
With this definition, the code for the @{b }add@{ub } method for the @{b }integer_tree@{ub }
class could be simplified. (The auxiliary pointer, @{b }p@{ub }, is still needed for
use with @{b }NEW@{ub }, since an expression like @{b }self.left@{ub } is not a pointer
variable.)
PROC add(i) OF integer_tree
DEF p:PTR TO integer_tree
IF i < self.int
IF self.left
self.left.add(i)
ELSE
self.left:=NEW p.create(i)
ENDIF
ELSEIF i > self.int
IF self.right
self.right.add(i)
ELSE
self.right:=NEW p.create(i)
ENDIF
ENDIF
ENDPROC
This, however, is not the best example of an abstract method, since the
@{b }add@{ub } method in every class derived from @{b }tree@{ub } must now take a single @{b }LONG@{ub }
value as an parameter, in order to be compatible. In general, though, a
class representing a tree with node data of type @{fg shine }t@{fg text } would really want an
@{b }add@{ub } method to take a single parameter of type @{fg shine }t@{fg text }. The fact that a @{b }LONG@{ub }
value can represent a pointer to any type is helpful, here. This means
that the definition of @{b }add@{ub } may not be so limiting, after all.
The @{b }print_node@{ub } method is much more obviously suited to being an
abstract method. The above definition prints something silly, because at
that point we didn't know about abstract methods and we needed the method
to be defined in the base class. A much better definition would make
@{b }print_node@{ub } abstract.
PROC print_node() OF tree IS EMPTY
It is quite safe to call these abstract methods, even for @{b }tree@{ub } class
objects. If a method is still abstract in any class (i.e., it has not
been overridden), then calling it on objects of that class has the same
effect as calling a function which just returns zero (i.e., it does very
little!).
The @{b }integer_tree@{ub } class could be used like this:
PROC main()
DEF t:PTR TO integer_tree
NEW t.create(10)
t.add(-10)
t.add(3)
t.add(5)
t.add(-1)
t.add(1)
WriteF('t has \\d nodes, with \\d leaves: ',
t.nodes(), t.leaves())
t.leaves(TRUE)
WriteF('\\n')
WriteF('Contents of t: ')
t.print()
WriteF('\\n')
END t
ENDPROC
@ENDNODE
@NODE "Data-Hiding in E" "Data-Hiding in E"
@Prev "Inheritance in E"
@Toc "main"
Data-Hiding in E
================
@{fg shine }Data-hiding@{fg text } is accomplished in E at the module level. This means,
effectively, that it is wise to define classes in separate modules (or at
least only closely related classes together in a module), taking care to
@{b }EXPORT@{ub } only the definitions that you need to. You can also use the
@{b }PRIVATE@{ub } keyword in the definition of any @{b }OBJECT@{ub } to hide all the
elements following it from code which uses the module (although this does
not affect the code within the module). The @{b }PUBLIC@{ub } keyword can be used in
a similar way to make the elements which follow visible (i.e., accessible)
again, as they are by default. For instance, the following @{b }OBJECT@{ub }
definition makes @{b }x@{ub }, @{b }y@{ub }, @{b }a@{ub } and @{b }b@{ub } private (so only visible to the code within
the same module), and @{b }p@{ub }, @{b }q@{ub } and @{b }r@{ub } public (so visible to code external to
the module, too).
OBJECT rec
p:INT
PRIVATE
x:INT
y
PUBLIC
q
r:PTR TO LONG
PRIVATE
a:PTR TO LONG, b
ENDOBJECT
For the set class you would probably want to make all the data private
and all the methods public. In this way you force programs which use this
module to use the supplied interface, rather than fiddling with the set
data structures themselves. The following example is the complete code
for a simple, inefficient set class, and can be compiled to a module.
OPT MODULE -> Define class 'set' in a module
OPT EXPORT -> Export everything
/* The data for the class */
OBJECT set PRIVATE -> Make all the data private
elements:PTR TO LONG
maxsize, size
ENDOBJECT
/* Creation constructor */
/* Minimum size of 1, maximum 100000, default 100 */
PROC create(sz=100) OF set
DEF p:PTR TO LONG
self.maxsize:=IF (sz>0) AND (sz<100000) THEN sz ELSE 100 -> Check size
self.elements:=NEW p[self.maxsize]
ENDPROC
/* Copy constructor */
PROC copy(oldset:PTR TO set) OF set
DEF i
self.create(oldset.maxsize) -> Call create method!
FOR i:=0 TO oldset.size-1 -> Copy elements
self.elements[i]:=oldset.elements[i]
ENDFOR
self.size:=oldset.size
ENDPROC
/* Destructor */
PROC end() OF set
DEF p:PTR TO LONG
IF self.maxsize<>0 -> Check that it was allocated
p:=self.elements
END p[self.maxsize]
ENDIF
ENDPROC
/* Add an element */
PROC add(x) OF set
IF self.member(x)=FALSE -> Is it new? (Call member method!)
IF self.size=self.maxsize
Raise("full") -> The set is already full
ELSE
self.elements[self.size]:=x
self.size:=self.size+1
ENDIF
ENDIF
ENDPROC
/* Test for membership */
PROC member(x) OF set
DEF i
FOR i:=0 TO self.size-1
IF self.elements[i]=x THEN RETURN TRUE
ENDFOR
ENDPROC FALSE
/* Test for emptiness */
PROC empty() OF set IS self.size=0
/* Union (add) another set */
PROC union(other:PTR TO set) OF set
DEF i
FOR i:=0 TO other.size-1
self.add(other.elements[i]) -> Call add method!
ENDFOR
ENDPROC
/* Print out the contents */
PROC print() OF set
DEF i
WriteF('{ ')
FOR i:=0 TO self.size-1
WriteF('\\d ', self.elements[i])
ENDFOR
WriteF('}')
ENDPROC
This class can be used in another module or program, as below:
MODULE '*set'
PROC main() HANDLE
DEF s=NIL:PTR TO set
NEW s.create(20)
s.add(1)
s.add(-13)
s.add(91)
s.add(42)
s.add(-76)
IF s.member(1) THEN WriteF('1 is a member\\n')
IF s.member(11) THEN WriteF('11 is a member\\n')
WriteF('s = ')
s.print()
WriteF('\\n')
EXCEPT DO
END s
SELECT exception
CASE "NEW"
WriteF('Out of memory\\n')
CASE "full"
WriteF('Set is full\\n')
ENDSELECT
ENDPROC
@ENDNODE