|
|
|
PureBasic
and the Object-Oriented Programming
In our first implementation, object concepts was adapted in a way more
or less extend.
Now, it's time to improve this first implementation thanks to the use
of the Interface instruction.
Notion
of Object interface:
The purpose of encapsulation is first to make visible, to the user, part
of an object contents.
The visible part of the contents is called interface, the hidden
part is called implementation.
Thus, the object interface is the only input-output access that the user
has to act on an object.
It is the responsibility that I'm going to give in our use of the Interface
instruction.
The Interface instruction allows to group under the same name, all
or part of the methods from an object that the user will have the right
access.
Object
Instanciation and Object Constructor
Play with an interface involve three steps:
- an Interface describing the required methods,
- a structure describing the pointers of the corresponding functions,
- a table of the methods: a structured variable initialized with the
required functions adresses.
Step 1, consists in specify the object Interface, which is not a difficulty.
Just name methods.
Steps 2 and 3 are linked. In our object approach, we already have the
adapted Structure: it is the one who describes the Class of an object.
Moreover, the Interface and the Class of an object are close: both contain
functions pointers.
Simply, the Interface instruction doesn't contain the attributes of
the Class but only all or any of the methods of the Class.
Thus it is completely possible to use the Class of an object to initialize
the Interface. This step is the most natural. Let us remind that the
interface is the visible part of the Class of an object, it is natural
that the Class determines the Interface.
To see how to process, let me resume the example of the Rectangle2
class which provided a Draw()
and Erase() methods.
The corresponding Class is the following one
Structure Rectangle2
*Draw
*Erase
x1.l
x2.l
y1.l
y2.l
EndStructure
Procedure Draw_Rectangle(*this.Rectangle2)
EndProcedure
Procedure Erase_Rectangle(*this.Rectangle2)
EndProcedure
|
The associated Interface is the following:
Interface Rectangle
Draw()
Erase()
EndInterface
|
Because the user may handle an object only through the interface, the
object must be created directly from the interface Rectangle rather
than class Rectangle2.
The object will thus be create by writing:
instead of Rect.Rectangle2 .
However, you should not forget to connect the Interface to the Class.
For this, it is necessary to initialize the Rect object during the statement.
Correction made, the good instruction to assign the object is the following
one:
Rect.Rectangle = New_Rect(0,
10, 0, 20)
|
New_Rect() is a function which
performs the initialization operation.
What it's already known about it, it's that the returned value is the
memory address containing the functions addresses to be processed by
the interface.
Here is the body of the New_Rect()
function:
Procedure New_Rect(x1.l,
x2.l, y1.l, y2.l)
*Rect.Rectangle2 = AllocateMemory(SizeOf(Rectangle2))
*Rect \Draw = @Draw_Rectangle()
*Rect \Erase = @Erase_Rectangle(
*Rect\x1 = x1
*Rect\x2 = x2
*Rect\y1 = y1
*Rect\y2 = y2
ProcedureReturn *Rect
EndProcedure
|
This function assigns a memory area of size the object Class size.
Then it initializes the methods and the attributes of the object.
It ends by retrieving the memory area address.
Because the addresses of Draw()
and Erase() functions are first
positioned in this memory area, the interface is effectively initialized.
To access to the Rect object methods, just write:
Then, the demonstration is made that:
- Class Rectangle2 allows the object Interface initialization.
- Rect, declared thanks the interface, is an object of the Class Rectangle2
which can use the Draw() and the
Erase() methods.
Thus the Interface instruction and the New_Rect()
function perform the instanciation of an Rect object from the Class
Rectangle2.
The New_Rect() function is called
as the object Constructor of the Class Rectangle2.
|
All the Methods implementations (Procedure/EndProcedure
blocks) must contain, as first argument, the *this pointer of the
object. On the other hand, the *this argument mustn't appear at
the Interface level. In fact, as this instruction allows to write
Rect\Draw(), it know that the Draw() method involves the Rect object:
no ambiguity! Everything happen as if the object Rect was "aware"
of its state. |
|
The Constructor could receive, as parameters,
the whole functions addresses which implement the methods. It is
not the case here because we know the implemented methods: the ones
from the class. On the other hand the initial state wished by the
user is unkown. Thus, the Constructor may contain parameters for
the attributes initialization.
It is the case here: the entries required by New_Rect() are the
two coordinates (x1, y1) and (x2, y2) of the diametrically opposite
points of the rectangle. |
Object
Initialization
After the required memory area for an object assigned, the Constructor
initializes the various members of the object (methods and attributes).
This operation will be isolated in a specific procedure called by the
Constructor.
By the way the distinction is made between the memory allocation and
the object initialization. This is very useful to achieve afterward
the concept of inheritance, because a single memory allocation is sufficient,
but several initializations are required.
In addition, the initialization of the methods and of the attributes
are separated too. It's because the methods implementation depends on
the class, while the attributes initialization depends on the object
itself (see previous remark ).
In our example, the two initialization procedures will be implemented
as:
Procedure Init_Mthds_Rect(*Rect.Rectangle2)
*Rect\Draw = @Draw_Rectangle()
*Rect\Erase = @Erase_Rectangle()
EndProcedure
Procedure Init_Mbers_Rect(*Rect.Rectangle2,
x1.l, x2.l, y1.l, y2.l)
*Rect\x1 = x1
*Rect\x2 = x2
*Rect\y1 = y1
*Rect\y2 = y2
EndProcedure
|
and the Constructor became:
Procedure New_Rect(x1.l,
x2.l, y1.l, y2.l)
*Rect = AllocateMemory(SizeOf(Rectangle2))
Init_Mthds_Rect(*Rect)
Init_Mbers_Rect(*Rect, x1,
x2, y1, y2)
ProcedureReturn *Rect
EndProcedure
|
Object
Destructor
Always associated to an object Constructor is the object Destructor.
During the construction of an object, a memory area is assigned to store
the method and attribute definitions.
When an object is useless, it must be destroy to free the computer memory.
This process is performed by using a specific function called Destructor
of the object.
In our example of Rectangle2 objects, the Destructor is:
Procedure Free_Rect(*Rect)
FreeMemory(*Rect)
EndProcedure
|
and will be used as:
|
The Destructor could be seen as a method
of the object. But to avoid weighing down the object and to preserve
homogeneity with the Constructor, I have chosen to see it as a function
of the Class. |
|
To delete an object by its Destructor means
releasing the memory area which containing the object information
(methods to use and attributes states) but not deleting the object
infrastructure.
So, in our example, having made:
Free_Rect(Rect2)
Rect2 can be reuse without specify its type again:
Rect2 = New_Rect(0, 10, 0, 20)
Rect2\Dessiner()
Definitely, when an object instanciation is realized, as hereafter:
Rect2. Rectangle
the life cycle of object Rect2 follows the same rules as those of
the variables because Rect2 is at first a variable: it is a structured
variable continuing the functions pointers of the object methods.
(See also the reminder which follows) |
|
Small reminder: the life cycle of a variable is linked to the life
cycle of the program part where the variable is declared:
- If the variable is declared inside a procedure, its life cycle
will be linked to that of the procedure, which is equal to the
function time of use.
- If the variable is declared outside any procedure, in the program's
main core, its life cycle is linked to that of the program.
|
Memory Allocations
In every new instanciation, the Constructor has to allocate dynamically
a memory area according the size of the information describing the object.
For that purpose, the Constructor should use the AllocateMemory() command
associated with FreeMemory() command for the Destructor.
But there is other candidate to achieve such dynamic memory allocation.
Under Windows OS, API can be directly used for example.
Standard PureBasic library provides linked lists, which also a good
candidate to allocate dynamically some memory.
Encapsulation
Let us imagine now that I want to give to the user only access to the
Draw() method of the Class Rectangle.
I shall begin by defining the wished interface:
Interface Rectangle
Draw()
EndInterface
|
To instanciate a new object I write:
Rect.Rectangle = New_Rect()
|
with,
Procedure Init_Mthds_Rect(*Rect.Rectangle2)
*Rect\Draw = @Draw_Rectangle()
*Rect\Erase = @Erase_Rectangle()
EndProcedure
Procedure Init_Mbers_Rect(*Rect.Rectangle2,
x1.l, x2.l, y1.l, y2.l)
*Rect\x1 = x1
*Rect\x2 = x2
*Rect\y1 = y1
*Rect\y2 = y2
EndProcedure
Procedure New_Rect(x1.l,
x2.l, y1.l, y2.l)
*Rect = AllocateMemory(SizeOf(Rectangle2))
Init_Mthds_Rect(*Rect)
Init_Mbers_Rect(*Rect, x1,
x2, y1, y2)
ProcedureReturn *Rect
EndProcedure
|
It is similar as previous because the first function address is the
Draw() method one.
Now, imagine that I want to give to the user only the access to the
Erase() method. I shall begin
by defining the new interface:
Interface Rectangle
Erase()
EndInterface
|
Nevertheless to instanciate the new object I cann't use the New_Rect()
Constructor above.
In the opposite case, the result would be identical to the previous
case. By writting Rect\Erase() the Draw() method is called.
Thus, a new Constructor is needed able to return the correct function
address.
Hereafter one of them is given:
Procedure Init_Mthds_Rect2(*Rect.Rectangle2)
*Rect\Draw = @Erase_Rectangle()
*Rect\Erase = @Draw_Rectangle()
EndProcedure
Procedure Init_Mbers_Rect(*Rect.Rectangle2,
x1.l, x2.l, y1.l, y2.l)
*Rect\x1 = x1
*Rect\x2 = x2
*Rect\y1 = y1
*Rect\y2 = y2
EndProcedure
Procedure New_Rect2(x1.l,
x2.l, y1.l, y2.l)
*Rect = AllocateMemory(SizeOf(Rectangle2))
Init_Mthds_Rect2(*Rect)
Init_Mbers_Rect(*Rect, x1,
x2, y1, y2)
ProcedureReturn *Rect
EndProcedure
|
You notice that the functions addresses were just inverted at the initialization
level.
Certainly, it is not very elegant to allocate Draw field of the Rectangle2
Structure with an other function address.
If it allows to preserve the same Structure, that of the Class, it also
underlines a matter:
The function pointer names are less interesting than their values!
To erase this non-problem, just rename the pointers of the Class as following:
Structure Rectangle2
*Method1
*Method2
x1.l
x2.l
y1.l
y2.l
EndStructure
|
In fact, the Interface and the Constructor are in charge to give a sense
to these pointers:
- by giving them a name (task of the interface)
- by allocating them the adequate functions addresses (task of the constructor)
|
In spite of this arrangement concerning the
function pointer names, it remains more practical to keep an explicit
name if hiding methods is not considered (what is the most common
situation). That allows to modify a Mother Class without retouching
the pointers numbering of the Child Classes. |
Inheritance
As for the first inheritance concept implementation, let takes advantage
that the Structure and Interface instructions have to be extend thanks
to the keyword Extends.
So, to pass from the Rectangle1 Class which has a single Draw() method
to
Interface
|
Interface
Rect1
Draw()
EndInterface
|
Class |
Structure Rectangle1
*Method1
x1.l
x2.l
y1.l
y2.l
EndStructure
Procedure Draw_Rectangle(*this.Rectangle1)
EndProcedure
Procedure
Init_Mthds_Rect1(*Rect.Rectangle1)
*Rect\Method1 = @Draw_Rectangle()
EndProcedure
|
Constructor |
Procedure Init_Mbers_Rect1(*Rect.Rectangle1,
x1.l, x2.l, y1.l, y2.l)
*Rect\x1 = x1
*Rect\x2 = x2
*Rect\y1 = y1
*Rect\y2 = y2
EndProcedure
Procedure New_Rect1(x1.l,
x2.l, y1.l, y2.l)
*Rect = AllocateMemory(SizeOf(Rectangle1))
Init_Mthds_Rect1(*Rect)
Init_Mbers_Rect1(*Rect, x1,
x2, y1, y2)
ProcedureReturn *Rect
EndProcedure
|
a Rectangle2 Class, which have two methods: Draw()
and Erase(), write:
Interface
|
Interface
Rect2 Extends Rect1
Erase()
EndInterface
|
Class |
Structure Rectangle2
Extends Rectangle1
*Method2
EndStructure
Procedure Erase_Rectangle(*this.Rectangle1)
EndProcedure
Procedure
Init_Mthds_Rect2(*Rect.Rectangle2)
Init_Mthds_Rect1(*Rect)
*Rect\Method2 = @Erase_Rectangle()
EndProcedure
|
Constructor |
Procedure Init_Mbers_Rect2(*Rect.Rectangle2,
x1.l, x2.l, y1.l, y2.l)
Init_Mbers_Rect1(*Rect, x1,
x2, y1, y2)
EndProcedure
Procedure New_Rect2(x1.l,
x2.l, y1.l, y2.l)
*Rect = AllocateMemory(SizeOf(Rectangle2))
Init_Mthds_Rect2(*Rect)
Init_Mbers_Rect2(*Rect, x1,
x2, y1, y2)
ProcedureReturn *Rect
EndProcedur
|
Carrying out an inheritance consists in extending Interface and Class
structure, but also in adapting the method and the attribut initializations
Both procedures Init_Mthds_Rect2()
and Init_Mbers_Rect2() call respectively
the initialization of the methods and to the initialization of the attributes
of the Class Rectangle1 ( Init_Mthds_Rect1()
and Init_Mbers_Rect1()
) rather than the Constructor New_Rect1().
Those because, a Child Class object doesn't need to instantiate a Mother
Class object but just to inherit methods and attributes.
On the other side, verification is made that the Child Class benefits
immediately of any changes made at the Mother Class level (adding a
method or a variable).
Is the inheritance currently correct? No, because it doesn't allow
the object of the Child Class (Rectangle2) to use the new Erase()
method.
The reason takes place on that function pointer *Methode2 doesn't follow
the *Methode1 one.
Let me show you the explicit form of Class Rectangle2 Structure:
Structure Rectangle2
*Method1
x1.l
x2.l
y1.l
y2.l
*Method2.l
EndStructure
|
instead of having the Structure below, authorizing a correct initialization
of the interface:
Structure Rectangle2
*Method1
*Method2
x1.l
x2.l
y1.l
y2.l
EndStructure
|
Remind that a correct interface initialization needs functions addresses,
which follow each other ().
To resolve this problem, just group all the methods into a specific
structure !
The Class structure needs just to have a pointer on this new structure
as the following example shows:
Interface
|
Interface
Rect1
Draw()
EndInterface
|
Class |
Structure Rectangle1
*Methods
x1.l
x2.l
y1.l
y2.l
EndStructure
Procedure Draw_Rectangle(*this.Rectangle1)
EndProcedure
Structure Methd_Rect1
*Method1
EndStructure
Procedure
Init_Mthds_Rect1(*Mthds.Mthds_Rect1)
*Mthd_Rect1\Method1 = @Draw_Rectangle()
EndProcedure
Mthds_Rect1. Mthds_Rect1
Init_Mthds_Rect1(@Mthds_Rect1)
|
Constructor |
Procedure Init_Mbers_Rect1(*Rect.Rectangle1,
x1.l, x2.l, y1.l, y2.l)
*Rect\x1 = x1
*Rect\x2 = x2
*Rect\y1 = y1
*Rect\y2 = y2
EndProcedure
Procedure New_Rect1(x1.l,
x2.l, y1.l, y2.l)
Shared Mthds_Rect1
*Rect.Rectangle1 = AllocateMemory(SizeOf(Rectangle1))
*Rect\Methods = @Mthds_Rect1
Init_Mbers_Rect1(*Rect, x1,
x2, y1, y3)
ProcedureReturn *Rect
EndProcedur
|
The Methd_Rect1 structure describes all the functions pointers of the
Class methods.
Follows the Methd_Rect1 variable statement and its initialization thanks
to Init_Mthds_Rect1().
Methd_Rect1 is called the table of the methods of
the call. This variable contains the whole methods adresses.
Here is reached the final Class methods description.
|
The following expression
Mthds_Rect1.Mthds_Rect1
Init_Mthds_Rect1(@Mthds_Rect1)
can be condensate into
Init_Mthds_Rect1(@Mthds_Rect1.Mthds_Rect1)
|
The Rectangle1 structure, contains now a pointer "*Methods",
initialized through the constructor by giving to it the Methd_Rect1 variable
address.
The inheritance can be now performed correctly, because by extending
the Methd_Rect1 Structure in a Methd_Rect2 new one, the addresses of
functions are going to follow each other:
Interface
|
Interface
Rect2 Extends Rect1
Erase()
EndInterface
|
Class |
Structure Rectangle2
Extends Rectangle1
EndStructure
Procedure Erase_Rectangle(*this.Rectangle2)
EndProcedure
Structure Methd_Rect2
Extends Methd_Rect1
*Method2
EndStructure
Procedure
Init_Mthds_Rect2(*Mthds.Mthds_Rect2)
Init_Mthds_Rect1(*Mthds)
*Mthd_Rect2\Method2 = @Erase_Rectangle()
EndProcedure
Mthds_Rect2. Mthds_Rect2
Init_Mthds_Rect2(@Mthds_Rect2)
|
Constructor |
Procedure Init_Mbers_Rect2(*Rect.Rectangle2
, x1.l, x2.l, y1.l, y2.l)
Init_Mbers_Rect1(*Rect, x1,
x2, y1, y2)
EndProcedure
Procedure New_Rect2(x1.l,
x2.l, y1.l, y2.l)
Shared Mthds_Rect2
*Rect.Rectangle2 = AllocateMemory(SizeOf(Rectangle2))
*Rect\Methods = @Mthds_Rect2
Init_Mbers_Rect2(*Rect, x1,
x2, y1, y2)
ProcedureReturn *Rect
EndProcedure
|
In this example, the Rectangle2 Structure is empty and it isn't a problem.
Two reasons in it:
- At first the *Methodes pointer needs to exist only once and this in
the Mother Class.
- Then, no supplementary attributes have been added to it.
|
there is three advantages to have a
methods initialization routine external to the Constructor and
a table of methods available in a single variable:
- The table of the methods are initialized once and not at each
object instanciation,
- The objects have no more than one pointer towards the methods:
it is a substantial gain of memory,
- All the objects referred towards the same table of methods,
which guaranteed an identical behavior for all the objects of
the same Class.
|
Get()
and Set() object methods
By Interface, it is possible to use only methods of an object.
The interface encapsulates completely the object attributes.
To act on attributes, either for examine or to modify them, it is necessary
to give specific methods to the user.
The methods allowing to examine object attributes are called Get()
methods.
The methods allowing to modify object attributes are called Set()
methods.
In our example of Rectangle1 Class, if I want to examine the value
of the attribute var2, I should create the following Get() method:
Procedure Get_var2(*this.Rectangle1)
ProcedureReturn *this\var2
EndProcedure
|
Also, to modify the value of the attribute var2, I should write the
following Set() method:
Procedure Set_var2(*this.Rectangle1,
value)
*this\var2 = value
EndProcedure
|
Because Get() and Set() methods exist only to allow the user to modify
all or any of the object attributes, they are necessarily present in the
Interface.
|
See the Appendix
of the tutorial for possible optimization.
|
[1-2-3-4-5-6-7-8-9]
Top of the page
|
|
|