PureBasic
and the Object-Oriented Programming
Now we saw OOP concepts and their possible implementations in PureBasic,
it's time to establish an implementation.
Here I present an implementation which seems to me, according to my
current knowledge, the most adapted to the OOP by the way of PureBasic.
It is based on the whole work exposed previously but also of my own
experience of the practice of the subject.
The other goal is to tend to simplify the use of object concepts, by
clear commands and by automating operations as much as possible.
In this step, macros are going to play a decisive role.
Greatly facilitated by Interface and Macro commands, the proposed implementation
remains naturally limited by the language itself.
At first, I'll present the instructions of a Class in PureBasic. Then
I'll analyze what hides behind by firing parallels with the previous
pages. This chapter ended on a discussion of the choices made.
PureBasic
Class
Class(<ClassName>)
[Method1()]
[Method2()]
[Method3()]
...
Methods(<ClassName>)
[<*Method1>]
[<*Method2>]
[<*Method3>]
...
Members(<ClassName>)
[<Attribute1>]
[<Attribute2>]
...
EndClass(<ClassName>)
Method(<ClassName>,
Method1) [,<variable1
[= DefaultValue]>,...])
...
[ProcedureReturn value]
EndMethod(<ClassName>,
Method1)
...(ditto for each method)
New(<ClassName>)
...
EndNew
Free(<ClassName>)
...
EndFree
|
As shown, the PureBasic Class is articulated with four main subjects:
- The definition of the class with Class
: EndClass block,
- The implementation of the methods of the class with Method:
EndMethod block,
- The construction of the object with New
: EndNew block,
- The destructor of the object with Free
: EndFree block.
You will
find here the file containing the declaration of this set of commands
as well as an example file of use based on the inheritance example,
what will allow you to compare with previous
implementation:
OOP.pbi
OOP_Inheritance.pb
|
If you have already looked at the source
code from the OOP.pbi file, you are going to notice that the final
OOP implementation in the source is a little more complicated
than the basic explanation from this acticle. This is because
some arrangements are made to service update of the source.
|
Let me run you through the Purebasic Class declaration...
Class :
EndClass
Class: EndClass block allows declaring
three types of constituents:
- The interface of the object, only part that the user can handle.
- The methods of the object -except implementation- which are reduced
to the pointers of the methods.
- Members (except methods) of the object. Afterward, words 'member'
and more correctly 'attribute' will often make reference to these
only elements (and not to the methods which are also members of the
object in the strict meaning).
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.
This order must be preserved and keywords always have to appear even
though any method or member will not be declared. Also, at each time
the name of the class is a parameter of the keyword.
The explanation takes place in the definition of every keyword. Here
is the code:
Class keyword
Macro Class(ClassName)
Interface ClassName#_
EndMacro
|
The keyword Class declares just
the header of the interface statement. The name of the interface comes
from the class name merged with "_". So what follows Class
will be the definition of the interface of the object.
Methods keyword
Macro
Methods(ClassName)
EndInterface
Structure Mthds_#ClassName
EndMacro |
The keyword Methods begins by
closing the definition of the interface with EndInterface. Then it opens
the statement of the structure which defined the pointers of the methods.
Members keyword
Macro
Members(ClassName)
EndStructure
Mthds_#ClassName.Mthds_#ClassName
Structure Mbrs_#ClassName
*Methods
*Instance.ClassName
EndMacro |
The keyword Members is more complicated
than the two previous ones.
It begins by closing the definition of the structure first opened by
Methods. Then it declares the
table of the methods based on the structure freshly built. For the moment
this table is empty and will be fill a the end of Method
: EndMethod statement. I'll be discussing this farther (I
can't wait).
Finally Members ends by opening the structure declaration which defined
the members of the object. At first position -as expected- is the pointer
to the table of the methods, i.e. to the variable just above. The assignment
is done later by the constructor. Follows
another pointer which will contain the address of the object itself.
I shall explain later the reason of this new member (not!
Now).
It remains for the user simply to declare the other members of the
object next Members keyword.
EndClass
keyword
Macro
EndClass(ClassName)
EndStructure
Structure ClassName
StructureUnion
*Md.ClassName#_ ; les méthodes
*Mb.Mbrs_#ClassName ; les membres
EndStructureUnion
EndStructure
EndMacro |
EndClass keyword code is at the
origin of the implementation chosen for our object. So I'm now going
to describe it correctly.
As for Methods and Members,
it begins by closing what was opened by the previous keyword, here the
structure describing the members of the object.
Then, follows the structure called with the name of the class and 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 methods of the object.
- The second is a pointer typed by the structure defining members.
It helps in acting on the members of the object.
This design puts into practice optimization on the accessors exposed
in appendix.
The benefit of this choice is double:
To reach a attribute, write:
- It avoids to declare systematically Get and Set methods of an object
when they are ordinary. That saves time and it's practical. At the
same moment, that limits the numbers of methods of an object (small
optimization).
|
The price of this choice is that all
the members of an object are visible by the user. |
|
This structure could be adapted a little.
As terms ' Md ' and ' Mb ' are visually very close, a better distinguish
could be studied. Although this choice was not retain, here is
an interesting possibility:
Structure ClassName
StructureUnion
*Md.ClassName#_; methods
*Get.Mbrs_#ClassName; used to read a member
*Set.Mbrs_#ClassName; used to modify a menbre
EndStructureUnion
EndStructure
In this code, the pointer *Mb was replaced by two pointers *Get
and *Set. They have the same functionality but they can lead to
a more legible code by clarifying if an attribute is readed or
modified.
|
Method
: EndMethod
Method: EndMethod
block allows to acheive the implementation of the various methods
of an object.
Method(<ClassName>,
Method1) [,<variable1
[= DefaultValue]>,...])
...
[ProcedureReturn value]
EndMethod(<ClassName>,
Method1)
|
Each keyword has the class name and the method name as parameters.
In use, Method : EndMethod works
like Procedure: EndProcedure. In
fact it is a wrap of this block
as exposed hereafter.
|
Note the very special syntax of the
method which requires two closed brackets. This specificity comes
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 |
Method keyword is not more than
a Procedure keyword where are
pre-declared the variable *this required as first argument.
Code doesn't end by a bracket to allow the user to complete it by the
specific parameters of its method. Up to him to close this bracket as
shown in the syntax, otherwise the compiler will not miss to notify
this!
EndMethod
keyword
Macro
EndMethod(ClassName, Mthd)
EndProcedure
Mthds_#ClassName\Mthd=@Mthd#_#ClassName()
EndMacro |
EndMethod keyword begins by closing
the procedure opened by the Method
keyword.
Once the method defined, it can be referenced into the table of methods
(declared by the Members keyword
of the class). Actually, by declaring a method, this method is automatically
referenced.
Object
Constructor
New :
EndNew block allows to
instanciate a new object of the class by declaring and initialising.
New(<ClassName>)
...
EndNew
|
The New keyword has 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
*this.Mbrs_#ClassName = AllocateMemory(SizeOf(Mbrs_#ClassName))
*this\Methods=@Mthds_#ClassName
*this\Instance= AllocateMemory(SizeOf(ClassName))
*this\Instance\Md = *this
Init_Mbers_#ClassName(*this, *input)
ProcedureReturn *this\Instance
EndProcedure
Init_Mbers(ClassName)
EndMacro |
New keyword is dense but doesn't
change really compare to previous design.
The goal of this keyword is to create a new object and to initialise
it. These tasks are performed into 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 attributs initialisation.
It begins by allocated the memory space required for the members of
the object.
Then it attaches to it the table of the methods of the class.
Next it performs the object instanciation by assigning an address to
the object then by initializing the interface.
The initialization of the attributes of the object follows through
Init_Mbers method.
Finally, New returns the address
of the object.
The trick is that the New macro ends by the keyword Init_Mbers.
Like this, what the user has to add inside the New : EndNew block is
simply the attributs initialisation. More on that in a moment though
(Give
me now!).
This arrangement is possible thanks declaring the Init_Mbers
method first in the macro.
|
Notice that the New_ClassName
procedure is common to all kind of Class. It is because the variant
part was externalized into Init_Mbers
method. |
EndNew
keyword
Macro
EndNew
EndInit_Mbers
EndMacro
|
EndNew keyword is limited to
call the EndInit_Mbers keyword
that end the statement of the attribut initialisation start at the end
of the New macro.
Conclusion: the goal is reached. Through the New
: EndNew
block, we have a new object of the Class initialised (methods
and attributs)
In use, the New :
EndNew block allows the
attibuts initialisation like this:
New(Rect1)
*this\var1 = *input\var1
*this\var2 = *input\var2
...
EndNew |
to instanciate an such object write:
input.Mbrs_Rect1
input\var1 = 10 input\var2 = 20
*Rect.Rect1 = New_Rect1(input)
|
Note that the constructor name is 'New' followed by the name of the
class separated by "_".
|
With regard to what was studied until
now, the object will always be a pointer. It doesn’t matter
but it's the consequence of the choice made in regrouping the
access of the methods and the access of the members (What!?
I don't remember!). |
|
The choice of the StructureUnion requires
two different memory allocations: the one for the members and
the one to regroup methods and members (4 bytes here).
This bivalence -which didn't exist for the previous implementation-
leads us to keep information into the object. So in the methods
of the object, you could access to the address of members with
*this and to the address of the instance (method and members)
by *this\Instance.
It ensues an important feature by using *this\Instance to call
the methods of the object within its methods (No I did not drink!).
This feature is the best way to do this task because the name
of the procedure behind the method isn't really known, what is
needed in the inheritance process.
For that purpose, a macro Mtd is directly proposed in source
file OOP.pbi.
|
Init_Mbers
: EndInit_Mbers private block
Init_Mbers: EndInit_Mbers block
is a private block of the OOP implementation, used by the New
: EndNew
block to initilalise attributs of an object. But it is important
to present this internal block to understand how an object initialisation
will by specified.
Init_Mbers(<ClassName>)
...
EndInit_Mbers
|
Between the two keywords are a series of member’s initialization.
Note that only 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 |
Instruction Init_Mbers is defined
as a method of the object accepting a single argument.
To initialize the object according to the wishes of the user and because
the number of members couldn't be known in advance, the choice was made
to pass information by referent.
This choice is reinforced by the prejudice that the constructor has
the responsibility to initialize the object (by calling this particular
method).
Finally, but not the least, this disposal allows to automate the process
of inheritance.
In use, the initialization of members will look like mostly what follows:
Init_Mbers(Rect1)
*this\var1 = *input\var1
*this\var2 = *input\var2 ...
EndInit_Mbers |
EndInit_Mbers keyword
Macro
EndInit_Mbers
EndProcedure
EndMacro |
EndInit_Mbers keyword is not
more and not less that the EndProcedure
keyword which ends the statement of the method of the object initialization.
|
If you are the impatient sort and have
already looked at the source code, you may have noticed that the
final update OOP implementation source file include extra optional
parameters called arg1 to arg5. This is because in some situation,
it is useful to complet the standard *input pointer by additional
information. |
Object
destructor
Free: EndFree
block allows to destroy object of the class and to restore memory.
Free(<ClassName>)
...
EndFree
|
Free keyword has 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 |
Free:EndFree
block is rather simple.
- Free opens a procedure with
the object address as argument. In passing we check that it is a none
null address (nevertheless it not guaranteed a valid address for FreeMemory()
).
- EndFree releases sequentially
the memory assigned to members then that of the object.
In use, to free an object intance write:
As for the constructor, note that the destructor name is 'Free' followed
by the name of the class separated by "_".
|
If your object consists of other objects,
i.e. that objects are members of the current object and exist
by this object (hic!) it is important to free them too by calling
their destructors between keywords Free and EndFree.
Even if PureBasic 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 memory waste especially
its growth. |
Inheritance
In the set of commands just exposed, nothing makes reference to the
inheritance process. It is normal because current commands do not support
this! (Damn! Fear!)! We need to decline an additional set of commands
to deal with the concept (Arghhh! <death pangs>).
Fortunately, it is not rocket science as our conception is ready for
this (Phew! I feeling better now).
Here what looks like the class in that case:
ClassEx(<ClassName>,<MotherClass>)
[Method1()]
[Method2()]
[Method3()]
...
MethodsEx(<ClassName>,<MotherClass>)
[<*Method1>]
[<*Method2>]
[<*Method3>]
...
MembersEx(<ClassName>,<MotherClass>)
[<Attribute1>]
[<Attribute2>]
...
EndClass(<ClassName>)
Method(<ClassName>,
Method1) [,<variable1
[= DefaultValue]>,...])
...
[ProcedureReturn value]
EndMethod(<ClassName>,
Method1)
...(ditto for each method)
NewEx(<ClassName>,<MotherClass>)
...
EndNew
Free(<ClassName>)
...
EndFree
|
Four extra keywords are supplied: ClassEx,
MethodsEx, MembersEx
and NewEx as a replacement of Class,
Methods, Members and New.
For each new keyword, in addition of the name of the current class,
the name of the mother class is given as a new argument.
The operation is finally rather simple for the user with a very easy
process for inheritance.
I won't list the declaration of the new keywords here to save space,
but it might be a good idea to check out OOP.pbi
in your IDE to get a feel for it.
Discussion
Phew! The presentation of a PureBasic Class is finished.
What else? Well, macros allow to define a real set of commands that:
- Clarified the object structure,
- Facilitate or automate some processes, as the methods initialization
or the inheritance.
I list here the various choices which drive the conception to the object.
Let me you feel that it is possible to adapt some parts to suit one’s
style to one’s object without fundamentally modifying it:
- Use of StructureUnion to define the object. It confers to the object
the peculiarity to act on the members without using any accessor method.
- The table of methods is class-specific and not object-specific.
- It is initialized once at the beginning of the program and not
at the intanciate level of an object,
- Objects instance store only a pointer towards the table of methods:
substantial save of memory space,
- All the objects point towards the same table of methods, which
guarantees an identical behavior of the objects from the same class.
- A constructor which initializes the object, driving to use a single
pointer as input parameter which store the initialisation 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 routine of initialization
himself. In this case, method Init_Mbers is not called by New method
and then it can support 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 not possible
any more to automate the process of inheritance: it’s up
to the user to manage it!
- A strong class-interdependence for enter parameters: as soon
as the parameters of the initialization method change from the
mother class, the user has to proceed this modification in all
the child classes.
In extreme, but it’s not advisable, we can imagine that the
user initializes member after member by using accessors. But a member
initialization is not always a single assignation. It can require
more complex internal operations to reach the assignation. Repeat
this at each new object, it is advised to keep this into a dedicated
method.
- A destructor consistent with the constructor. It is not a part
of the interface although it is possible. In this case, to free an
object write ' Objet\Md\Free () ' instead of writing ' Free_ClassName
( object) '. This disposal is easy to operate and doesn't modify the
conception of the object.
- I didn't achieve the automation of the table of methods. It is
important to remind why it is articulated with a structure. Structure
allows to create abstract classes, that are classes where all the
methods are not implemented. It is a major notion of the object concepts.
Structure facilitates to preserve the addresses order into the table
of methods what ever the implemented methods are while preserve the
inheritance process! An array, a linkedlist or a hash map as a replacement
of a structure shall not have this flexibility (at least I didn't
find a such solution).
Reminder
of types
Find here the list of the types which are used by a class:
Type |
Applied to |
Origin |
<ClassName> |
Object instance |
EndClass |
<ClassName>_ |
Interface |
Class |
Mthds_<ClassName> |
Table of methods |
Methods |
Mbrs_<ClassName>_ |
Members structure |
Members |
Mbrs_<ClassName> |
Members structure |
EndClass |
Top of the page
|