Dernière version sur https://formation-perl.fr/guide-perl.html
© 2002-2024 Sylvain Lhullier - Permission est accordée de copier et distribuer ce document sans modification
et à condition de fournir un lien vers la page https://formation-perl.fr/guide-perl.html
Envie d'une formation Perl ?
Sylvain Lhullier propose des formations au langage Perl,
contactez-le.
La programmation objet est le concept nouveau de ces vingt dernières années. C++ est bâti sur le C et apporte l'objet. Java a été mis au point (entre autres) pour passer outre les nombreux pièges et problèmes de C++. Ruby est un langage interprété basé sur ce concept objet.
Perl, qui utilisait un garbage collector bien avant que Java n'existe, ne pouvait pas être en reste et la communauté Perl a rapidement proposé les extensions du langage nécessaires à ce type de programmation.
On notera que ces extensions sont peu nombreuses, car l'idée a été de réutiliser au maximum ce qui existait déjà en Perl et de l'appliquer à la programmation objet. Le C++ étant compilé et devant rester compatible avec le C, cela fut un challenge de mettre sur pied ce langage ; cela explique sans doute pourquoi C++ est si complexe et comporte tant de pièges. Perl, de par sa nature interprétée, n'a pas posé de problème pour s'étendre à l'objet.
La programmation par objets ouvre le champ des possibilités offertes au programmeur ; alliée à un langage puissant et flexible comme Perl, elle offre la souplesse, la richesse et la facilité d'écriture qu'il manque aux langages uniquement objet. Toutefois, de par sa nature permissive, le langage Perl ne saurait être aussi strict que des langages exclusivement objet. Le programmeur est invité à faire les choses proprement, mais rien ne l'y oblige.
Sans revenir sur la théorie de la programmation objet, je vais tenter ici d'y faire une courte introduction. La programmation orientée objet est un type de programmation qui se concentre principalement sur les données. La question qui se pose en programmation OO (orientée objet) est "quelles sont les données du problème ?" à l'instar de la programmation procédurale par exemple, qui pose la question « quelles sont les fonctions/actions à faire ? ». En programmation OO, on parle ainsi d'objets, auxquels on peut affecter des variables/attributs (propriétés) et des fonctions/actions (méthodes).
On parle de « classe », qui est une manière de représenter des données et comporte des traitements : une classe « Chaussure » décrit, par exemple, les caractéristiques d'une chaussure. Elle contient un champ décrivant la pointure, la couleur, la matière, etc. Une telle classe comporte de plus des traitements sur ces données ; ces traitements sont appelés « méthodes ». Grossièrement une méthode est une fonction appliquée à un objet.
Une fois définie une telle classe, il est possible d'en construire des instances : une instance d'une classe est dite être un objet de cette classe. Dans notre exemple, il s'agirait d'une chaussure dont la pointure, la couleur et la matière sont renseignées.
Nous allons maintenant voir comment écrire une classe en Perl. Vous verrez, cela est très simple et démystifie la programmation objet.
En Perl, une classe n'est autre qu'un module et un objet (instance de cette classe) n'est autre qu'une référence associée à cette classe. Dans le constructeur, nous allons donc créer une référence (typiquement vers une table de hachage) et nous allons l'associer au package en question ; lors de cette association, on dit en Perl que l'on bénit (bless en anglais) la référence.
Les champs de l'objet seront en fait stockés dans cette table de hachage, sous forme de la clef pour le nom du champ et de la valeur pour la valeur du champ.
Voyons un exemple : définissons une classe Vehicule
qui comporte deux champs : un nombre de roues et une couleur.
Nous définissons un package Vehicule
dans un fichier
Vehicule.pm
comme nous le faisons pour tout module.
1: # --- fichier Vehicule.pm --- 2: package Vehicule; 3: use strict; 4: use warnings; 5: sub new { 6: my ($class,$nbRoues,$couleur) = @_; 7: my $this = {}; 8: bless($this, $class); 9: $this->{NB_ROUES} = $nbRoues; 10: $this->{COULEUR} = $couleur; 11: return $this; 12: } 13: 1; # À ne pas oublier...
La ligne numéro 2 indique le nom du package actuel ; il est conseillé de
choisir le même nom que pour le fichier, simplement pour des raisons d'organisation
et de maintenance. La ligne 13 comporte le fameux code de retour
du chargement du module. La ligne 3 force une syntaxe plus rigoureuse.
En Perl, le nom des modules et donc des classes que le programmeur
définit doit être composé de majuscules et de minuscules, avec typiquement
une majuscule au début de chaque mot ; les noms de package exclusivement
en minuscules sont réservés pour les modules pragmatiques de Perl
(dits modules pragma comme strict
,
etc.), les noms exclusivement en majuscules sont inélégants. :-)
Nous définissons une fonction new
(ligne 5) dont le but
est de construire un objet Vehicule
. Il s'agit
donc d'un constructeur ; un constructeur en Perl est une simple
fonction renvoyant un objet. Il est bon de noter que le choix du nom
pour cette fonction est totalement libre ; il est courant de l'appeler
new
, mais rien ne nous y oblige. On pourrait par exemple
choisir le nom de la classe (si on est un habitué de C++ ou de Java),
mais on verra par la suite que cela n'est pas forcément une bonne idée.
Cette fonction prend en premier paramètre le nom de la classe ; cela semble superflu, mais on verra qu'il n'en est rien. Les paramètres suivants sont laissés à la discrétion du programmeur : bien souvent on passe ici les valeurs de champs pour l'initialisation. C'est ce que l'on fait ici : le nombre de roues et la couleur sont transmis (ligne 6).
Ligne 7, nous créons une référence anonyme vers une table de hachage vide
{}
. Cette référence est stockée
dans une variable scalaire nommée $this
, car il va
s'agir de notre futur objet. Le nom de cette variable est totalement arbitraire
et j'ai choisi de prendre le nom $this
car il rappelle
les variables this
de C++ et de Java. Mais comprenez bien
qu'il n'y a rien d'obligatoire dans cette appellation. Il est d'ailleurs fréquent
que le nom de l'objet dans les méthodes soit plutôt $self
en Perl (vous trouverez ce nom dans la plupart des modules objet de CPAN).
Ligne 8, nous indiquons que cette référence est liée au package (à la
classe) $class
. Cette variable $class
vaudra ici Vehicule
. L'opérateur bless
associe le package en question à la référence. La référence est maintenant
liée au package.
Dans les lignes 9 et 10, les champs NB_ROUES
et
COULEUR
sont initialisés. Le champ d'un objet
n'est rien d'autre qu'une entrée dans la table de hachage qui constitue
l'objet. Pour affecter un champ de l'objet que nous sommes en train
de construire, il suffit de créer un couple clef/valeur dans la table
de hachage référencée par $this
. J'ai pris l'habitude
de mettre le nom des champs en lettres majuscules.
Notez que le nombre, le nom et le contenu des champs peuvent donc varier d'une instance de la classe à une autre instance de cette même classe. Libre au programmeur de faire ce qu'il veut : si le but est de vraiment programmer objet de façon formelle, il va respecter les habitudes de ce type de programmation qui veut que toutes les instances d'une même classe aient les mêmes champs ; mais s'il ne tient pas à respecter ces contraintes, il est libre de faire ce qu'il veut de chacun de ses objets.
La ligne 11 consiste en un return
de la référence
vers la table de hachage ainsi construite.
Pour faire usage de cette classe, nous allons devoir disposer d'un script
écrit dans un autre fichier (par exemple script.pl
) :
#!/usr/bin/perl use strict; use warnings;
Comme pour tout module, nous devons explicitement indiquer que nous allons l'utiliser :
use Vehicule;
Nous pouvons maintenant utiliser le constructeur que nous avons défini :
my $v = Vehicule->new( 2, "bleu" ); my $v2 = Vehicule->new( 4, "rouge" );
Nous venons ici de créer deux instances de la classe Vehicule
.
On dit que ces deux variables $v
et $v2
sont des Vehicule
. Ces deux objets sont donc indépendants
l'un de l'autre ; ils sont de la même classe Vehicule
,
mais en constituent des instances autonomes, la modification de l'un ne
modifiant pas l'autre. Voici un schéma de l'état de notre mémoire :
Cette syntaxe Vehicule->new
correspond à l'appel
du constructeur new
que nous venons d'écrire.
La variable $v
est initialisée à la valeur de retour
de cette fonction. Elle est donc une référence vers une table de hachage
dont deux champs sont initialisés et qui a été bénie (bless
)
en Vehicule
. Idem pour $v2
.
Il est aussi possible de faire usage de la syntaxe suivante :
my $v = new Vehicule( 2, "bleu" );
Cette formulation va sans doute rassurer les habitués de Java ou de C++,
mais peut induire le programmeur en erreur. En effet, cette dernière syntaxe
semble indiquer que new
est un opérateur spécifique
pour appeler un constructeur et est donc un mot réservé du langage.
Il n'en est rien ; comme on le verra un peu plus loin, ce nom
est totalement arbitraire.
Revenons à notre exemple. En plus de savoir qu'elle pointe vers une table
de hachage, la référence $v
sait de quelle classe elle est.
En effet, si nous l'affichons :
print "$v\n";
Nous obtenons la chose suivante à l'écran :
Vehicule=HASH(0x80f606c)
Je vous rappelle que dans le cas de l'affichage d'une référence vers une table de hachage non bénie, nous obtenons quelque chose de la forme :
HASH(0x80fef74)
Un objet (à partir de maintenant, nommons ainsi une référence vers une table de hachage bénie) sait donc de quelle classe il est. Cela va lui permettre de choisir le bon package quand on appellera une méthode sur cet objet (lire la suite).
Voyons maintenant ce que donne le module Data::Dumper
(dont j'ai déjà parlé) sur une telle référence :
use Data::Dumper; print Dumper($v)."\n";
L'affichage suivant est effectué :
$VAR1 = bless( { 'COULEUR' => 'bleu', 'NB_ROUES' => 2 }, 'Vehicule' );
On remarquera que la tradition de Data::Dumper
qui
consiste en ce que la chaîne renvoyée est directement intégrable dans
un code Perl est respectée : même l'opération de bénédiction
(bless
) est présente.
Il faut bien voir que $v
est une référence vers un
objet. Autrement dit, si on fait une copie de cette variable
my $w = $v;
nous obtenons deux variables qui pointent vers le même objet.
La modification de l'objet pointé par l'un modifiera celui pointé par l'autre, car il s'agit du même objet.
Comment écrire plusieurs constructeurs pour une même classe ?
Rien de plus simple. Le nom new
que j'ai choisi
pour le constructeur n'a rien d'obligatoire, en fait un constructeur
est une simple fonction qui renvoie une référence bénie. Seule la syntaxe
d'appel change par rapport à ce que l'on a vu
pour les modules. Je peux donc écrire un autre constructeur (donc avec
un autre nom) :
sub nouveau { my ($class,$couleur) = @_; my $this = {}; bless($this, $class); $this->{COULEUR} = $couleur; return $this; }
Ce constructeur de Vehicule
prend par exemple
un seul paramètre (la couleur) et n'affecte pas de champs
NB_ROUES
, car rien de ne l'y oblige.
À moi de savoir comment je veux gérer mes véhicules, à savoir comment
j'autorise qu'ils soient construits.
Pour être propre et cohérent, nous allons tout de même considérer
qu'il est sage d'affecter une valeur à la clef NB_ROUES
:
sub nouveau { my ($class,$couleur) = @_; my $this = {}; bless($this, $class); $this->{NB_ROUES} = 0; $this->{COULEUR} = $couleur; return $this; }
Voici comment on va faire appel à ce constructeur :
my $v2 = Vehicule->nouveau( "bleu" );
De la même façon que pour le précédent constructeur, il est possible d'utiliser la syntaxe suivante :
my $v2 = nouveau Vehicule( "bleu" );
Ce qui est, reconnaissons-le, quelque peu déroutant. C'est pour cela
que je vous conseille d'utiliser plutôt la première syntaxe
Vehicule->nouveau()
ou alors de vous en tenir
à un constructeur new
pour écrire
new Vehicule()
.
Une méthode est une fonction qui s'applique à une instance de la classe. Cela est vrai dans tous les langages objet, mais bien souvent cela est masqué ; dans notre cas rien de masqué, le premier paramètre de la fonction sera l'objet (la référence bénie) :
sub roule { my ($this,$vitesse) = @_; print "Avec $this->{NB_ROUES} roues, je roule à $vitesse.\n"; }
Cette fonction déclarée dans le fichier Vehicule.pm
a donc pour premier paramètre l'objet sur lequel elle est appelée.
Vous noterez une fois de plus que rien n'oblige le programmeur à nommer
cette variable $this
; la seule contrainte est sa première
place parmi les arguments.
Cette méthode peut dès maintenant être appelée depuis le fichier
script.pl
sur les objets de type Vehicule
.
Il n'y pas de nécessité de faire usage du lourd mécanisme
d'Exporter
, car nous n'avons pas besoin de modifier
l'espace de nom des fonctions des scripts appelants.
Pour appeler la méthode, il suffit d'écrire dans le fichier
script.pl
:
$v->roule( 15 );
La fonction appelée sera celle qui a pour nom roule
définie dans le package lié à la référence $v
lors
de sa bénédiction.
L'affichage suivant a donc lieu :
Avec 2 roues, je roule à 15.
Écrivons par exemple une méthode d'affichage :
sub toString { my ($this) = @_; return "(Vehicule:$this->{NB_ROUES},$this->{COULEUR})"; }
Cette méthode renvoie une chaîne de caractères, représentation de l'objet. Les habitués de Java noteront que j'ai choisi le nom de cette fonction pour leur rappeler des souvenirs, mais qu'il n'a rien de spécial. Voici comment l'utiliser dans le script :
print $v->toString()."\n";
Et l'affichage a lieu :
(Vehicule:2,bleu)
Libre à vous de choisir un plus bel affichage.
Un champ est une donnée propre à une instance de classe. En Perl, ces champs
sont stockés comme clef/valeur dans la table de hachage qui constitue
l'objet (si on utilise une table de hachage comme objet, ce qui est souvent
le cas). Nos véhicules comportent deux champs :
NB_ROUES
et COULEUR
.
Ces champs étant de simples clef/valeur d'une table de hachage dont
on dispose d'une référence dans le script, ils y sont accessibles.
Dans le script, nous pouvons écrire dans le fichier
script.pl
:
foreach my $k (keys %$v) { print "$k : $v->{$k}\n"; }
C'est-à-dire que je peux accéder sans restriction à l'ensemble des champs de l'objet. En effet, j'ai en main une référence vers une table de hachage ; le fait qu'elle soit bénie ne change rien au fait que je peux la déréférencer et accéder aux diverses valeurs qu'elle contient. Je peux aussi bien modifier ces valeurs ou même en ajouter ou en supprimer, car il s'agit en effet d'une table de hachage comme les autres.
« Comment protéger les données d'un objet ? », allez-vous alors me demander. Eh bien, Perl n'a rien prévu pour ça. Cela va sans doute faire hurler les puristes de la programmation objet, mais c'est comme cela... Perl vous propose les principaux mécanismes pour faire de la programmation objet tout en restant cohérent avec le reste du langage, certaines choses ne sont donc pas possibles.
Faute de champs privés en Perl, il existe une convention qui dit que les
champs dont la clef commence par un underscore (souligné _
)
sont des champs privés et les scripts qui utilisent les objets ainsi faits
sont priés de respecter cette convention. C'est le cas de beaucoup de modules
CPAN. Cette convention est aussi valable pour les méthodes (une méthode dont
le nom commence par un underscore est une méthode privée).
De façon générale, un programmeur Perl est quelqu'un de bonne éducation
(sinon il programmerait en Java ;-))
)
et il ne modifiera pas une instance
d'une classe qu'il n'a pas écrite. En effet, pourquoi modifier un objet
Net::FTP
alors qu'il fait très bien son travail ?
De toute façon, il a forcément accès au code source de cette classe et
s'il veut la modifier, il peut en faire une copie et la modifier !
La protection des données est donc plus une nécessité sociale (manque de
confiance en l'espèce humaine à laquelle les développeurs prétendent encore
faire partie :-))
) qu'une nécessité technique. Pas dramatique pour
faire de l'objet.
Prenons le temps de faire un petit exemple pratique pour illustrer le concept de composition. La composition est le fait qu'un objet est constitué d'autres objets. Par exemple un garage comporte des véhicules.
Je décide qu'un garage aura une taille limite : il s'agira du nombre maximal de véhicules qu'il pourra contenir. Un garage devra donc contenir une liste de véhicules.
Nous devons créer un fichier Garage.pm
contenant :
package Garage; use strict; use warnings; # ... ici les méthodes qui vont suivre 1;
Voyons ensuite le constructeur :
sub new { my ($class,$places) = @_; my $this = {}; bless($this, $class); $this->{PLACES} = $places; $this->{VEHICULE} = []; return $this; }
Ce constructeur prendra en paramètre le nombre de places disponibles
du garage ; cette valeur est enregistrée dans le champ
PLACES
. Le champ VEHICULE
comportera la liste des véhicules ; pour cela elle est initialisée
à la référence anonyme vers une liste vide. La méthode
ajoute
se chargera d'ajouter les véhicules
dans la limite du nombre de places :
sub ajoute { my ($this,$vehicule) = @_; if( @{$this->{VEHICULE}} < $this->{PLACES} ) { push @{$this->{VEHICULE}}, $vehicule; return 1; } return 0; }
Cette méthode prend en paramètre une référence vers un véhicule.
Elle compare la longueur de la liste des véhicules au nombre total
de places (l'expression @{$this->{VEHICULE}}
est la liste pointée par la référence $this->{VEHICULE}
;
évaluée en contexte numérique, elle vaut son nombre d'éléments).
S'il reste de la place, le véhicule est ajouté
(fonction push
) et elle renvoie vrai, sinon
elle renvoie faux.
Voici comment l'utiliser dans le script :
use Garage; my $g = Garage->new(3); my $v = new Vehicule( 2, "bleu" ); $g->ajoute( $v ) or die("ajoute: plus de place"); $g->ajoute( Vehicule->new( 4, "vert" ) ) or die("ajoute: plus de place"); $g->ajoute( Vehicule->new( 1, "jaune" ) ) or die("ajoute: plus de place");
Écrivons maintenant une méthode d'affichage pour un tel objet :
sub toString { my ($this) = @_; my $s = "{Garage:$this->{PLACES},"; foreach my $v ( @{$this->{VEHICULE}} ) { $s .= $v->toString(); } return $s."}"; }
On appelle sur chaque objet Vehicule
la méthode
toString
de façon à le faire apparaître dans la
chaîne que nous allons renvoyer. Voici comment appeler cette méthode
dans le script :
print $g->toString()."\n";
Ce qui donne l'affichage suivant :
{Garage:3,(Vehicule:2,bleu)(Vehicule:4,vert)(Vehicule:1,jaune)}
On pourrait écrire cette méthode différemment, si on voulait séparer chaque véhicule par une virgule :
sub toString { my ($this) = @_; my $s = "{Garage:$this->{PLACES},"; $s .= join( ',', map( { $_->toString() } @{$this->{VEHICULE}} ) ); return $s."}"; }
Ce qui donne l'affichage suivant :
{Garage:3,(Vehicule:2,bleu),(Vehicule:4,vert),(Vehicule:1,jaune)}
Pour ajouter encore une difficulté, je décide de trier les véhicules par nombre de roues croissant :
sub toString { my ($this) = @_; my $s = "{Garage:$this->{PLACES},"; $s .= join( ',', map( {$_->toString()} sort( {$a->{NB_ROUES} <=> $b->{NB_ROUES} } @{$this->{VEHICULE}} ) ) ); return $s."}"; }
Ce qui donne l'affichage suivant :
{Garage:3,(Vehicule:1,jaune),(Vehicule:2,bleu),(Vehicule:4,vert)}
Ces deux derniers exemples vous sont proposés pour que vous vous torturiez un
peu les méninges ;-))
Un objet est détruit dès qu'aucune référence ne pointe vers cet objet.
La ligne suivante, par exemple, libère la place mémoire occupée par
l'objet Vehicule
référencé par $v2
:
$v2 = undef;
À cet instant, Perl se rend compte que l'objet en question n'est plus accessible, la mémoire sera donc automatiquement libérée par le mécanisme du garbage collector.
La même chose a lieu dans le cas de la disparition d'une variable locale :
if( ... ) { my $v3 = Vehicule->new(3,'jaune'); ... ... }
La variable $v3
cesse d'exister à la fermeture de
l'accolade du if
. L'objet qui a été créé dans le bloc
sera donc détruit (sauf si on a fait en sorte qu'une autre variable dont la
visibilité dépasse ce bloc pointe aussi vers l'objet).
Cette libération n'est peut-être pas faite en temps réel, mais ce n'est pas au programmeur de s'en occuper.
Il existe une méthode très spéciale, dont le nom est réservé, qui est appelée
lors de la destruction d'une instance d'un objet. Il s'agit de la méthode
DESTROY
. Cette méthode sera appelée (si elle existe
dans la classe) par le garbage collector juste avant la libération de la
mémoire de l'objet.
sub DESTROY { my ($this) = @_; print "À la casse Vehicule ! "; print "($this->{NB_ROUES} $this->{COULEUR})\n"; }
Cette méthode doit être définie dans le package de l'objet en question et reçoit en premier argument une référence vers l'objet qui va être détruit.
Cette méthode est très utile pour libérer des ressources (fichier, connexion réseau, etc.) qui ont été allouées lors de la création de l'objet.
L'héritage est un des apports de la programmation objet. Perl dispose de tout ce qu'il faut pour le mettre en œuvre.
Imaginons qu'en plus de véhicules, nous avons besoin de manipuler des
vélos et des voitures. Ces objets ont en commun d'avoir un nombre
de roues et une couleur. Ils ont des caractéristiques supplémentaires
qui leur sont propres ; les vélos ont un nombre de vitesses
et les voitures, un nombre de sièges. Ces classes Velo
et Voiture
vont donc hériter de la classe
Vehicule
, c'est-à-dire qu'elles vont comporter tous
les champs et les méthodes de cette classe.
On dit alors que la classe Vehicule
est la classe
mère ; les classes Velo
et Voiture
étant des classes filles. Notez bien que je prends comme exemple
deux classes filles et qu'il n'est nullement nécessaire d'avoir deux
classes filles pour mettre en œuvre l'héritage, une seule est suffisante.
Voyons le cas de la classe Velo
, créons pour cela un
fichier Velo.pm
qui contient :
package Velo; use strict; use warnings; use Vehicule; our @ISA = qw(Vehicule); # ... ici les méthodes qui vont suivre 1;
On signale le lien de filiation entre classes au moyen de la variable
@ISA
positionnée à la valeur d'une liste
contenant le nom de la classe mère. ISA vient de l'anglais "is a",
est un : on dit qu'un vélo est un véhicule. Il hérite donc
des champs et méthodes de la classe Vehicule
.
Définissons-lui un constructeur :
sub new { my ($class,$couleur,$nbVitesses) = @_; my $this = $class->SUPER::new( 2, $couleur ); $this->{NB_VITESSES} = $nbVitesses; return bless($this,$class); }
Ce constructeur prend donc deux paramètres. Il appelle le constructeur
de la classe mère (syntaxe $class->SUPER::new
).
Il ajoute un champ NB_VITESSE
et renvoie
une référence bénie en Velo
.
Notez bien qu'aucun appel au constructeur de la classe mère n'est fait
par défaut, il faut le faire explicitement dans tous les cas.
Le lecteur aura noté que, comme les champs de la classe mère et de la classe fille sont stockés dans la même table de hachage, il n'y a pas de moyen simple pour faire de surcharge ou de masquage des champs. Les noms des champs devront être minutieusement choisis pour ne pas entrer en conflit les uns avec les autres.
Voyons à présent comment écrire une méthode pour cet objet :
sub pedale { my ($this,$ici) = @_; print "Sur mon vélo $this->{COULEUR} "; print "je pédale avec $this->{NB_VITESSES} vitesses"; print " dans $ici.\n"; }
Utilisons maintenant tout cela dans notre script :
use Velo; my $velo = Velo->new('blanc',18); $velo->pedale('les bois'); $velo->roule(10);
Un vélo dispose de la méthode roule
, car il est aussi
un véhicule. L'affichage suivant est effectué :
Sur mon vélo blanc je pédale avec 18 vitesses dans les bois. Avec 2 roues, je roule à 10.
Voyons à présent comment afficher un tel objet. Nous laisserons
le soin à la classe Vehicule
d'afficher le vélo
comme étant un véhicule et nous n'effectuerons dans la classe
Velo
que l'affichage de ce que nous avons ajouté
à la classe mère :
sub toString { my ($this) = @_; my $s = "[Velo:$this->{NB_VITESSES}"; $s .= $this->SUPER::toString(); $s .= "]"; }
La syntaxe $this->SUPER::toString()
correspond
à l'appel de la méthode toString
de la classe
mère. Nous pouvons maintenant l'appeler dans notre script :
print $velo->toString()."\n";
L'affichage suivant est effectué :
[Velo:18(Vehicule:2,blanc)]
Rien de plus simple !
Seule chose pas extrêmement pratique, l'appel au destructeur s'arrête
au premier destructeur rencontré. Si dans notre exemple nous définissions
une méthode DESTROY
pour la classe
Velo
, la méthode DESTROY
de la classe Vehicule
ne sera pas appelée.
Cela peut être gênant si des ressources importantes sont libérées dans
cette méthode ; il faut alors l'appeler explicitement.
Voici un exemple de méthode DESTROY
pour la classe
Velo
:
sub DESTROY { my ($this) = @_; $this->SUPER::DESTROY(); print "Bye bye Velo ! "; print "($this->{NB_VITESSES} $this->{NB_ROUES} ". "$this->{COULEUR})\n"; }
La deuxième ligne de la méthode fait un appel à la méthode de la classe mère.
Pour faire de l'héritage multiple en Perl, rien de plus simple.
Vous aurez peut-être noté que la variable @ISA
est un tableau, elle peut donc contenir plusieurs noms de classe :
package Voiture; our @ISA = qw(Vehicule Danger Pollution);
La détermination de la bonne méthode à appeler est effectuée dynamiquement par une recherche en profondeur dans l'arbre d'héritage. Je ne m'étendrai pas plus sur cette question de l'héritage multiple.
Dans cette partie nous allons voir comment connaître la classe d'un objet ainsi que tester l'appartenance à une classe pour un objet.
Souvenez-vous de l'opérateur ref
qui, appliqué à une
référence, renvoie le type de structure de données vers laquelle elle
pointe (scalaire, tableau, table de hachage, etc.). Appliqué à un objet,
il renvoie la classe de l'objet :
print ref($velo)."\n"; print "Ouf, tout va bien !\n" if( ref($velo) eq "Velo" );
L'affichage effectué est le suivant :
Velo Ouf, tout va bien !
Il s'agit donc de la classe "principale" de l'objet. Sachant qu'un vélo
est aussi un véhicule, il peut être utile de pouvoir tester l'appartenance
de l'objet à une classe plutôt que de connaître sa classe principale.
Par exemple, si nous manipulons un tableau comportant des objets divers
et variés et si nous souhaitons y retrouver tous les objets de classe
Vehicule
pour appeler leur méthode roule
,
nous ne pouvons pas écrire
if( ref($r) eq "Vehicule" ) { ... }
car les vélos ne seront pas sélectionnés. En fait la question que nous devons
ici nous poser n'est pas de savoir si la classe principale de l'objet
est Vehicule
, mais nous devons nous demander si
l'objet est un objet de classe Vehicule
(ce qui est vrai pour tout objet de classe Vehicule
et
ses sous-classes, comme Velo
et Voiture
).
La fonction isa
du package UNIVERSAL
va nous permettre de faire cela :
use UNIVERSAL; if( UNIVERSAL::isa( $r, "Vehicule" ) ) { $r->roule( 40 ); }
On teste ainsi si la variable $r
est un
objet de classe Vehicule
.
Un champ statique est un champ qui est commun à tous les objets d'une classe, c'est-à-dire qu'il n'est pas lié à une instance particulière, mais à une classe. C'est une sorte de variable globale (dont l'accès peut éventuellement être contrôlé) qui est située dans une classe.
Pour faire cela en Perl, nous utiliserons des variables déclarées dans le package de la classe en question.
package Vehicule; my $privateVar = 0; our $publicVar = "hello";
Avec le qualificateur my
, nous déclarons des
variables privées (car visibles uniquement depuis l'intérieur du package).
Avec our
, sont déclarées des variables publiques
(accessibles depuis n'importe quel package).
Je rappelle que pour accéder à la variable $publicVar
du package
Vehicule
, on doit écrire
$Vehicule::publicVar
; depuis les fonctions
et méthodes de ce package, il est suffisant d'écrire $publicVar
(sauf masquage par une variable locale).
On pourrait par exemple compter le nombre de véhicules créés au moyen d'une telle variable :
package Vehicule; my $nbVehicules = 0; sub new { my ($class,$nbRoues,$couleur) = @_; my $this = {}; bless($this, $class); $this->{NB_ROUES} = $nbRoues; $this->{COULEUR} = $couleur; $nbVehicules++; # un véhicule de plus return $this; }
À chaque appel au constructeur de cette classe, la variable
$nbVehicules
sera donc incrémentée.
Maintenant, comment écrire une méthode statique ? Une méthode statique
(ou méthode de classe) est une méthode qui n'est pas appelée sur une
instance de la classe (donc pas de variable $this
),
mais pour toute la classe. Ce n'est ni plus ni moins qu'une brave fonction
présente dans le package. Cette fonction pourra donc uniquement accéder
aux champs statiques de la classe.
Nous pourrions, par exemple, écrire une méthode statique qui renvoie
le nombre de véhicules créés (variable $nbVehicules
) :
sub getNbVehicules { my ($class) = @_; return $nbVehicules; }
On notera que la méthode prend en premier argument le nom de la classe. Cela a pour conséquence que l'appel à la méthode ne se fait pas tout à fait comme pour une fonction d'un package (comme vu pour les modules), mais de la manière suivante :
print Vehicule->getNbVehicules()."\n";
Le nom de la classe est suivi d'une flèche, du nom de la méthode
et des éventuels arguments entre parenthèses. N'écrivez pas
Vehicule::getNbVehicules()
, car le nom de la classe
n'est pas transmis et surtout car les mécanismes d'héritage ne sont
pas mis en œuvre. S'il est possible d'écrire
Velo->getNbVehicules()
, il n'est pas permis
d'écrire Velo::getNbVehicules()
.
Le lecteur notera que les constructeurs sont des méthodes statiques. Ils retournent des références bénies, mais n'ont rien de particulier par rapport à d'autres méthodes de classe.
Il est tout à fait possible d'appeler cette méthode sur une instance
de la classe Vehicule
print $v->getNbVehicules()."\n";
mais dans ce cas le premier argument reçu n'est pas le nom de la classe
mais l'objet en question (c'est donc une méthode d'instance et de classe...).
Cela ne change rien pour notre méthode getNbVehicules
car elle n'utilise pas son premier argument, mais le cas est gênant pour
les constructeurs qui ont à bénir une référence. Pour cela, tout constructeur
devrait commencer par déterminer s'il a en premier argument
le nom de la classe ou une référence. L'instruction qui suit place dans
la variable $class
la classe actuelle, que cette variable
ait pour valeur initiale le nom de la classe ou une instance de la classe :
$class = ref($class) || $class;
Il convient dorénavant d'écrire le constructeur ainsi :
sub new { my ($class,$nbRoues,$couleur) = @_; $class = ref($class) || $class; my $this = {}; bless($this, $class); $this->{NB_ROUES} = $nbRoues; $this->{COULEUR} = $couleur; $nbVehicules++; # un véhicule de plus return $this; }
Il est maintenant possible d'écrire la ligne suivante dans le script :
$v2 = $v->new( 1, "noir" );
Le constructeur est appelé sur une instance de la classe plutôt que sur la classe. On pourrait faire de même pour toutes les méthodes statiques (c'est même plutôt conseillé).
Voici le contenu exact des fichiers d'exemple bâtis tout au long de cette partie du document.
Fichier Vehicule.pm
:
package Vehicule; use strict; use warnings; my $nbVehicules = 0; sub new { my ($class,$nbRoues,$couleur) = @_; $class = ref($class) || $class; my $this = {}; bless($this, $class); $this->{NB_ROUES} = $nbRoues; $this->{COULEUR} = $couleur; $nbVehicules++; # un véhicule de plus return $this; } sub roule { my ($this,$vitesse) = @_; print "Avec $this->{NB_ROUES} roues, je roule à $vitesse.\n"; } sub toString { my ($this) = @_; return "(Vehicule:$this->{NB_ROUES},$this->{COULEUR})"; } sub getNbVehicules { my ($class) = @_; $class = ref($class) || $class; return $nbVehicules; } sub DESTROY { my ($this) = @_; print "Bye bye Vehicule ! "; print "($this->{NB_ROUES} $this->{COULEUR})\n"; } 1; # À ne pas oublier...
Fichier Velo.pm
:
package Velo; use strict; use warnings; use Vehicule; our @ISA = qw(Vehicule); sub new { my ($class,$couleur,$nbVitesses) = @_; $class = ref($class) || $class; my $this = $class->SUPER::new( 2, $couleur ); $this->{NB_VITESSES} = $nbVitesses; return bless($this,$class); } sub pedale { my ($this,$ici) = @_; print "Sur mon vélo $this->{COULEUR} "; print "je pédale avec $this->{NB_VITESSES} vitesses"; print " dans $ici.\n"; } sub toString { my ($this) = @_; my $s = "[Velo:$this->{NB_VITESSES}"; $s .= $this->SUPER::toString(); $s .= "]"; } sub DESTROY { my ($this) = @_; $this->SUPER::DESTROY(); print "Bye bye Velo ! "; print "($this->{NB_VITESSES} $this->{NB_ROUES} ". $this->{COULEUR})\n"; } 1;
Fichier Garage.pm
:
package Garage; use strict; use warnings; sub new { my ($class,$places) = @_; $class = ref($class) || $class; my $this = {}; bless($this, $class); $this->{PLACES} = $places; $this->{VEHICULE} = []; return $this; } sub ajoute { my ($this,$vehicule) = @_; if( @{$this->{VEHICULE}} < $this->{PLACES} ) { push @{$this->{VEHICULE}}, $vehicule; return 1; } return 0; } sub toString { my ($this) = @_; my $s = "{Garage:$this->{PLACES},"; $s .= join( ',', map( {$_->toString()} sort( {$a->{NB_ROUES} <=> $b->{NB_ROUES} } @{$this->{VEHICULE}} ) ) ); return $s."}"; } 1;
Fichier script.pl
:
#!/usr/bin/perl use strict; use warnings; use Data::Dumper; use UNIVERSAL; use Vehicule; use Garage; use Velo; my $v = Vehicule->new( 2, "bleu" ); my $v2 = Vehicule->new( 4, "rouge" ); print "$v\n"; print Dumper($v)."\n"; $v->roule(30); $v2 = undef; if( 1 ) { my $v3 = Vehicule->new(3,'jaune'); } foreach my $k (keys %$v) { print "$k : $v->{$k}\n"; } print $v->toString()."\n"; my $g = Garage->new(3); $g->ajoute( $v ) or die("ajoute: plus de place"); $g->ajoute( Vehicule->new( 4, "vert" ) ) or die("ajoute: plus de place"); $g->ajoute( Vehicule->new( 1, "jaune" ) ) or die("ajoute: plus de place"); print $g->toString()."\n"; my @tab = ( Velo->new('rose',15), Vehicule->new(3,'gris'), "hello", Velo->new('vert',21), ); foreach my $r (@tab) { if( UNIVERSAL::isa( $r, "Vehicule" ) ) { $r->roule( 40 ); } } my $velo = Velo->new('blanc',18); $velo->pedale('les bois'); $velo->roule(10); print $velo->toString()."\n"; print ref($velo)."\n"; print "Ouf, tout va bien !\n" if( ref($velo) eq "Velo" ); print Vehicule->getNbVehicules()."\n"; print Velo->getNbVehicules()."\n"; print $v->getNbVehicules()."\n"; $v2 = $v->new( 1, "noir" );