|
|
|
PureBasic
et la Programmation Orientée Objet
Dans notre première implémentation, nombres de concepts
ont été traduits d'une manière plus ou moins étendue.
Nous allons voir maintenant comment on peut améliorer cette implémentation
grâce à l'utilisation de l'instruction Interface.
Notion d'interface
Objet :
Nous avons vu que la notion d'encapsulation avait comme but premier de
rendre visible à l'utilisateur qu'une partie de l'objet.
La partie visible du contenu est appelée interface, l'autre
partie cachée est appelée implémentation.
L'interface d'un objet est donc la seule porte d'entrée/sortie
dont dispose l'utilisateur pour agir sur un objet.
C'est le rôle que l'on va donner dans notre utilisation de l'instruction
Interface.
L'instruction Interface va donc nous permettre de regrouper sous un même
Nom, tout ou partie des méthodes d'un objet que l'utilisateur aura
le droit de manipuler.
Instanciation et
Constructeur d'Objet
Vouloir utiliser une interface c'est d'abord se munir :
- d'une Interface décrivant les méthodes que l'on veut
utiliser,
- d'une Structure décrivant les pointeurs d'adresses des fonctions
correspondantes,
- d'une table des méthodes: variable initialisée issue
de cette structure.
L'étape 1, consistant à d'écrire l'Interface d'un
objet, n'est pas compliquée. Il suffit de nommer les méthodes.
Les étapes 2 et 3 sont liées. Or dans notre approche objet,
nous disposons déjà de la Structure adaptée: c'est
celle qui décrit la Classe d'un objet.
En effet, l'Interface et la Classe d'un objet se ressemblent: Tous deux
comportent des pointeurs de fonctions.
Simplement, l'instruction Interface ne contient pas les attributs de la
Classe mais seulement tout ou partie des méthodes de la Classe.
Il est donc tout à fait possible de se servir de la Classe d'un
objet pour initialiser l'interface. Cette démarche est d'ailleurs
des plus naturelles. Rappelons que l'interface est la partie visible de
la Classe d'un objet, il est donc normal que l'interface soit déterminée
par la Classe.
Voyons comment procéder.
Reprenons la Classe Rectangle2 munie des deux méthodes : Dessiner()
et Effacer()
Sa Classe est la suivante
Structure Rectangle2
*Dessiner
*Effacer
x1.l
x2.l
y1.l
y2.l
EndStructure
Procedure Dessiner_Rectangle(*this.Rectangle2)
EndProcedure
Procedure Effacer_Rectangle(*this.Rectangle2)
EndProcedure
|
Définissons maintenant l'interface suivante:
Interface Rectangle
Dessiner()
Effacer()
EndInterface
|
Comme on veut obliger l'utilisateur à passer par l'Interface,
il n'est plus question de créer un objet directement à partir
de la Classe Rectangle2.
L'objet sera donc créée en écrivant :
au lieu de Rect.Rectangle2
Cependant, il ne faut pas oublier de relier l'Interface à la Classe.
Pour cela il faut initialiser l'objet Rect et il est conseillé
de le faire lors de la déclaration de l'objet.
Correction faite, la bonne instruction pour déclarer l'objet via
l'interface est la suivante :
Rect.Rectangle = New_Rect(0,
10, 0, 20)
|
New_Rect() est une fonction qui
réalise l'opération d'initialisation.
Ce que l'on sait déjà d'elle, c'est qu'elle retourne comme
valeur l'adresse mémoire contenant les adresses des fonctions utilisées
par l'interface.
Voici maintenant le corps de la fonction New_Rect()
Procedure New_Rect(x1.l,
x2.l, y1.l, y2.l)
*Rect.Rectangle2 = AllocateMemory(SizeOf(Rectangle2))
*Rect\Dessiner = @Dessiner_Rectangle()
*Rect\Effacer = @Effacer_Rectangle()
*Rect\x1 = x1
*Rect\x2 = x2
*Rect\y1 = y1
*Rect\y2 = y2
ProcedureReturn *Rect
EndProcedure
|
Cette fonction alloue une zone mémoire de la taille de la Classe
de l'objet.
Elle initialise ensuite les méthodes puis les attributs de l'objet.
Elle se termine en retournant l'adresse de cette zone mémoire.
Comme on trouve au début de cette zone mémoire d'abord les
adresses des fonctions Dessiner_Rectangle()
et Effacer_Rectangle(), on initialise
effectivement l'interface.
Pour accéder aux méthodes de l'objet Rect, il suffit d'écrire
:
Rect\Dessiner()
Rect\Effacer()
|
On vérifie bien que :
- la Classe Rectangle2 permet d'initialiser l'interface de l'objet.
- Rect, déclaré via l'interface, est un objet de la Classe
Rectangle2 pouvant utiliser les méthodes Dessiner()
et Effacer()
Nous avons donc réalisé, via l'Interface et la fonction
New_Rect(), l'instanciation d'un
objet Rect de la Classe Rectangle2.
La fonction New_Rect() est appelée
Constructeur de l'objet de Classe Rectangle2.
|
Toutes les implémentations des Méthodes
(blocs Procedure/EndProcedure) doivent comporter comme premier argument
le pointeur *this de l'objet sur lequel on doit appliquer la fonction.
A l'opposé, l'argument *this ne doit plus apparaît au
niveau de l'Interface. En effet, comme l'instruction nous permet d'écrire
Rect\Dessiner(), on sait que la méthode Dessiner() est celle
de l'objet Rect: Il n'y a pas d'ambiguïté. Tout se passe
comme si l'objet Rect était "conscient" de son état. |
|
Le constructeur pourrait recevoir comme paramètres
supplémentaires, les adresses des fonctions implémentant
les méthodes. Il n'en est rien ici car on connait l'implémentation
des méthodes: c'est celle de la classe. Par contre on ne connait
pas l'état initial que l'utilisateur veut donner à l'objet.
Il se peut donc que le constructeur comporte des paramètres
pour l'initialisation des attributs.
C'est le cas ici pour New_Rect() demandant en entrée les coordonnées
(x1,y1) et (x2,y2) des points diamétralement opposés
du rectangle. |
Initialisation d'Objet
Nous avons vu que le constructeur, après avoir alloué la
place mémoire nécessaire à l'objet, initialise les
différents membres de l'objet (méthodes et attributs).
On peut isoler cette opération dans une procédure à
part, que le constructeur appellera.
Cette précaution permet de distinguer l'allocation mémoire
de l'initialisation de l'objet. Ceci sera très utile pour mener
à bien par la suite le concept d'héritage, car une seule
allocation de mémoire suffit, mais plusieurs initialisations seront
nécessaires.
Cependant nous séparerons l'initialisation des méthodes
et celle des attributs.
En effet, l'implémentation des méthodes dépend de
la classe, alors que l'initialisation des attributs dépend de l'objet
lui-même (voir remarque précédente ).
Dans notre exemple, nous écrirons les deux procédures suivantes
:
Procedure Init_Mthds_Rect(*Rect.Rectangle2)
*Rect\Dessiner = @Dessiner_Rectangle()
*Rect\Effacer = @Effacer_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
|
et le Constructeur devient:
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
|
Destructeur d'Objet
On associe toujours à un constructeur d'objet, son opposé
: le destructeur d'objet.
Lors de la construction d'un objet, une zone mémoire a était
allouée pour stocker les définitions des méthodes
et des attributs.
Quant un objet n'est plus utile, il ne faut pas oublier de le détruire
pour libérer la mémoire.
Ce processus se fait en utilisant une fonction appelée Destructeur
d'objet.
Dans notre exemple d'objet de la Classe Rectangle2, le destructeur d'objet
s'écrira :
Procedure Free_Rect(*Rect)
FreeMemory(*Rect)
EndProcedure
|
et s'utilisera, comme suite:
|
On peut voir le Destructeur d'objet comme
une méthode de l'objet. Mais pour éviter d'alourdir l'objet et garder
une homogénéité avec le Constructeur, nous avons préféré le
voir comme une fonction de la Classe. |
|
Détruire un objet par son Destructeur,
signifie que l'on libère la place mémoire contenant
les informations de l'objet mais en aucun cas on ne détruit
l'infrastructure de l'objet.
Aussi, dans notre exemple, après avoir fait:
Free_Rect(Rect2)
on peut toujours réutiliser Rect2 sans préciser à
nouveau son type:
Rect2 = New_Rect(0, 10, 0, 20)
Rect2\Dessiner()
En effet, lorsque l'on réalise l'instanciation d'un objet,
comme suite :
Rect2.Rectangle
on crée un objet Rect2 dont la durée de vie est assujettie
aux mêmes règles que celles des variables car Rect2 est
d'abord une variable : C'est une variable structurée continuant
les pointeurs de fonctions des méthodes de l'objet.(voir aussi
le rappel qui suit) |
|
Petit rappel : La durée de vie d'une variable est liée
à la durée de vie de la partie du programme où
elle est déclarée :
- Si la variable est déclarée à l'intérieur
d'une fonction, sa durée de vie sera liée à
celle de la fonction, c'est à dire égale au temps
d'utilisation de la fonction.
- Si la variable est déclarée en dehors de toute
fonction, c'est à dire dans le corps principal du programme,
sa durée de vie est liée à celle du programme
|
Allocations Mémoire
A chaque nouvelle instanciation, le constructeur doit allouer dynamiquement
une place mémoire de la taille des informations décrivant
l'objet.
Pour cela, le Constructeur peut utiliser la commande AllocateMemory(), associée
à la commande FreeMemory() pour le Destructeur.
Mais cela peut être une toute autre commande d'allocation dynamique
de mémoire.
Sous OS Windows, on peut passer directement par les API par exemple.
En standard, PureBasic propose les listes chaînées qui permettent
aussi d'allouer dynamiquement de la mémoire.
Encapsulation
Imaginons maintenant que l'on ne veuille donner à l'utilisateur
seulement accès à la méthode Dessiner()
de la Classe Rectangle2. On commencera par définir l'interface
désirée :
Interface Rectangle
Dessiner()
EndInterface
|
L'instanciation du nouvel objet reste la même:
Rect.Rectangle = New_Rect()
|
avec
Procedure Init_Mthds_Rect(*Rect.Rectangle2)
*Rect\Dessiner = @Dessiner_Rectangle()
*Rect\Effacer = @Effacer_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
|
car en effet, la première adresse de fonction est bien celle de
la méthode Dessiner().
Maintenant, imaginons que l'on veuille donner à l'utilisateur
seulement accès à la méthode Effacer().
On commencera par définir l'interface suivante:
Interface Rectangle
Effacer()
EndInterface
|
Par contre l'instanciation du nouvel objet ne peut utiliser le constructeur
New_Rect().
Dans le cas contraire, le résultat serait identique au cas précédent.
Il faut donc créer un nouveau constructeur capable de retourner
l'adresse de fonction adaptée.
En voici un :
Procedure Init_Mthds_Rect2(*Rect.Rectangle2)
*Rect\Dessiner = @Effacer_Rectangle()
*Rect\Effacer = @Dessiner_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
|
Vous constatez qu'il a suffit d'inverser les adresses de fonction dans
l'initialisation des méthodes de la Classe.
Certes, ce n'est pas très élégant de devoir affecter
au champ Dessin de la Structure Rectangle2 l'adresse d'une toute autre
fonction.
Si cela permet de conserver la même Structure, celle de la Classe,
cela souligne aussi une chose :
Les noms des pointeurs de fonctions nous intéressent moins que
leurs valeurs !
Pour gommer ce faux problème, il suffit de renommer les pointeurs
de la Classe comme suite :
Structure Rectangle2
*Methode1
*Methode2
x1.l
x2.l
y1.l
y2.l
EndStructure
|
C'est l'Interface et le Constructeur qui donnent un sens à ces
pointeurs :
- en leur donnant un nom (rôle de l'interface)
- en leur affectant les adresses de fonctions adéquates (rôle
du constructeur)
|
Malgré cette disposition concernent
les noms des pointeurs de fonction, il reste bien plus pratique de
conserver un nom explicite si l'on ne compte pas cacher les méthodes
(ce qui est le plus courant). Cela permet de faire évoluer
une Classe Mère sans retoucher à la numérotation
des pointeurs des Classes Filles. |
Héritage
Comme lors de notre première implémentation du concept d'héritage,
nous allons profiter de la qualité qu'ont à la fois les
instructions Structure et Interface d'être étendues grâce
au mot-clé Extends.
Ainsi, nous passerons de la Classe Rectangle1 possédant une seule
méthode Dessiner()
Interface
|
Interface
Rect1
Dessiner()
EndInterface
|
Classe |
Structure Rectangle1
*Methode1
x1.l
x2.l
y1.l
y2.l
EndStructure
Procedure Dessiner_Rectangle(*this.Rectangle1)
EndProcedure
Procedure
Init_Mthds_Rect1(*Rect.Rectangle1)
*Rect\Methode1 = @Dessiner_Rectangle()
EndProcedure
|
Constructeur |
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
|
à la Classe Rectangle2, possédant 2 méthodes
: Dessiner() et Effacer()
en écrivant :
Interface
|
Interface
Rect2 Extends Rect1
Effacer()
EndInterface
|
Classe |
Structure Rectangle2
Extends Rectangle1
*Methode2
EndStructure
Procedure Effacer_Rectangle(*this.Rectangle2)
EndProcedure
Procedure
Init_Mthds_Rect2(*Rect.Rectangle2)
Init_Mthds_Rect1(*Rect)
*Rect\Methode2 = @Effacer_Rectangle()
EndProcedure
|
Constructeur |
Procedure
Init_Mbers_Rect2(*Rect.Rectangle2,
x1.l, x2.l, y1.l, y2.l)
Init_Mbers_Rect1(*Rect, x1,
x2, y1, y2)
EndProcedure
Procedure New_Rect2(x1.l,
x2.l, y1.l, y2.l)
*Rect = AllocateMemory(SizeOf(Rectangle2))
Init_Mthds_Rect2(*Rect)
Init_Mbers_Rect2(*Rect, x1,
x2, y1, y2)
ProcedureReturn *Rect
EndProcedur
|
Accomplir un héritage consiste non seulement à étendre
l'Interface et la Classe mais aussi à adapter l'initialisation
des méthodes et des attributs.
Les deux procédures Init_Mthds_Rect2()
et Init_Mbers_Rect2() font appel
respectivement à l'initialisation des méthodes et à
l'initialisation des attributs de la Classe Rectangle1 ( Init_Mthds_Rect1()
et Init_Mbers_Rect1()
) et non au constructeur New_Rect1().
En effet, il n'est pas question d'instancier un objet de la Classe Mère
(Rectangle1) pour construire un objet de la Classe Fille (Rectangle2).
Il est question simplement d'hériter des méthodes et des
attributs, ce à quoi contribue l'emploi des initialisations de
la Classe Mère dans la Classe Fille.
D'autre part, on vérifie bien qu'en modifiant la Classe Mère
(en ajoutant une méthode ou une variable), la Classe Fille bénéficie
instantanément des changements.
L'héritage est-il pour autant correct? Non, car dans l'état
actuel, il ne permet pas à l'objet de la Classe Fille (Rectangle2)
d'utiliser la nouvelle méthode Effacer()
!
Ceci tout simplement car le pointeur de fonction *Methode2 ne se trouve
pas directement à la suite du pointeur de fonction *Methode1.
Si on explicite la Structure de la Classe Rectangle2, on a :
Structure Rectangle2
*Methode1
x1.l
x2.l
y1.l
y2.l
*Methode2
EndStructure
|
au lieu de disposer de la Structure ci-dessous, autorisant une initialisation
correcte de l'interface:
Structure Rectangle2
*Methode1
*Methode2
x1.l
x2.l
y1.l
y2.l
EndStructure
|
Rappelez-vous qu'il faut des adresses de fonction qui se suivent et qui
soient ordonnées à l'image de l'Interface ().
On résout ce problème en regroupant dans une structure spécifique
les méthodes entre-elles !
Il suffit alors que la Structure de la Classe garde un pointeur sur cette
nouvelle structure comme le montre l'exemple suivant :
Interface
|
Interface
Rect1
Dessiner()
EndInterface
|
Classe |
Structure Rectangle1
*Methodes
x1.l
x2.l
y1.l
y2.l
EndStructure
Procedure Dessiner_Rectangle(*this.Rectangle1)
EndProcedure
Structure Mthds_Rect1
*Methode1
EndStructure
Procedure
Init_Mthds_Rect1(*Mthds.Mthds_Rect1)
*Mthds\Methode1 = @Dessiner_Rectangle()
EndProcedure
Mthds_Rect1. Mthds_Rect1
Init_Mthds_Rect1(@Mthds_Rect1)
|
Constructeur |
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\Methodes = @Mthds_Rect1
Init_Mbers_Rect1(*Rect, x1,
x2, y1, y3)
ProcedureReturn *Rect
EndProcedure
|
La structure Mthds_Rect1 décrit tous les pointeurs de fonction
des méthodes de la Classe.
S'en suit la déclaration de la variable Mthds_Rect1 de type Mthds_Rect1
ainsi que son initialisation grace à Init_Mthds_Rect1().
La variable Mthds_Rect1 est appelée la table des méthodes
de la class car elle contient l'ensemble des adresses des méthodes
de la class.
Cet ensemble constitue la description complète des méthodes
de la Classe.
La structure Rectangle1, comporte maintenant un pointeur *Methodes, initialisé
par le constructeur en donnant l'adresse de la variable Mthds_Rect1.
|
L'expression
Mthds_Rect1.Mthds_Rect1
Init_Mthds_Rect1(@Mthds_Rect1)
peut etre condensée en
Init_Mthds_Rect1(@Mthds_Rect1.Mthds_Rect1)
|
L'héritage est alors possible car en étendant la Structure
Methd_Rect1 en une nouvelle Methd_Rect2, les adresses de fonction vont
se suivre:
Interface
|
Interface
Rect2 Extends Rect1
Effacer()
EndInterface
|
Classe |
Structure Rectangle2
Extends Rectangle1
EndStructure
Procedure Effacer_Rectangle(*this.Rectangle2)
EndProcedure
Structure Mthds_Rect2
Extends Mthds_Rect1
*Methode2
EndStructure
Procedure
Init_Mthds_Rect2(*Mthds.Mthds_Rect2)
Init_Mthds_Rect1(*Mthds)
*Mthds\Methode2 = @Effacer_Rectangle()
EndProcedure
Mthds_Rect2. Mthds_Rect2
Init_Mthds_Rect2(@Mthds_Rect2)
|
Constructeur |
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\Methodes = @Mthds_Rect2
Init_Mbers_Rect2(*Rect, x1,
x2, y1, y2)
ProcedureReturn *Rect
EndProcedure
|
Dans cet exemple, la Structure Rectangle2 est vide, ce qui n'est pas
gênant en soit.
Deux raisons à cela :
- D'abord le pointeur *Methodes n'a besoin d'exister qu'une seule fois
et ceci dans la Classe Mère.
- Ensuite, nous n'avons pas souhaitez ajouter d'attributs supplémentaires,
auquel cas elle aurait dû les contenir.
|
Le fait d'avoir externalisé l'initialisation des méthodes
hors du constructeur combiné à des pointeurs de fonctions
disponiblent dans une variable fixe a trois avantages:
- Les pointeurs de fonction des méthodes de la Classe sont
initialisés une fois pour toute et non plus à chaque
instanciation d'un objet
- Les objets instanciés ne disposent plus que d'un pointeur
vers les pointeurs des méthodes: le gain en place mémoire
est substantiel.
- Comme tous les objets pointent vers les mêmes pointeurs
de fonction, cela garantit un comportement identique des objets
de même Classe.
|
Accesseur et Modifieur
d'Objet
En passant par l'Interface, il n'est possible de manipuler que des méthodes
de l'objet.
L'interface encapsule donc entièrement les attributs des objets,
c'est à dire qu'elle les cache.
Pour accéder aux attributs, soit pour les lires, soit pour les
modifier, il faut donc disposer de méthodes spécifiques
et les mettre à disposition de l'utilisateur.
Les méthodes qui permettent de lire les attributs d'un objet sont
appelées les Accesseur de l'objet.
Les méthodes qui permettent de modifier les attributs d'un objet
sont appelées les Modifieurs de l'objet.
Dans notre exemple de Classe Rectangle1, si nous voulons lire la valeur
de l'attribut var2, on créera l'Accesseur suivant:
Procedure Get_var2(*this.Rectangle1)
ProcedureReturn *this\var2
EndProcedure
|
De même, pour modifier la valeur de l'attribut var2, on écrira
le Modifieur suivant
Procedure Set_var2(*this.Rectangle1,
valeur)
*this\var2 = valeur
EndProcedure
|
Comme les Accesseurs et les Modifieurs n'existent que pour permettre
à l'utilisateur de modifier tout ou partie des attributs d'un objet,
ils sont obligatoirement présents dans l'interface.
|
Voir l'annexe Optimisation
du tutorial pour étudier de quelle manière on peut
optimiser les performances des accesseurs et des modifieurs lors
de l'exécution
|
Retour en haut de page
|
|
|