PureBasic and the Object-Oriented Programming

Second Implementation

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:

  1. an Interface describing the required methods,
  2. a structure describing the pointers of the corresponding functions,
  3. 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:

Rect.Rectangle

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:

Rect\Draw()
Rect\Erase()

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:

Free_Rect(Rect2)


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.


Contents

[1-2-3-4-5-6-7-8-9]

Top of the page