PureBasic
et la Programmation Orientée Objet
Maintenant que nous avons vu les concepts objets et leurs possibles
implémentations en PureBasic, il est grand temps de se fixer
une implémentation.
Je vous propose ici l'implémentation qui me semble, à
l'heure actuelle de mes connaissances, la plus adaptée à
la programmation objet via PureBasic.
Elle s'appuie sur l'ensemble du travail exposé précédemment
mais aussi de ma pratique du sujet.
L'autre objectif affiché est de tendre à simplifier l'utilisation
des concepts objets, par la clarté des instructions et l'automatisation
des opérations autant que possible.
Dans cette démarche les macros
vont jouer un rôle décisif.
Grandement facilitée par les instructions Interface et Macro,
l'implémentation proposée reste tout naturellement limitée
par le langage lui-même.
Dans un premier temps, nous découvrirons les instructions d'une
Classe en PureBasic. Puis nous analyserons ensemble ce qui se cache
derrière en tirant des parallèles avec les pages précédentes
pour terminer sur une discussion des choix adoptés.
Classe
PureBasic
Class(<ClassName>)
[Methode1()]
[Methode2()]
[Methode3()]
...
Methods(<ClassName>)
[<*Methode1>]
[<*Methode2>]
[<*Methode3>]
...
Members(<ClassName>)
[<Attribut1>]
[<Attribut2>]
...
EndClass(<ClassName>)
Method(<ClassName>,
Method1) [,<variable1
[= DefaultValue]>,...])
...
[ProcedureReturn value]
EndMethod(<ClassName>,
Method1)
...(idem pour déclarer chaque
methode)
New(<ClassName>)
...
EndNew
Free(<ClassName>)
...
EndFree
|
Comme on peut le voir, la Classe PureBasic s'articule autour de quatre
grands thèmes:
- La définition de la classe via l'instruction Class
: EndClass.
- L'implémentation des méthodes de la classe via l'instruction
Method : EndMethod.
- La construction de l'objet avec le constructeur New
: EndNew.
- La destructeur de l'objet avec le destructeur Free
: EndFree.
Vous trouverez
ici le fichier comportant la déclaration de ce jeu d'instructions
ainsi qu'un exemple d'utilisation basé sur l'exemple
d'héritage précédent, ce qui vous permettra
de comparer:
OOP.pbi
OOP_Inheritance.pb
|
Si vous avez jetté un coup d'oeil
au source OOP.pbi, vous aurez remarqué que l'implémentation
finale est légèrement plus compliquée que
ce qui est exposé ici. Cela s'explique par quelques dispositions
prises dans le source pour maintenir plus facilement le code.
|
Passons en revue maintenant cette Classe Purebasic...
Class :
EndClass
L'instruction Class : EndClass
permet de déclarer trois types de composantes:
- L'interface de l'objet, seule partie -rappelons le- que l'utilisateur
peut manipuler.
- Les méthodes de l'objet hors implémentation qui se
réduisent aux seuls pointeurs des méthodes.
- Les membres (hors méthodes) de l'objet. Par la suite, de
fait, les mots 'membre' et plus correctement 'attribut' feront souvent
référence à ces seuls éléments
(et non aux méthodes qui sont aussi des membres de l'objet
au sens strict).
Class(<ClassName>)
[Methode1()]
[Methode2()]
[Methode3()]
...
Methods(<ClassName>)
[<*Methode1>]
[<*Methode2>]
[<*Methode3>]
...
Members(<ClassName>)
[<Attribut1>]
[<Attribut2>]
...
EndClass(<ClassName>)
|
Chaque composante est clairement identifiée par les mots clés
Class/Methods/Members. Cet ordre doit être respecté et
les mots clés doivent toujours figurer même si aucune méthode
ou aucun membre ne sera déclaré. De même, à
chaque fois le nom de la classe se doit d'apparaître entre parenthèses.
L'explication trouve son origine dans la définition de chaque
mot clé dont voici le code:
Instruction Class
Macro Class(ClassName)
Interface ClassName#_
EndMacro
|
L'instruction Class se limite
à l'entête de la déclaration de l'interface avec
pour nom d'interface celui de la classe suivi de "_". Ce qui
suit l'instruction Class sera
donc la définition de l'interface de l'objet.
Instruction Methods
Macro
Methods(ClassName)
EndInterface
Structure Mthds_#ClassName
EndMacro |
L'instruction Methods commence
par fermer la définition de l'interface avec l'instruction EndInterface.
Puis elle débute la déclaration de la structure qui définit
les pointeurs des méthodes.
Instruction Members
Macro
Members(ClassName)
EndStructure
Mthds_#ClassName.Mthds_#ClassName
Structure Mbrs_#ClassName
*Methods
*Instance.ClassName
EndMacro |
L'instruction Members est plus
compliquée que les deux précédentes.
Elle commence par fermer la définition de la structure précédemment
ouverte par l'instruction Methods.
Ensuite elle déclare tout naturellement la table des methodes
basée sur la structure fraîchement acquise. Pour l'instant
cette table est vide et se remplira au fur et à mesure de la
déclaration des méthodes. Nous aborderons cela plus loin
(j'peux
pas attendre).
Enfin l'instruction Members se
termine en ouvrant la déclaration de la structure qui définit
les membres de l'objet. On trouve en début -comme il convient-
le pointeur porteur de l'adresse de la table des méthodes de
l'objet, c.a.d celle de la variable juste au-dessus. Rappelons que c'est
le constructeur qui initialisera le tout. Puis nous trouvons un autre
pointeur qui contiendra l'adresse de l'objet lui-même. J'expliquerai
plus tard la raison de ce nouveau membre (non!
maintenant ).
Il reste simplement pour l'utilisateur qu'à déclarer
les autres membres de l'objet à la suite de l'instruction Members.
Instruction
EndClass
Macro
EndClass(ClassName)
EndStructure
Structure ClassName
StructureUnion
*Md.ClassName#_ ; les méthodes
*Mb.Mbrs_#ClassName ; les membres
EndStructureUnion
EndStructure
EndMacro |
L'instruction EndClass est à
l'origine du choix d'implémentation de notre objet. Nous allons
donc nous attarder à la décrire correctement.
Comme pour Methods et Members,
elle commence par fermer ce qui a été ouvert par l'instruction
précédente, ici la structure décrivant les membres
de l'objet.
Ensuite, nous trouvons la structure qui porte le nom de la classe et
qui servira donc à l'utilisateur pour déclarer son objet.
Cette structure est en fait l'union de deux éléments:
- Le premier est un pointeur typé par l'interface qui permet
d'appeler les méthodes de l'objet.
- Le second est un pointeur typé par la structure définissant
les membres. Il sert à accéder aux membres de l'objet.
Cette architecture met en pratique l'optimisation sur les accesseurs
d'un objet exposée en annexe.
L'intérêt de ce choix est double:
Pour accéder à un attribut, il suffira d'écrire:
- Il évite à l'utilisateur de déclarer systématiquement
les accesseurs/modifieurs de l'objet lorsque ceux-ci sont triviaux.
Le gain en temps et en commodité est des plus appréciable.
On limite du même coup le recourt à des méthodes
(petite optimisation).
|
En contre partie de ce choix, tous les
membres d'un objet sont visibles par l'utilisateur. |
|
Il est possible d'agrémenter
un peu cette structure. Comme les termes 'Md' et 'Mb' sont visuellement
très proches on peut être tenté de mieux les
distinguer. Bien que ce choix n'ai pas été retenu,
voici une possibilité intéressante:
Structure ClassName
StructureUnion
*Md.ClassName#_ ; les méthodes
*Get.Mbrs_#ClassName ; utilisé pour lire un membre
*Set.Mbrs_#ClassName ; utilisé pour modifier un menbre
EndStructureUnion
EndStructure
Ici, le pointeur *Mb a été remplacé par
deux pointeurs *Get et *Set. Bien que servant à la même
chose, ils peuvent rendre le code plus lisible en précisant
si l'on veut lire ou modifier la valeur d'un attribut. |
Method
: EndMethod
L'instruction Method : EndMethod
permet de réaliser l'implémentation des différentes
méthodes de l'objet.
Method(<ClassName>,
Method1) [,<variable1
[= DefaultValue]>,...])
...
[ProcedureReturn value]
EndMethod(<ClassName>,
Method1)
|
Chaque mot clé est suivi du nom de la classe et du nom de la
méthode.
A l'usage, cette instruction se travaille comme l'instruction Procedure
: EndProcedure. Nous allons voir qu'il s'agit là d'un
habillage ce cette instruction.
|
Notez la syntaxe très particulière
des méthodes qui nécessite deux parenthèses
fermées. Cette spécificité vient de l'utilisation
d'une macro combinée à un nombre variable d'arguments
possible pour chaque méthode. |
Instruction Method
Macro
Method(ClassName, Mthd)
Procedure Mthd#_#ClassName(*this.Mbrs_#ClassName
EndMacro |
L'instruction Method n'est rien
d'autre que l'instruction Procedure à laquelle on aura pris soin
de déclarer la variable *this exigée en début d'arguments.
Le code ne se termine pas par une parenthèse afin de permette
à l'utilisateur de compléter par les paramètres
spécifiques de sa méthode. A lui de fermer cette parenthèse
comme la syntaxe le montre, sinon le compilateur ne manquera pas de
le signaler!
Instruction
EndMethod
Macro
EndMethod(ClassName, Mthd)
EndProcedure
Mthds_#ClassName\Mthd=@Mthd#_#ClassName()
EndMacro |
L'instruction EndMethod commence
par fermer la procédure ouverte par l'instruction Method.
Maintenant que la méthode est définie, on peut la référencer
dans la table des méthodes déclarée lors du mot
clé Members de la classe. De fait, déclarer une méthode
revient aussi à la référencer automatiquement.
Le
constructeur de l'objet
L'instruction New : EndNew permet
d'instancier et d'initialiser un objet de la classe.
New(<ClassName>)
...
EndNew
|
Le mots clé New exige
le nom de la classe comme paramètre.
Instruction
New
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 |
L'instruction New est dense mais
ne change pas vraiment par rapport à la structuration vue auparavant.
Le but de ce mot clé est de créer un nouvel objet et
de l'initialiser. L'ensemble de ces taches est réalisé
au sein de la procédure New_ClassName
qui est l'essentiel de la commande New.
Cette procédure accepte un seul argument, celui nécessaire
à l'instruction Init_Mbers
pour initialiser les attributs de l'objet.
Elle commence par réserver la place mémoire requise par
les membres de l'objet.
Puis elle y attache la table des méthodes de la classe.
Elle s'attaque alors à l'instanciation de l'objet en attribuant
une adresse à l'objet et en initialisant l'interface.
Vient alors l'initialisation des attributs de l'objet via la méthode
Init_Mbers.
Et pour finir, l'instruction retourne l'adresse de l'objet.
L'astruce de cette macro réside dans la déclaration de
Init_Mbers en toute fin. De la
sorte, tout ce que l'utilisateur aura à ajouter à l'intérieur
du block New:EndNew se limitera à l'initialisation des attributs.
Cet aspect sera abordé plus en détail dans un moment(Où
ça? Où ça?).
Ceci est rendu possible en declarant la method Init_Mbers
en début de macro.
|
On peut remarquer que la procedure New_ClassName
est générique quelque soit la classe. Ceci s'explique
car la partie qui varie (et donc spécifique à l'objet),
est déportée hors de la procédure dans la
methode Init_Mbers.
|
Instruction EndNew
Macro
EndNew
EndInit_Mbers
EndMacro |
L'instruction EndNew se limite
à l'appel de l'instruction EndInit_Mbers
qui termine la déclaration des attributs débutée
par l'instruction New.
Conclusion: l'objectif est atteint. l'instruction New
: EndNew
permet bien de créer un nouvel objet initialisé.
A l'usage, l'instruction New :
EndNew permettra d'initialiser
les attributs d'un objet comme suite:
New(Rect1)
*this\var1 = *input\var1
*this\var2 = *input\var2
...
EndNew |
l'instanciation d'un objet par l'utilisateur se fera alors ainsi:
input.Mbrs_Rect1
input\var1 = 10
input\var2 = 20
*Rect.Rect1 = New_Rect1(input) |
Notez que l'on appelle le constructeur par New suivi du nom de la classe
séparé par "_".
|
Par rapport à ce qui a été
étudié jusqu'à présent, l'objet sera
toujours un pointeur (car recevant une adresse). Loin d'etre gênant,
cela s'explique par le choix fait de regrouper l'accès
des méthodes et des membres (Quoi!?
J'me rappelle pas...). |
|
C'est ce même choix qui requis
deux allocations mémoire distinctes: celle des membres
et celle pour le regroupent des méthodes et des membres
(4 octets ici).
Cette bivalence qui n'existait pas lors de l'implémentation
précédente, nous conduit à conserver cette
information dans l'objet lui-même. Ainsi dans les méthodes
de l'objet, vous aurait accès à l'adresse des membres
avec *this et à l'adresse de l'instance (méthode
et membres) par *this\Instance.
Une des conséquences importante est la possibilité
d'utiliser *this\Instance pour appeler les méthodes de
l'objet au sein même de ses méthodes (Non je n'ai
pas bu!). Cette fonctionnalité est la meilleure manière
pour y parvenir car on n'a pas a connaître le nom de la
procédure présente derrière la méthode,
ce qui est essentiel dans le processus d'héritage.
A cet effet, une macro Mtd est proposée dans le source
OOP.pbi. |
L'instruction
privée Init_Mbers : EndInit_Mbers
L'instruction Init_Mbers : EndInit_Mbers
est une instruction privée que seul l'instruction New
: EndNew va utiliser. Néanmoins,
il est interessant de présenter cette instruction pour bien comprend
comment procéder à l'initialisation d'un objet.
Init_Mbers(<ClassName>)
...
EndInit_Mbers
|
On trouvera donc entre les deux mots clés une série d'initialisation
de membres.
Notez que seul Init_Mbers est
suivi du nom de la classe.
Instruction Init_Mbers
Macro
Init_Mbers(ClassName)
Method(ClassName, Init_Mbers), *input.Mbrs_#ClassName =0)
EndMacro |
L'instruction Init_Mbers est
définie comme une méthode de l'objet acceptant un seul
argument.
Afin d'initialiser l'objet en fonction des souhaits de l'utilisateur
et ne sachant pas à l'avance le nombre de membres, le choix c'est
porté sur un passage de l'information par référent.
Ce choix s'explique aussi par le parti pris que le constructeur à
la responsabilité d'initialiser l'objet.
Enfin, et non des moindres, cette disposition permet d'automatiser la
tache lors du processus d'héritage.
A l'usage, l'initialisation de membres ressemblera le plus souvent
à ce qui suit:
Init_Mbers(Rect1)
*this\var1 = *input\var1
*this\var2 = *input\var2
...
EndInit_Mbers |
Instruction EndInit_Mbers
Macro
EndInit_Mbers
EndProcedure
EndMacro |
L'instruction EndInit_Mbers est
ni plus ni moins que l'instruction EndProcedure
qui termine la déclaration de la méthode d'initialisation
de l'objet.
|
Dans le code source final OOP.pbi,
l'instruction Init_Mbers:EndInit_Mbers comporte des arguments
falcutatifs supplémentaires appelés arg1 à
arg5. Dans certaines situations, il peut être utile de faire
appel à ces arguments pour compléter les données
d'entrées du pointeur *input. |
Destructeur
de l'objet
L'instruction Free : EndFree permet
de détruire un objet de la classe afin de restituer la mémoire
allouée.
Free(<ClassName>)
...
EndFree
|
L'instruction Free exige le nom
de la classe en paramètre.
Instruction
Free:EndFree
Macro
Free(ClassName)
Procedure Free_#ClassName(*Instance.ClassName)
If *Instance
EndMacro
Macro EndFree
FreeMemory(*Instance\Md)
FreeMemory(*Instance)
EndIf
EndProcedure
EndMacro |
L'instruction Free:EndFree est
assez simple:
- Free ouvre une procédure
avec pour argument l'adresse de l'objet. On vérifie au passage
que l'adresse est non nulle (cela ne garanti malheireusement pas une
adresse valide pour FreeMemory).
- EndFree libère dans
l'ordre la zone mémoire allouée aux membres puis celle
de l'objet.
A l'usage, la destruction d'un objet se fait ainsi:
Comme pour le constructeur, notez bien que l'on appelle le destructeur
par Free suivi du nom de la classe séparé par "_".
|
Si votre objet se compose d'autres objets,
c.a.d que des objets sont des membres de l'objet en question et
qu'ils existent par cet objet (hic!), il est important de les
détruire en appelant leurs destructeurs entre les mots
clés Free et EndFree.
Bien que PureBasic se charge de restituer toute mémoire
utilisée, cela se produira uniquement une fois le programme
terminé. Au court de l'exécution du programme, c'est
à l'utilisateur de veiller aux grains pour éviter
toute gourmandise binaire. |
Héritage
Dans l'ensemble des commandes qui vient d'être exposé rien
ne fait référence au processus d'héritage. Et c'est
normal, puisque les instructions présentées ne le permettent
pas et ne le peuvent pas (Moua ! l’angoisse !) ! Il est nécessaire
de décliner un jeu d'instructions complémentaire pour
traiter ce concept(Arghhh ! Mega angoisse).
Fort heureusement, il y a peu d'effort à fournir pour y parvenir
puisque notre conception y est préparée (Ouf ! Ca va mieux).
Voici donc à quoi ressemble la classe dans ce cas de figure:
ClassEx(<ClassName>,<MotherClass>)
[Methode1()]
[Methode2()]
[Methode3()]
...
MethodsEx(<ClassName>,<MotherClass>)
[<*Methode1>]
[<*Methode2>]
[<*Methode3>]
...
MembersEx(<ClassName>,<MotherClass>)
[<Attribut1>]
[<Attribut2>]
...
EndClass(<ClassName>)
Method(<ClassName>,
Method1) [,<variable1
[= DefaultValue]>,...])
...
[ProcedureReturn value]
EndMethod(<ClassName>,
Method1)
...(idem pour déclarer chaque
méthode)
NewEx(<ClassName>,<MotherClass>)
...
EndNew
Free(<ClassName>)
...
EndFree
|
Quatre nouvelles instructions font leur apparition: ClassEx,
MethodsEx, MembersEx
et NewEx en remplacement de Class,
Methods, Members et New.
Pour chacune d'elle, en plus du nom de la classe, doit être précisé
le nom de la classe mère.
L'opération est finalement assez simple pour l'utilisateur rendant
le processus d'héritage très facile d'accès.
Je vous laisse le soin d'aller regarder le code pour analyser comment
cela a été implémenté (OOP.pbi).
Discussion
Ouf! la présentation d'une Classe PureBasic est terminée.
Que peut-on en dire? Déjà, les macros ont permis de définir
un jeu d'instructions permettant de:
- Clarifier la structure d'un objet
- Faciliter voir d'automatiser certaines taches, comme l'initialisation
des méthodes ou l'héritage.
Je liste ici les choix de conception qui conduisent à la spécificité
de l'objet. Comme nous allons le voir, il est possible d'en adapter
certains pour vous approprier l'objet sans fondamentalement le modifier:
- Utilisation d'une structure d'union pour définir l'objet.
Cela lui confère la particularité de pouvoir accéder
aux membres sans générer obligatoirement un accesseur.
- La table des méthodes est propre à la classe et non
à l'objet.
- Elle est initialisée une fois pour toute et non plus
à chaque instanciation d'un objet,
- Les objets instanciés ne disposent que d'un pointeur
vers la table des méthodes: le gain en place mémoire
est substantiel,
- Tous les objets pointent vers la même table des méthodes,
cela garantit un comportement identique des objets de même
Classe.
- Un constructeur qui initialise l'objet, conduisant à utiliser
un seul paramètre d'entré par référent
pour passer les valeurs d'initialisation de l'objet. Le processus
d'héritage en est grandement facilité.
Hors, on peut tout à fait imaginer créer un objet, puis
que l'utilisateur appelle lui-même la routine d'initialisation:
auquel cas, la méthode Init_Mbers n'est plus appelée
par New et peut de ce fait comporter un nombre d'arguments quelconque.
J'y vois au moins deux inconvénients:
- Le risque d'une initialisation incorrecte de l'objet: on peut
oublier de faire cet appel, mais surtout il n'est plus possible
d'automatiser le processus d'héritage: c'est à l'utilisateur
de le gérer!
- Une forte interdépendance entre classe mère et
classe fille : dès que les paramètres d'entrée
de la méthode d'initialisation changent pour la classe
mère, l'utilisateur doit procéder à cette
modification dans toutes les classes filles.
Malgré tout cette disposition ne change pas fondamentalement
notre objet.
A l'extrême, mais je le déconseille fortement, on peut
imaginer que l'utilisateur initialise membre après membre
en utilisant les accesseurs. Mais initialiser les membres d'un objet
ne se limite pas toujours à une opération d'affectation.
Elle peut nécessiter d'autres opérations internes
plus complexes pour y parvenir. Si cela doit être répété
à chaque nouvel objet, il est vivement conseillé de
conserver une méthode dédiée.
- Un destructeur homogène avec le constructeur. Il ne fait
pas partie de l'interface bien que cela soit envisageable. Dans le
cas contraire on écrirait 'Objet\Md\Free()' au lieu d'écrire
'Free_ClassName(Objet)'. Cette disposition est aisée à
opérer et ne modifie en rien la conception de l'objet.
- Je ne suis pas arrivé à automatiser la génération
de la table des méthodes. Il est important de rappeler ici
pourquoi elle s'articule autour d'une structure. La structure permet
de créer des classes abstraites, c'est à dire des classes
où toutes les méthodes ne sont pas implémentées.
C'est une notion très important de la conception objet et on
respecte ainsi l'ordre des adresses dans la table quelque soient les
méthodes implémentées de la classe tout en s'accordant
avec le processus d'héritage. Utiliser un tableau, une liste
chaînée ou une table de hâchage en remplacement
d'une structure n'aurai pas cette souplesse (du moins je ne l'ai pas
trouvé).
Rappel
des types
Vous trouverez ici la liste des types qu'utilise une classe:
Type |
S'applique à |
Origine |
<ClassName> |
L'objet instancié |
EndClass |
<ClassName>_ |
L'Interface |
Class |
Mthds_<ClassName> |
La table des méthodes |
Methods |
Mbrs_<ClassName>_ |
La Structure des membres |
Members |
Mbrs_<ClassName> |
La Structure des membres |
EndClass |
Retour en haut de page
|