Why OOP in PureBasic?
Since many object-oriented languages already exist, trying to achive OOP in PureBasic — which is a procedural language — might come as a surprise.
The fact that some languages are defined «Object-Oriented», while others are not, is merely indicating the presence of specific keywords which facilitate writing code according to the OOP paradigm. That is to say: object-oriented languages introduce richer semantics; but compilation-wise they are not introducing any new aspects compared to a non-OO language, they’re just adding a layer over the latter.
Therefore, we can fully implement the OO paradigm in PureBasic, but we have to pay a price in terms of accuracy — both in development and in notation. And here we face the immediate advantages of (natively) object-oriented languages.
Nevertheless, besides the possibility of programming in PureBasic according to the OO paradigm, implementing object-oriented concepts in PureBasic offers us the interesting chance to reveal some of the underlying mechanisms of OO languages keywords.
This paper presents a programming technique that allows large projects to benefit from object oriented design. It is not intended as a course on OOP, and it assumes that the reader has good knowledge of PureBasic language.
Object Concepts
The Object-Oriented Programming paradigm introduces concepts like: objects, inheritance and polymorphism. Before jumping into how these concepts can be implemented in PureBasic, we must first define them.
The Notion of Object
An object has an internal state:
-
The state is represented by the value of each inner component at a given moment,
-
A component is either a value or another object.
It uses and provides services:
-
It interacts with the outside through functions called «methods».
It is unique (notion of identity).
An object can be seen at two levels:
-
By the services which it provides: external view (specification). This is the User side,
-
By the way these services are implemented within it: internal view (implementation). This is the Developer side.
From the developer’s point of view, an object is a contiguous memory area containing information: variables called attributes and functions called methods.
The fact that we refer to an object’s functions as «its methods» is because they belong to it and allow manipulation of its attributes.
The Notion of Class
This is an extension of the concept of «type» found in PureBasic.
In a given context, multiple objects can have the same structure and behavior. They can therefore be grouped together in the same «Class».
From a developer’s point of view, a Class defines the type of contents that will be held by objects belonging to it: the nature of their attributes (type of each variable) and their methods (names, implementation).
Just as integer is the type of a variable, the type of an object is its Class.
The Notion of Instance
An instance is an object defined from a Class.
This process is called «instanciation».
It corresponds to the assignement of variables in PureBasic.
Normally, an object is initialized at the time of its instanciation.
Encapsulation
In theory, the manipulation of an object’s attributes should be possible only through its methods. This technique, which allows making visible to the user only a part of the object, is called «encapsulation».
The advantage of encapsulation is that it guarantees the integrity of attributes. Indeed, the developer is the only one who, through the methods provided to the user, manages the modifications allowed on an object.
At our level, this is the least that should be retained of the encapsulation concept.
Inheritance
Inheritance allows defining new Classes by using already existing ones.
From the developer’s point of view, it means being able to add/modify attributes and methods to/of an existing Class in order to define a new Class.
There are two kinds of inheritances:
-
Simple inheritance: the new Class is defined from a single existing Class.
-
Multiple inheritance: the new Class is defined from several existing Classes.
Multiple inheritance is complex to implement, and it will not be covered here. Thus, this papers deals only with simple inheritance.
Terminology:
-
The Class which inherits from another Class, is usually called Child Class.
-
The Class which gives its inheritance to a Child Class is usually called Parent Class.
Overloading
A method is overloaded if it carries out different actions according to the nature of the target objects.
Let us take an example:
The following objects: circle, rectangle and triangle are all geometrical shapes.
We can define for these objects the same Class with the given name: Shape
.
Thus, these objects are all instances of the Shape
Class.
If we want to display the objects, the Shape
Class needs to have a Draw
method.
So endowed, every object has a Draw
method to display itself. Now, this method could not possibly be the same for each object, since we want to display a circle, in one case, a rectangle, in another, etc.
Objects of the same Class employ the same Draw
method, but the object’s nature (Circle, Rectangle, or Triangle) dictates the actual implementation of the method.
The Draw
method is overloaded: for the user, displaying a circle or a rectangle is achieved in the same way.
From the developer’s point of view, the methods implementations needs to be different.
Instead of overloaded methods, we can also speak of polymorphic methods (having several forms).
The Notion of Abstract Class
As we’ve seen, a Class includes the definition of both attributes and methods of an object. Let us suppose that we can’t provide the implementation of one of the Class methods. This method is just a name without code. We’re then speaking of an «abstract method». A Class containing at least one abstract method qualifies as an «abstract Class».
You might wonder why an abstract class should exist at all, since objects of such a Class can’t be created. Abstract Classes allow defining Object Classes, which are considered — by opposition — as being «concrete». The transition from the former to the latter occurs through inheritance, where the concrete Class takes care of providing the missing implementations to the abstract methods inherited.
Thus, abstract Classes are a kind of interface, because they describe the generic specification of all the Classes which inherit from them.
First Implementation
In this section, I shall demonstrate how the aforementioned object concepts can be implemented in PureBasic. This implementation doesn’t refer to what is programmed in object-oriented languages. Furthermore, this implementations is meant be improved upon, or adapted according to needs.
I’ll be presenting here one of these implementations, with all its pros and cons.
Concrete Class and Abstract Class
As seen, a Class defines the contents of an object:
-
Its attributes (each variable type)
-
Its methods (Names, implementation)
For example, if I want to represent Rectangle objects and display them on screen, I shall define a Rectangle
Class including a Draw()
method.
The Rectangle
Class could have the following construction:
Structure Rectangle
*Draw
x1.l
x2.l
y1.l
y2.l
EndStructure
Procedure Draw_Rectangle(*this.Rectangle)
; [ ...some code... ]
EndProcedure
where x1
, x2
, y1
and y2
are four attributes (the coordinates of the diametrically opposed points of the rectangle) and *Draw
is a pointer referencing the drawing function which displays Rectangles.
Here *Draw
is a function pointer used to contain the address of the desired function: @Draw_Rectangle()
.
Functions referenced in this manner can be invoked by using CallFunctionFast()
.
Thus, the proposed Structure
is completely adapted to the notion of Class:
* the structure stores the definition of the object’s attributes: here x1
, x2
, y1
and y2
are Long variables.
* the structure stores the definition of the object’s method: here the Draw()
function, through to a function pointer.
When a similar Class definition is followed by the implementations of its methods (in our example, Draw_Rectangle()
’s Procedure : EndProcedure
block statement), it becomes a concrete Class. Otherwise, it will be an abstract Class.
|
Instanciation
Now, to create an object called Rect1
from the Rectangle
Class, write:
Rect1.Rectangle
To initialize it, simple write:
Rect1\Draw = @Draw_Rectangle()
Rect1\x1 = 0
Rect1\x2 = 10
Rect1\y1 = 0
Rect1\y2 = 20
Next, to draw the Rect1
object, use:
CallFunctionFast(Rect1\Draw, @Rect1)
Encapsulation
In this implementation, encapsulation doesn’t exist, simply because there is no way to hide the attributes or the methods of such an object.
By writing Rect1\x1
, the user can access the x1
attribute of the object. This is the way we used to initialize the object.
The next implementation (Second Implementation section) will show how to fix this.
Although significant, this feature is not essential in implementing OOP.
Inheritance
Now I want to create a new Class with the capability to Erase rectangles from the screen.
I can implement this new Rectangle2
Class by using the existing Rectangle
Class and by providing it with a new method called Erase()
.
A Class being a Structure
, let’s take advantage of the extension property of structures. So, the new Class Rectangle2
could be:
Structure Rectangle2 Extends Rectangle
*Erase
EndStructure
Procedure Erase_Rectangle(*this.Rectangle2)
; [ ...some code... ]
EndProcedure
The Class Rectangle2
includes the members of the previous Rectangle
Class as well as those of the new Erase()
method.
To instanciate an object from this new Class write:
Rect2.Rectangle2
Rect2\Draw = @Draw_Rectangle()
Rect2\Erase = @Erase_Rectangle()
Rect2\x1 = 0
Rect2\x2 = 10
Rect2\y1 = 0
Rect2\y2 = 20
To use Rect2
’s Draw()
and Erase()
methods, I shall proceed the same way as before: through CallFunctionFast()
.
This demonstrates that the Rectangle2
Class inherited the properties of the Rectangle
Class.
Inheritance is a category of polymorphism. The object |
Overloading
During initialization of an object, its function pointers are initialized by assigning to them the addresses of the methods suiting the object.
So, given an object Rect
from the Rectangle
Class, by writing:
Rect1\Draw = @Draw_Rectangle()
I can invoke its Draw()
method the following way:
CallFunctionFast(Rect1\Draw, @Rect1)
Now, imagine that it was possible to implement another method for displaying a rectangle (by using a different algorithm from the one in the first method).
Let us call this implementation as Draw_Rectangle2()
:
Procedure Draw_Rectangle2(*this.Rectangle)
; [ ...some code... ]
EndProcedure
It’s possible to initialize our object Rect1
with this new method effortlessly:
Rect1\Draw = @Draw_Rectangle2()
To use the method, write again:
CallFunctionFast(Rect1\Draw, @Rect1)
We can see that with both the former method (ie: Draw_Rectangle()
) as well as the latter (ie: Draw_Rectangle2()
) the use of the Rect1
method is strictly identical.
It isn’t possible to distinguish by the single line of code CallFunctionFast(Rect1\Draw, @Rect1)
which one of the Draw()
methods the Rect1
object is really using.
To know this, it is necessary to go back to the object initialization.
The notion of function pointer allows overloading the Draw()
method.
One limitation: the use of the CallFunctionFast()
instruction implies paying attention to the number of parameters passed.
Interface instruction
Interface <Name1> [Extends <Name2>]
[Procedure1]
[Procedure2]
...
EndInterface
The PureBasic Interface
instruction allows grouping under the same Name (<Name1>
in the above box) various procedures.
Interface My_Object
Procedure1(x1.l, y1.l)
Procedure2(x2.l, y2.l)
EndInterface
It’s now sufficient to declare an element as being of the My_Object
type in order to access all the procedures that it contains. The declaration is carried out in the same manner as with Structure
types:
Object.My_Object
As a result, we can now acess the Object
’s functions directly:
Object\Procedure1(10, 20)
Object\Procedure2(30, 40)
Thanks to the Interface
instruction, procedures can be called via a very practical and pleasant notation.
By writing Object\Procedure1(10, 20)
, the Procedure1()
from Object
is called.
This notation is typical of the Object-oriented Programming paradigm.
Initialization
Any typed variable declaration is normally followed by initialization. The same applies when declaring an element whose type is an Interface
.
Unexpectedly, naming the Interface : EndInterface
block with the name of a desired Procedure isn’t enough to make it refer to its implementation — i.e., to reference the Procedure : EndProcedure
block of the desired procedure.
In fact, we can rename procedures inside an Interface : EndInterface
block: we can give any name we like to the procedures that we are going to use.
Then, how are we going to connect this new name with the desired real procedure?
As with overloaded methods, the solution is in function addresses.
We must see the names inside the Interface : EndInterface
block as function pointers to the desired function — i.e., as pointer holding function addresses.
However, to initialize the function pointers of an Interface
typed element, the approach is different from that of a Structure
typed element.
Indeed, it isn’t possible to initialize individually each field defined by an Interface
, because, you must remember, that writing Object\Procedure1()
means calling that procedure.
Initialization occurs indirectly, by giving to the element the address of a pre-initialized variable storing functions pointers.
This kind of variable is called a method table.
Example:
Let us carry on with the Interface My_Object
.
Consider the following Structure
describing the function pointers:
Structure My_Methods
*Procedure1
*Procedure2
EndStructure
and its associated method table:
Methods.My_Methods
Methods\Procedure1 = @My_Procedure1()
Methods\Procedure2 = @My_Procedure2()
where My_Procedure1()
and My_Procedure2()
are the desired procedure implementations.
Then, initialization of Object
(of the My_Object
type, an Interface
) looks like this:
Object.My_Object = @Methods
Next, by writing
Object\Procedure2(30, 40)
the Object
’s Procedure2()
function is called — i.e., My_Procedure2()
.
When declaring elements of an |
To summarize, using an Interface
involves:
-
an
Interface
describing the required procedures to use, -
a
Structure
describing the function pointers, -
a method table: a structured variable initialized with the required function adresses.
And its benefits are:
-
an object-oriented notation
-
an easy way to rename procedures
Second Implementation
In our first implementation, object concepts were adapted in a more or less extended way.
Now, it’s time to improve this first implementation thanks to the use of the Interface
instruction.
Notion of Object Interface
The main purpose of encapsulation is to make visible, to the user, only part of an object contents. The visible part of an object’s contents is called its interface, the hidden part is called it implementation.
Therefore, an object’s interface is the only input/output access available to the user for interacting with it.
This is the aim that we are going to achieve through the use of the Interface
instruction.
The Interface
instruction allows to group, under the same name, all or part of an object’s methods which the user will have the right to access.
Object Instanciation and Object Constructor
Implementing an Interface involves three steps:
-
An
Interface
describing the required methods, -
A
Structure
describing the pointers of the corresponding functions, -
A method table: a structured variable initialized with the required functions adresses.
Step 1, consists in specifying the object’s Interface
; this is not difficult. Just name the methods.
Steps 2 and 3 are linked. In our object approach, we already have the adapted Structure
: it’s the one that describes the Class of an object.
Moreover, the Interface and the Class of an object are similar: both contain functions pointers.
Simply, the Interface
instruction doesn’t contain the Class attributes but only all or part of its methods.
Therefore it’s possible to use an object’s Class to initialize its Interface. This approach is the most natural one. Let’s not forget that an interface is the visible part of an object’s Class, so it is natural that the Class determines the Interface.
To see how this can be achieved, let’s carry on with the example of the Rectangle2
class, which provided the Draw()
and Erase()
methods.
The corresponding Class is:
Structure Rectangle2
*Draw
*Erase
x1.l
x2.l
y1.l
y2.l
EndStructure
Procedure Draw_Rectangle(*this.Rectangle2)
; [ ...some code... ]
EndProcedure
Procedure Erase_Rectangle(*this.Rectangle2)
; [ ...some code... ]
EndProcedure
The associated Interface is:
Interface Rectangle
Draw()
Erase()
EndInterface
Since the user can handle an object only through its Interface, the object must be created directly from the Rectangle
Interface, rather than from the Rectangle2
Class.
The object will thus be created 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 its declaration.
Correction made, the proper instruction to declare the object is the following one:
Rect.Rectangle = New_Rect(0, 10, 0, 20)
New_Rect()
is a function which performs the initialization operation.
We already know that its 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 allocates a memory area with the same size as the object’ Class size.
Then it initializes the methods and attributes of the object.
Finally, it ends by returning the memory area’s address.
Because the addresses of the Draw()
and Erase()
functions are positioned at the beginning of this memory area, the Interface is effectively initialized.
To access the methods of the Rect
object, just write:
Rect\Draw()
Rect\Erase()
Therefore, we have ascertained that:
-
Class
Rectangle2
allows initialization of the object’s Interface . -
Rect
— declared viaInterface
— is an object of theRectangle2
Class, and can use theDraw()
and theErase()
methods.
Thus the Interface
instruction and the New_Rect()
function perform the instanciation of a Rect
object from the Rectangle2
Class.
The New_Rect()
function is the Constructor for objects of the Rectangle2
Class.
All the Methods implementations ( |
Object Initialization
We’ve seen that after allocating the required memory area for an object, the Constructor initializes the various members of the object (methods and attributes). This operation can be isolated in a specific procedure, called by the Constructor. This precaution allows to make a distinction between an object’s memory allocation and its initialization. This approach will turn out to be very useful later on, when implementing the concept of Inheritance, because a single memory allocation is sufficient, but several initializations are required.
In addition, initialization of methods and attributes are separated too — 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 becomes:
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
An object Constructor is always associated with its counterpart: the object Destructor. During construction of an object, a memory area is allocated to store its method and attribute definitions. When an object becomes useless, it must be destroyed to free the computer memory. This process is performed by using a specific function, known as the object’s Destructor.
In our example of Rectangle2
objects, the Destructor will be:
Procedure Free_Rect(*Rect)
FreeMemory(*Rect)
EndProcedure
and can be used by:
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 containing its information (the methods it uses, and the state of itsattributes) but not deleting the object’s infrastructure. So, in our example, after doing a:
Indeed, after we instantiate an object with:
the life cycle of object |
Small reminder: the life cycle of a variable is linked to the life cycle of the program part where the variable is declared:
|
Memory Allocations
At every new instanciation, the Constructor has to dynamically allocate a memory area the size of the information describing the object.
For this purpose, the Constructor should use the AllocateMemory()
command; and the Destructor should use its associated counterpart, the FreeMemory()
command.
But there are also other candidates for achieving dynamic memory allocation. Under Windows OS, for example, the Windows API could be employed directly.
PureBasic’s standard library provides linked lists, which are also a good candidate for dynamically allocating some memory.
Encapsulation
Suppose now that we wanted to restrict the user’s access to just the Draw()
method of the Class Rectangle
. We shall begin by defining the desired interface:
Interface Rectangle
Draw()
EndInterface
Instanciation of a new object reamins the same:
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 to the previous example, because the first function address is that of the Draw()
method.
Now, suppose that we wanted to give to the user only the access to the Erase()
method. We 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:
doing so would yeld results identical to the previous case, and Rect\Erase()
would call the Draw()
method.
Thus, a new Constructor is needed, capable of returning the correct function address.
Here it is:
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
Notice how the functions addresses were simply inverted at the initialization level.
Certainly, it is not a very elegant solution to allocate the Draw
field of Rectangle2
’s Structure
with an other function’s address.
But it allows to preserve the same Structure
of the Class; and it also underlines a point:
function pointers’ names are less interesting than their values!
To solve this false problem, just rename the pointers of the Class as follows:
Structure Rectangle2
*Method1
*Method2
x1.l
x2.l
y1.l
y2.l
EndStructure
Indeed, it’s the Interface and the Constructor which give meaning 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 pointers’ names, it remains more practical to keep an explicit name when not considering to hide methods (which is the most common scenario). This allows to modify a Parent Class without having to retouch the pointers’ numbering in Children Classes. |
Inheritance
For our first implementation of the inheritance concept, let’s takes advantage of the fact that the Structure
and Interface
instructions can be extended thanks to the Extends
keyword.
So, to pass from the Rectangle1
Class, which has a single Draw()
method…
Interface Rect1
Draw()
EndInterface
Structure Rectangle1
*Method1
x1.l
x2.l
y1.l
y2.l
EndStructure
Procedure Draw_Rectangle(*this.Rectangle1)
; [ ...some code... ]
EndProcedure
Procedure Init_Mthds_Rect1(*Rect.Rectangle1)
*Rect\Method1 = @Draw_Rectangle()
EndProcedure
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
…to a Rectangle2
Class, which has two methods: Draw()
and Erase()
, we write:
Interface Rect2 Extends Rect1
Erase()
EndInterface
Structure Rectangle2 Extends Rectangle1
*Method2
EndStructure
Procedure Erase_Rectangle(*this.Rectangle1)
; [ ...some code... ]
EndProcedure
Procedure Init_Mthds_Rect2(*Rect.Rectangle2)
Init_Mthds_Rect1(*Rect)
*Rect\Method2 = @Erase_Rectangle()
EndProcedure
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
EndProcedure
Carrying out an inheritance doesn’t consist only in extending the object’s Interface
and Class Structure
, but also in adapting the initialization of its methods and attributes.
The Init_Mthds_Rect2()
and Init_Mbers_Rect2()
procedures call, respectively, the initialization of Class Rectangle1
’s methods (Init_Mthds_Rect1()
) and attributes (Init_Mbers_Rect1()
), rather than the New_Rect1()
Constructor.
This because the Child Class object (Rectangle2
) doesn’t need to instantiate its Parent Class object (Rectangle1
), but just to inherit its methods and attributes.
On the other hand, we must verify that any changes made to the Parent Class (adding a method or a variable) should be immediately reflected in its Child Class.
So, is the current state of inheritance correct? No, because it doesn’t allow the object of the Child Class (Rectangle2
) to use the new Erase()
method.
The reason being that the function pointer *Method2
doesn’t immediately follow *Method1
in order of succession.
If we look at the explicit Structure
of the Rectangle2
Class, we find:
Structure Rectangle2
*Method1
x1.l
x2.l
y1.l
y2.l
*Method2
EndStructure
instead of the Structure
below, which permits a correct initialization of the interface:
Structure Rectangle2
*Method1
*Method2
x1.l
x2.l
y1.l
y2.l
EndStructure
Remember that a correct interface initialization requires that this successsion of functions addresses appears in the same order within its Interface
(see previous note).
To solve this problem, we’ll just group all the methods into a specific Structure
!
The Class’s Structure
will need just a pointer to this new Structure
, as shown in the following example:
Interface Rect1
Draw()
EndInterface
Structure Rectangle1
*Methods
x1.l
x2.l
y1.l
y2.l
EndStructure
Procedure Draw_Rectangle(*this.Rectangle1)
; [ ...some code... ]
EndProcedure
Structure Methds_Rect1
*Method1
EndStructure
Procedure Init_Mthds_Rect1(*Mthds.Mthds_Rect1)
*Mthds\Method1 = @Draw_Rectangle()
EndProcedure
Mthds_Rect1.Mthds_Rect1
Init_Mthds_Rect1(@Mthds_Rect1)
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
EndProcedure
The Methds_Rect1
structure describes all the functions pointers of the Class’ methods.
Then follows the Methds_Rect1
variable declaration (of the Methds_Rect1
type) and its initialization thanks to Init_Mthds_Rect1()
.
The Methds_Rect1
variable is the Class’ method table, because it contains the set of all the addresses of its methods. This set constitutes the complete description of the methods of the Class.
The Structure
of Rectangle1
now contains the *Methods
pointer, which is initialized by passing the Methds_Rect1
variable address to the Constructor.
The following expression:
can be condensed into:
|
Inheritance can be now performed correctly, because by extending Methd_Rect1
’s Structure
into the new Methd_Rect2
, the functions’ addresses are going to be consecutive:
Interface Rect2 Extends Rect1
Erase()
EndInterface
Structure Rectangle2 Extends Rectangle1
EndStructure
Procedure Erase_Rectangle(*this.Rectangle2)
; [ ...some code... ]
EndProcedure
Structure Methds_Rect2 Extends Methds_Rect1
*Method2
EndStructure
Procedure Init_Mthds_Rect2(*Mthds.Mthds_Rect2)
Init_Mthds_Rect1(*Mthds)
*Mthds\Method2 = @Erase_Rectangle()
EndProcedure
Mthds_Rect2.Mthds_Rect2
Init_Mthds_Rect2(@Mthds_Rect2)
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, Rectangle2
’s Structure
is empty, and it isn’t a problem. Here are two reasons why:
-
First, the
*Methods
pointer only needs to exist once, and this is in the Parent Class. -
Second, no supplementary attributes have been added to it.
There are three advantages in having the methods’ initialization function external to the Constructor, and the method table in a single variable:
|
Get() and Set() Object Methods
Through an Interface
, it’s only possible to access an object’s methods.
The interface encapsulates completely the object’s attributes.
In order to allow access the object’s attributes — to examine, or to modify them — we must provide specific methods to the user.
The methods allowing to examine objects’ attributes are called Get()
methods.
The methods allowing to modify objects’ attributes are called Set()
methods.
In our example of the Rectangle1
Class, if I want to examine the value of the var2
attribute, I should create the following Get()
method:
Procedure Get_var2(*this.Rectangle1)
ProcedureReturn *this\var2
EndProcedure
Similarly, to modify the value of the var2
attribute, I should write the following Set()
method:
Procedure Set_var2(*this.Rectangle1, value)
*this\var2 = value
EndProcedure
Since Get()
and Set()
methods exist only to allow the user to modify all (or some) of the object’s attributes, they necessarily belong to the Interface
.
See the Appendix of the tutorial for possible optimizations of |
Synthesis and notation
Before presenting the final Class implementation, I’m going to spend some time summarizing in a formal notation the work made so far. Implementation of an object involved the following elements:
-
An Interface,
-
A Class (concrete/abstract) including methods definitions,
-
A Constructor provided with a routine for initializating attributes,
-
A Destructor.
The following table summarizes what is our object in PureBasic.
-
The word
Class
refers to the name of the Class (e.g.:Methd_Class
) -
The word
Parent
refers to the name of the Parent Class during inheritance (e.g.:Methd_ ParentClass
) -
Expressions between braces
{…}
are to be used during inheritance
Interface <Interface> {Extends <ParentInterface>}
Method1()
[Method2()]
[Method3()]
...
EndInterface
Structure <Class> {Extends <ParentClass>}
*Methods
[Attribute1]
[Attribute2]
...
EndStructure
Procedure Method1(*this.Class, [arg1]...)
...
EndProcedure
Procedure Method2(*this.Class, [arg1]...)
...
EndProcedure
...
Structure <Mthds_Class> {Extends <Mthds_ParentClass>}
*Method1
*Method2
...
EndStructure
Procedure Init_Mthds_Class(*Mthds.Mthds_Class)
{Init_Mthds_ParentClass(*Mthds)}
*Mthds\Method1 = @Method1()
*Mthds\Method2 = @Method2()
...
EndProcedure
Mthds_Class.Mthds_Class
Init_Mthds_Class(@Mthds_Class)
Procedure Init_Mbers_Class(*this.Class, [var1]...)
{Init_Mbers_ParentClass(*this)}
[*this\Attibute1 = var1]
...
EndProcedure
Procedure New_Class([var1]...)
Shared Mthds_Class
*this.Class = AllocateMemory(SizeOf(Class))
*this\Methods = @Mthds_Class
Init_Mbers_Class(*this, [var1]...)
ProcedureReturn *this
EndProcedure
Procedure Free_Class(*this)
FreeMemory(*this)
EndProcedure
PB Class
Now that we’ve explored OOP concepts and their possible implementations in PureBasic, it’s time to establish an implementation.
Here I shall present an implementation which I deem, to the best of my current knowledge, as the one best fitting OOP programming in PureBasic.
It is based on all the previously exposed work, as well as on my personal practical experience with the subject matter at hand.
Another goal here is to simplify the use of object concepts, through clear commands and by automating operations as much as possible.
During this step, macros are going to play a decisive role.
Greatly facilitated by the Interface
and Macro
commands, the proposed implementation remains naturally limited by the language itself.
At first, I’ll present the instructions for a Class in PureBasic. Then I’ll analyze what hides behind by firing parallels with the previous pages. This chapter ends with a discussion about the choices made.
PureBasic Class
; Object class
Class(<ClassName>)
[Method1()]
[Method2()]
[Method3()]
...
Methods(<ClassName>)
[<*Method1>]
[<*Method2>]
[<*Method3>]
...
Members(<ClassName>)
[<Attribute1>]
[<Attribute2>]
...
EndClass(<ClassName>)
; Object methods (implementation)
Method(<ClassName>, Method1) [,<variable1 [= DefaultValue]>,...])
...
[ProcedureReturn value]
EndMethod(<ClassName>, Method1)
; ...(ditto For each method)...
; Object constructor
New(<ClassName>)
...
EndNew
; Object destructor
Free(<ClassName>)
...
EndFree
As shown, a PureBasic Class revolves around four main themes:
-
Definition of the Class via the
Class : EndClass
block, -
Implementation of the Class’ methods via the
Method : EndMethod
block, -
Construction of the object via the
New : EndNew
block, -
Destruction of the object via the
Free : EndFree
block.
Second Code-Example
Here is a header file cotaining the definition of this set of commands, along with a usage-example file (based on the previous inheritance example, so that you might compare them):
If you have already looked at the source code of the |
Let me guide you through the PureBasic Class declaration…
Class : EndClass
The Class : EndClass
block allows declaring three types of constituent:
-
The object’s Interface, i.e: the only part of the object that the user can handle.
-
The object’s Methods — implementation excluded — which are reduced to pointers to the methods.
-
The object’s Members — methods excluded. Henceforth, the terms «member» and (more correctly) «attribute» will mainly refer to just these elements of an object (not including its methods, which are also members of the object, strictly speaking).
; Object class
Class(<ClassName>)
[Method1()]
[Method2()]
[Method3()]
...
Methods(<ClassName>)
[<*Method1>]
[<*Method2>]
[<*Method3>]
...
Members(<ClassName>)
[<Attribute1>]
[<Attribute2>]
...
EndClass(<ClassName>)
Each constituent is clearly identified with keywords: Class
/Methods
/Members
.
Their order must be preserved and all keywords must always be present, even when a method or a member will not be declared.
Also, the name of a class is always a parameter of the keyword, and must be enclosed in parentheses.
The explanation for this is to be found in the definition of each keyword. Here is the code:
Class keyword
Macro Class(ClassName)
; Declare the class interface
Interface ClassName#_
EndMacro
The keyword Class
defines just the header of the Interface
statement. The name of the interface is derived from the Class’ name followed by “_
”. So, whatever follows Class
will become the definition of the object’s interface.
Methods keyword
Macro Methods(ClassName)
EndInterface
; Declare the method-table structure
Structure Mthds_#ClassName
EndMacro
The keyword Methods
starts by closing the interface’s definition with EndInterface
. Then it opens the definition of the Structure
which defines the pointers to the methods.
Members keyword
Macro Members(ClassName)
EndStructure
; Create the method-table
Mthds_#ClassName.Mthds_#ClassName
; Declare the members
; No parent class: implement pointers for the Methods and the instance
Structure Mbrs_#ClassName
*Methods
*Instance.ClassName
EndMacro
The keyword Members
is more complicated than the two previous ones.
It begins by closing the Structure
definition previously opened by Methods
. Then it declares the method table, using the freshly-built structure (as its type). For the moment this table is empty; it will be filled up a the end of Method : EndMethod
statement. I’ll be discussing this further on (I can’t wait!).
Finally Members
ends by opening the Structure
declaration which defines the object’s members. In first position — as expected — we find the pointer to the method table (i.e.: to the variable just mentioned above). Its assignment will be done later, by the Constructor.
Then follows another pointer, which will contain the address of the object itself. I shall explain later the reasons for this new member (no, now!).
After the Members
keyword, the user has only to declare the other members of the object.
EndClass keyword
Macro EndClass(ClassName)
EndStructure
Structure ClassName
StructureUnion
*Md.ClassName#_ ; its methods
*Mb.Mbrs_#ClassName ; its memebers
EndStructureUnion
EndStructure
EndMacro
The EndClass
keyword code is at the origin of the implementation chosen for our object. So I’m now going to describe it correctly.
As with Methods
and Members
, it begins by closing what was opened by the previous keyword, in this case: the Structure
describing the object’s members.
Then, follows a Structure
named as the Class’ name, which will be use to instanciate the object.
This Structure
is in fact the union of two elements:
-
The first is a pointer typed by the interface which allows to call the object’s methods.
-
The second is a pointer typed by the structure defining members. It helps accessing the object’s members.
This design puts into practice the optimizations for Get()
and Set()
methods presented in the Appendix. The benefit of this choice is twofold:
-
It provides a seamless approach for reaching an object’s methods and members.
To reach a method, write:
*Rect\Md\Draw()
To reach an attribute, write:
*Rect\Mb\var1
-
It prevents having to systematically declare an object’s
Get()
andSet()
methods, when these are trivial. This saves time and it’s practical. At the same time, it reduces the number of objects’ methods (small optimization).
The price of this choice is that all members of an object are visible to the user. |
This structure could be slighly retouched. Since terms like “
In this code, the |
Method : EndMethod
The Method : EndMethod
block allows to achieve implementation of the various methods of an object.
; Object methods (implementation)
Method(<ClassName>, Method1) [,<variable1 [= DefaultValue]>,...])
...
[ProcedureReturn value]
EndMethod(<ClassName>, Method1)
Each keyword occurence has the Class name and the method name as parameters.
Usewise, Method : EndMethod
works like Procedure : EndProcedure
— in fact it’s a wrapper of this block, as we shall see next.
Note the very special syntax of the method which requires two closing parentheses. This specificity ensues from the use of a macro combined with a different number of possible arguments for each method. |
Method keyword
Macro Method(ClassName, Mthd)
Procedure Mthd#_#ClassName(*this.Mbrs_#ClassName
EndMacro
The Method
keyword is nothing more than a Procedure
instruction for pre-declaring the variable *this
, which is required as first argument.
The code omits the closing parenthesis so that the user might complete it by adding the parameters specific to its method. It’s the user’s responsibility to close this parenthesis, as shown in the syntax — and if he forgets, the compiler won’t fail to notice it!
EndMethod keyword
Macro EndMethod(ClassName, Mthd)
EndProcedure
; Save the method’s address into the method-table
Mthds_#ClassName\Mthd=@Mthd#_#ClassName()
EndMacro
The EndMethod
keyword begins by closing the Procedure
opened by the Method
keyword.
Once defined, the method can be referenced in the method table
(declared by the Members
keyword of the Class). Actually, by declaring a method, this method is automatically referenced.
Object Constructor
The New : EndNew
block allows to instanciate a new object of the Class by declaring and initializing it.
; Object constructor
New(<ClassName>)
...
EndNew
The New
keyword takes the class name as parameter.
New keyword
Macro New(ClassName)
Declare Init_Mbers_#ClassName(*this, *input.Mbrs_#ClassName=0)
Procedure.l New_#ClassName(*input.Mbrs_#ClassName =0)
Shared Mthds_#ClassName
; Allocate the memory required for the object members
*this.Mbrs_#ClassName = AllocateMemory(SizeOf(Mbrs_#ClassName))
; Attach the method-table to the object
*this\Methods=@Mthds_#ClassName
; The object is created then initialized
; Create the object
*this\Instance= AllocateMemory(SizeOf(ClassName))
*this\Instance\Md = *this
; Now init members
Init_Mbers_#ClassName(*this, *input)
ProcedureReturn *this\Instance
EndProcedure
Init_Mbers(ClassName)
EndMacro
The New
keyword is dense, but hasn’t really changed compared to the previous design.
The goal of this keyword is to create a new object and initialize it. These tasks are performed in the New_ClassName
procedure, which is the main part of the New
macro.
This procedure accepts a single argument, the one required by Init_Mbers
for attributes initialization.
It begins by allocating the memory space required for the object’s members.
Then it attaches to the object the method table of the Class.
Next it instanciates the object by assigning an address to it and initializing the interface.
Then follows initialization of the object’s attributes via the Init_Mbers
method.
Finally, New
returns the object’s address.
The trick in the New
macro is that it ends with the Init_Mbers
keyword. This way, what the user has to add inside the New : EndNew
block is simply the attributes initialization. More on that in a moment though (Show me now!).
This arrangement is made possible by declaring the Init_Mbers
method first in the macro.
Notice how the |
EndNew keyword
Macro EndNew
EndInit_Mbers
EndMacro
The EndNew
keyword is limited to calling the EndInit_Mbers
keyword, which completes the attributes’ initialization declaration started at the end of the New
macro.
Conclusion: the goal is reached. Through the New : EndNew
block, we have created from the Class a new object with initialized methods and attributes.
In practical use, the New : EndNew
block allows to initialize attibutes like this:
New(Rect1)
*this\var1 = *input\var1
*this\var2 = *input\var2
; [ ...some code... ]
EndNew
to instanciate such an object, write:
input.Mbrs_Rect1
input\var1 = 10
input\var2 = 20
; *Rect is a new object from Rect1 class
*Rect.Rect1 = New_Rect1(input)
Note that the constructor name is New
followed by the Class’ name separated by “_
”.
In relation to what was studied up to now, the object will always be a pointer. It isn’t an issue, rather it’s the consequence of our choice of grouping together access to methods and members (What?! I don’t remember!). |
The choice of An important feature ensues: the us of For this purpose, a |
Init_Mbers : EndInit_Mbers private block
The Init_Mbers: EndInit_Mbers
is a private block of the OOP implementation, used by the New : EndNew
block to initialize an object’s attributes. Explaining this internal block is important for understanding how initialization of an object will be carried out.
; Attributes initialization
Init_Mbers(<ClassName>)
...
EndInit_Mbers
Between the two keywords are a series of member’s initialization.
Note that only the Init_Mbers
keyword requires the Class’ name as parameter.
Init_Mbers keyword
Macro Init_Mbers(ClassName)
Method(ClassName, Init_Mbers), *input.Mbrs_#ClassName =0)
EndMacro
The Init_Mbers
instruction is defined as a method of the object accepting a single argument.
In order to initialize the object according to the user’s wishes, and because the number of its members can’t be known in advance, it was chosen to pass information by reference.
This choice is reinforced by the bias that it’s the Constructor’s responsibility to initialize the object (by calling this particular method). Last but not least, this arrangement allows to automate the process of inheritance.
In practical use, members’ initialization will mostly look like this:
Init_Mbers(Rect1)
*this\var1 = *input\var1
*this\var2 = *input\var2
; [ ...some code... ]
EndInit_Mbers
EndInit_Mbers keyword
Macro EndInit_Mbers
EndProcedure
EndMacro
The EndInit_Mbers
keyword is nothing more than the EndProcedure
keyword, which ends the definition of the object’s initialization method.
If you are the impatient sort, and have already peeked at the source code, you might have noticed that the final OOP implementation file includes extra optional parameters, named |
Object destructor
The Free: EndFree
block allows to destroy an object of the Class and to restore its memory.
; Object destructor
Free(<ClassName>)
...
EndFree
The Free
keyword takes the Class’ name as parameter.
Free : EndFree block
Macro Free(ClassName)
Procedure Free_#ClassName(*Instance.ClassName)
If *Instance
EndMacro
Macro EndFree
FreeMemory(*Instance\Md)
FreeMemory(*Instance)
EndIf
EndProcedure
EndMacro
The Free : EndFree
block is rather simple.
-
Free
opens aProcedure
with the object’s address as argument. We then check that the passed argument is not a Null address — nevertheless, it doesn’t guarantee that it’s a valid address forFreeMemory()
! -
EndFree
releases the memory allocated to the object’s members, then that of the object itself — in that specific order.
In practical use, to free an object’s intance write:
Free_Rect1(*Rect)
As for the constructor, note that the destructor’s name is “Free
” followed by the class’ name separated by “_
”.
If your object consists of other objects — i.e.: that some objects are members of the current object, and they exist by (and for) this object (hic!) — it’s then important to free them too, by calling their destructors in-between the Even if PureBasic does automatically free the allocated memory areas, it will occur only when the programs ends. During programs execution, it is up to the user to take care of any garbage memory, especially its bloat. |
Inheritance
In the set of commands just exposed, nothing makes reference to the process of inheritance. It is normal, because the current commands do not support it! (Damn! What an anguish!)! We need to introduce an additional set of commands to deal with the concept (Arghhh! Mega-anguish!).
Fortunately, it is not rocket science, and our design is ready for this (Phew! I’m feeling better now).
Here is what the Class looks like in this case:
; Object class
ClassEx(<ClassName>,<ParentClass>)
[Method1()]
[Method2()]
[Method3()]
...
MethodsEx(<ClassName>,<ParentClass>)
[<*Method1>]
[<*Method2>]
[<*Method3>]
...
MembersEx(<ClassName>,<ParentClass>)
[<Attribute1>]
[<Attribute2>]
...
EndClass(<ClassName>)
; Object methods (implementation)
Method(<ClassName>, Method1) [,<variable1 [= DefaultValue]>,...])
...
[ProcedureReturn value]
EndMethod(<ClassName>, Method1)
; ...(ditto For each method)...
; Object constructor
NewEx(<ClassName>,<ParentClass>)
...
EndNew
; Object destructor
Free(<ClassName>)
...
EndFree
Four extra keywords are supplied as replacements for Class
, Methods
, Members
and New
keywords: ClassEx
, MethodsEx
, MembersEx
and NewEx
(respectively).
For each new keyword, in addition to the current class’ name, its parent class’ name is now a parameter being passed.
The operation is simple enough for the end user, making the process of inheritance easily accessible.
In order to save space, I won’t review here the code of the new keywords; but it might be a good idea to check out OOP.pbi
in your IDE to get a feel of it.
Discussion
Phew! The presentation of a PureBasic Class is finished.
What else? Well, macros allow to define a real set of commands that:
-
Clarify the object’s structure,
-
Facilitate or automate some processes, like methods’ initialization or inheritance.
I list here the various design choices which drive the object’s conception. Let me remind you that it’s possible to partly re-adapt the design to customize objects, according to your own style, without fundamentally altering it:
-
Use
StructureUnion
to define the object. It confers to the object the peculiarity to act on the members without requiring any mutator (setter/getter) method. -
The method table is class-specific and not object-specific:
-
It is initialized only once, at the beginning of program execution, rather than at objects’ instanciation time,
-
Objects’ instances store only a pointer to their method table: a substantial save of memory space,
-
All the objects point to the same method table, which guarantees identical behavior for all objects of the same class.
-
-
A constructor which initializes the object by taking a single pointer as its input parameter, which store the initialization data of the object. The process of inheritance is largely facilitated. We can envisage to split the process in two steps: step one, the user create an object; step two, the user calls the initialization routine himself. In this case, the
Init_Mbers
method is no longer called by theNew
method, and therefore it might contain any number of arguments. Two disadvantages:-
The risk of an incorrect initialization of the object: one can forget to do it, but — more important — it’s no longer possible to automate the inheritance process: it’s up to the user to manage it!
-
Strong class-interdependence of input parameters: as soon as the initialization method’s parameters of a parent class change, the user has to carry out this changes across all its children classes.
In extreme — but it’s not advisable — we can imagine the user initializing all members, one after the other, by using mutators (setters). But members’ initialization doesn’t always boil down to mere assignment operations: it may involve more complex internal operations to reach its goal. If this is going to be repeated with each new object, it is strongly recommended to keep a dedicated method.
-
-
A destructor consistent with the constructor. It is not part of the interface, although it possibly could be. In this case, to free an object write
Objet\Md\Free()
instead of writingFree_ClassName(object)
. This arrangement is easy to operate, and doesn’t alter the design of the object. -
I have not managed to automate the generation of the
method table
. It is important to remember why it was implemented with aStructure
. Structures allow to create abstract classes — i.e.: classes whose methods are not implemented. It is a major notion of OOP’s concepts. Structures facilitate preserving the addresses’ order within the method table — whatever the implemented methods of the Class might be —, which in turn preserves the inheritance process! Using an array, a linked list, or a hash map as replacement for a Structure shall not provide this flexibility (at least I didn’t find such a solution).
Reminder of Types
Here is a list of the types used by a Class:
Type | Applied to | Origin |
---|---|---|
|
Members structure |
|
|
Object instance |
|
|
Interface |
|
|
Method Table |
|
|
Members structure |
|
The |
Conclusion
You should have understood that, while it is indeed possible to implement Object-Oriented Programming in PureBasic, it require some rigor in coding. But once this task is dealt with, manipulation of objects becomes extremely simple.
However, while OO-Programming introduces greater flexibility in coding (through the application of object concepts), its structural organization uses lots of methods, which leads to bigger executables and some performance losses.
Nevertheless, I hope that this tutorial has at least allowed you to grasp the underlying mechanisms of the OOP paradigm, and to understand its concepts.
Appendix A: Optimisations
This section offers some considerations on how to improve runtime performance of our Object-oriented approach.
Optimisation: Get() and Set() Methods
Making frequent calls to Get()
and Set()
methods means lots of function calls, and therefore a loss in performance.
For those seeking performance, there are two possible ways to speedup the process: both consist in coupling a pointer to the object, but the second solution adds a layer to the first one.
First Solution:
The pointer is specified by the Class’ Structure.
So, for an object Rect
of the Rectangle1
Class, write:
Rect.Rect = New_Rect()
*Rect.Rectangle1 = Rect
To act on the var2
attribute write:
*Rect\var2
It is then possible to both examine and modify it.
This is the simpler solution to implement.
Second solution:
The first solution requires that we work with two differently typed elements: Rect
and *Rect
.
This second solution suggests grouping these two elements in a StructureUnion
block.
Structure Rect_
StructureUnion
Mthd.Rect
*Mbers.Rectangle1
EndStructureUnion
EndStructure
Creating an object of Rectangle1
Class means declaring the object through this new Structure, by adapting its constructor like this:
New_Rect(@Rect.Rect_)
with,
Procedure New_Rect(*Instance.Rect_)
*Rect = AllocateMemory(SizeOf(Rectangle2))
Init_Rect1(*Rect)
Init_Rect2(*Rect)
*Instance\Mthd = *Rect
EndProcedure
To access to the Draw()
method, write:
Rect\Mthd\Draw()
To access to the var2
attribute, write:
Rect\Mbers\var2
The advantage of this second solution is that there is just a single element that can be dealt with, like an object whose attributes are all accessible from outside of the class. It also preserves object-oriented notation, although it introduces an extra level in its fields.
The inconvenience lies in the fact that it introduces the necessity of maintaining a new structure within the Class.