[couverture de Linux Dossiers 2]
[couverture de Linux Magazine 47]

Introduction à la programmation en Perl (7/7)

Article publié dans Linux Magazine 47, février 2003. Repris dans Linux Dossiers 2 (avril/mai/juin 2004).

Copyright © 2002-2003 - Sylvain Lhullier.

[+ del.icio.us] [+ Developers Zone] [+ Bookmarks.fr] [Digg this] [+ My Yahoo!]

Ce document est généré à partir du source XML fourni par Sylvain à l'adresse http://sylvain.lhullier.org/publications/perl.html.

Chapeau

Voici le septième et dernier article de notre série introductive à la programmation en Perl ; nous abordons cette fois la programmation objet. Nombre de bibliothèques Perl sont bâties sur ce principe. 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 et la facilité d'écriture qu'il manque aux langages uniquement objet.

Programmation objet

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 de l'y oblige.

Vous avez dit objet ?

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.

Préparatifs

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.

Écrire un constructeur

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  sub new
        5  {
        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 4) 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.

Appeler le constructeur

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 -w
    use strict;

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.

Manipulations de l'objet

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 appelera 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.

Plusieurs constructeurs

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().

Écrire une méthode

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 appelant.

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 noterons 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.

Reparlons des champs

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 simple 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. Et 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 une underscore est une méthode privée).

De façon général, 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.

Composition

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;
    # ... 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 ;-))

Destruction d'un objet

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.

Héritage

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 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 moyen simple de 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.

Classes d'un objet

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 qw(isa);
    if( isa( $r, "Vehicule" ) )
    {
       $r->roule( 40 );
    }

On teste ainsi si la variable $r est un objet de classe Vehicule.

Champs et méthodes statiques

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é).

Exemple complet

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;
    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 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;
    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 -w
    use strict;
    use Data::Dumper;
    use UNIVERSAL qw(isa);
    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( 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" );

Conclusion

Nous sommes ici au terme de ce cours introductif à la programmation en Perl. Vous avez maintenant en main la plupart des concepts et des notions pour être autonome en Perl.

N'oubliez pas que la documentation de Perl est très bien faite et est disponible sur votre ordinateur au moyen de la commande perldoc : perldoc perl vous donne accès à la liste des thèmes consultables avec perldoc. Par exemple perldoc perldata vous explique les structures de données en Perl. Pour une fonction particulière, utilisez l'option -f : perldoc -f chomp Pour un module particulier, utilisez perldoc sans option : perldoc Data::Dumper

Vous avez maintenant en main beaucoup de notions importantes de Perl. À vous de les mettre en œuvre pour vos propres problèmes. Il n'est pas forcement facile d'exprimer son besoin en termes directement exploitable dans un langage particulier, mais en ayant un peu d'expérience de Perl, vous trouverez vite votre manière de résoudre un problème. Vous allez alors commencer à faire connaissance avec Tim Towtdi (There is more than one way to do it). ;-)

L'auteur

Sylvain Lhullier sylvain@lhullier.org http://sylvain.lhullier.org/

À l'Université de Marne-la-Vallée (http://www.univ-mlv.fr/), il est chargé chaque année d'un module complet d'enseignement en maîtrise informatique ; ce module porte sur des sujets tels que Perl, Perl/Tk, Perl/DBI, Apache, CGI/Perl et consiste en dix semaines d'un cours en amphithéatre et de trois travaux dirigés, ainsi qu'en un examen final et un projet.

Il a travaillé pour Linagora (http://www.linagora.com/) comme consultant formateur et développeur ; il intervenait principalement pour des projets et prestations concernant le développement (Perl, Java, C++, programmation système, PHP, Apache, MySQL, XML etc.).

Il a développé chez feue-Atrid des projets bâtis en Perl (LogTrend, Mioga) et publiés sous GPL.

Il a donné une conférence "Perl : pour quoi faire ?" lors du salon LinuxSolutions de janvier 2004.

Entre autres associations, il est membre et administrateur de l'association "Les Mongueurs de Perl" dont l'objet est de promouvoir le langage Perl en France par des réunions, des conférences etc (http://www.mongueurs.net/). Il y coordonne le groupe de travail "Articles" dont le but est de publier un article dans la presse francophone technique, notamment dans GNU/Linux & Hurd Magazine France (http://www.linuxmag-france.org/) chaque mois. L'auteur tient à remercier très chaleuseusement les autres membres de l'association pour leur aide, leur soutien et les relectures effectuées.

[IE7, par Dean Edwards] [Validation du HTML] [Validation du CSS]