930 lines
38 KiB
Plaintext
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
|
|
|