Programmation Perl/Orienté Objet
Introduction
modifierCette page sera écrite en deux parties distinctes : la définition de la programmation orientée objet en faisant abstraction d'un langage en particulier, puis comment l'utiliser dans le langage étudié.
Gardez à l'esprit que le concept de programmation orientée objet est simple, il y a juste quelques mots de jargon à connaître.
Ce qu'est la programmation orientée objet
modifierLa programmation orientée objet est une manière de concevoir une application. La page wikipedia à ce sujet est bien faite, mais faisons un petit résumé pratique (et plus clair).
La POO se base sur quelques notions assez simples (rappelons que le but est de simplifier la programmation).
Les objets et leur classe
modifierUn objet est un ensemble d'attributs (variables) et de méthodes (fonctions). Une voiture par exemple pourrait se représenter par un objet « Voiture ». Elle est composée de quatre roues, un volant, quatre sièges… qui représentent autant d'attributs de l'objet. La voiture peut également se démarrer, rouler, tourner, s'arrêter… qui représentent les méthodes possibles pour cet objet.
Dans la plupart des langages, pour créer un objet et le définir, nous créons une classe (un fichier avec les différents attributs et méthodes de l'objet). À partir de cette classe, nous pouvons créer des instances. Ainsi, nous pouvons avoir autant de voitures que nous le souhaitons, qui possèdent toutes les mêmes attributs (pas forcément de même valeur cependant) et les mêmes méthodes, à partir de la classe voiture. Exemple : la classe voiture possède un attribut « couleur » et des méthodes (rouler, tourner…), nous créons deux instances de la classe voiture, une avec l'attribut « couleur » à « rouge » et l'autre à « bleue ». Les voitures sont indépendantes.
Le typage et le polymorphisme
modifierChaque objet est un type différent (le type est le nom de sa classe). Il définit ses propres attributs et ses méthodes, et il est utile d'un point de vue sémantique dans notre code. Prenons par exemple la gestion d'un parking, on va faire entrer des « Voitures » dans ce parking, pas des entiers ou une chaîne de caractères. C'est donc une structure de données qui a un but sémantique.
Héritage et redéfinition
modifierPour simplifier l'écriture de code (et en écrire moins), on a créé le principe d'héritage. Prenons un exemple simple : nous souhaitons gérer un jeu de courses de voitures. Nous possédons une trentaines de modèles de voitures différentes (quelques particularités qui changent), cependant elles ont toutes quatre roues, des portières etc. Pour simplifier l'écriture du code, nous allons définir ce qu'est une voiture, et dire que les différents modèles « héritent » de la classe « voiture ». Ainsi, tous les modèles possèdent de base les attributs qu'ils ont en commun, sans avoir à le dire explicitement dans chaque classe. On vient donc de factoriser grandement notre code.
Perl et la POO
modifierPerl a connu plusieurs implémentations de programmation orientée objet via différents modules. Afin d'harmoniser les pratiques, Perl6 reprend le concept de POO dans sa syntaxe de base, et ceci a été repris dans Perl5 par le module Moose. Nous allons voir comment programmer en orienté objet avec cette bibliothèque, qui s'avère de nos jours être la bonne manière de coder en POO en Perl.
Bases de POO en Perl
modifierCréer une instance : new
modifierChaque package possède implicitement une méthode particulière (souvent appelée « new ») pour créer une instance de la classe. Nous appelons les méthodes d'un objet via l'opérateur « -> ».
use Voiture;
my $objet = Voiture->new(); # Appel d'une méthode avec l'opérateur « -> », lié à la POO
$objet est maintenant une référence à un objet « Voiture ».
Connaître la classe d'un objet : ref
modifieruse Voiture;
my $objet = Voiture->new(); # Exemple précédent
say ref($objet); # Affiche « Voiture »
Créer et appeler une méthode : sub et ->
modifierUne méthode est une simple fonction comme nous avons déjà vu dans un chapitre précédent. Le premier paramètre est une référence à l'objet courant. Cette méthode est définie dans un « package », qui sera notre classe. Nous verrons plus loin comment créer une classe, pour voir directement la bonne manière de faire (via Moose).
Comme nous l'avons vu précédemment avec « new », pour appeler une méthode il suffit d'utiliser l'opérateur « -> ».
use Voiture; # on indique qu'on utilise la classe Voiture
my $voiture = Voiture->new(); # on crée une instance
$voiture->demarrer(); # démarre la voiture référencée par $voiture
À remarquer : au début on utilise l'opérateur « -> » sur le package directement pour créer l'instance, après on l'utilise sur l'objet référençant l'instance pour faire appel aux méthodes liées à un objet en particulier (que l'on nomme méthodes d'instance).
POO via Moose
modifierNous arrivons à la partie la plus intéressante. Jusque-là tout était peut-être un peu flou, mais cela va s'éclaircir !
Créer une classe
modifierUne classe se définit par un package, l'utilisation de Moose et se termine par « 1; » tout à la fin du fichier.
#!/usr/bin/env perl
package Voiture; # on crée la classe « Voiture »
use Moose; # Utilisation de Moose pour créer une classe
1; # Permet de s'assurer qu'une classe a bien été chargée
Difficile de faire plus simple non ? Cette classe ne contient rien de bien utile pour le moment, on ne peut que créer une instance de l'objet comme vu précédemment.
Les attributs
modifierBases : has is isa required
modifierUn attribut est déclaré avec le mot clé has. On doit lui passer au moins un paramètre : l'option « is » qui définit si l'attribut est en lecture seule (ro pour read-only) ou s'il est en lecture et écriture (rw pour read-write).
has couleur => ( is => 'rw');
Les attributs peuvent avoir un type précis, qui se trouve dans [liste]. Nous forçons à vérifier ce type avec l'option isa.
has force => ( is => 'rw', isa => 'Int');
Nous pouvons même créer de nouveaux types mais cela va au delà du but de ce livre, veuillez vous référer à la documentation officielle du module MooseUtilTypeConstraint.
Nous pouvons décider qu'une option est obligatoire.
has couleur => ( is => 'rw', required => 1 );
Il y a d'autres options disponibles dont de très intéressantes et puissantes à découvrir dans la documentation du module [[1]], que j'encourage vivement à aller voir pour les anglophones.
Déclarer plusieurs attributs directement
modifierPetite astuce pour gagner du temps, déclarer plusieurs attributs avec les mêmes options.
has [ 'force', 'vitesse', 'endurance' ] => ( is => 'rw', isa => 'Int' ); # déclare 3 attributs de type entier en lecture et écriture
Créer l'instance en modifiant la valeur des attributs
modifierOn peut choisir de modifier la valeur d'un attribut dès la création de l'instance de la classe.
my $voiture = Voiture->new(couleur => "rouge"); # l'attribut "couleur" vaut maintenant "rouge"
Récupérer et modifier la valeur d'un attribut
modifierPour récupérer ou modifier un attribut, il faut passer par des fonctions nommées accesseurs. Pour récupérer la valeur d'un attribut il suffit d'écrire $objet->nom_attribut et pour la modifier $objet->nom_attribut("nouvelle valeur").
my $voiture = Voiture->new(couleur => "rouge"); # l'attribut "couleur" vaut maintenant "rouge"
my $couleur_voiture = $voiture->couleur; # on récupère l'attribut "couleur" de l'instance de la classe "Voiture"
say $couleur_voiture; # affichera "rouge"
$voiture->couleur("bleue"); # on modifie l'attribut "couleur"
$couleur_voiture = $voiture->couleur; # on récupère à nouveau l'attribut (qui a été modifié)
say $couleur_voiture; # affichera "bleue"
Modifier les noms des accesseurs
modifierIl est possible de modifier les fonctions d'accès et de modification des attributs. Cela se fait grâce aux options reader et writer de la création d'un attribut.
package Personnage; # on crée la classe « Personnage »
# dans cet exemple, nous modifions le nom des deux fonctions permettant l'accès en lecture et en écriture de l'attribut "nom"
has nom => ( is => rw, reader => 'obtenir_nom_personnage', writer => 'renommer_personnage' );
Une convention veut que nous nous précédions le nom de l'accesseur par un underscore ( _ ) lorsque nous ne voulons pas que les classes filles l'utilisent. Rien n'empêche en pratique leur utilisation, mais cela est à utiliser à vos risques.
Déclencheur sur la modification d'un attribut : trigger
modifierLors la modification d'un attribut on veut avoir une fonction qui soit appelée.
Soit le fichier Personnage.pm :
package Personnage; # on crée la classe « Personnage »
use Moose; # Utilisation de Moose pour créer une classe
has nom => ( 'is' => 'rw',
trigger => \&_renommer ); # renvoie une référence vers la fonction "_renommer" de l'espace de nom courant
sub _renommer()
{
my ($self, $nouveau, $ancien) = @_;
say "Ancien nom : " . $ancien if @_ > 2;
say "Nouveau nom : " . $nouveau ;
}
1; # Permet de s'assurer qu'une classe a bien été chargée
Soit le fichier Jeu.pl :
use Personnage; # pour utiliser notre module "Personnage"
my $perso = Personnage->new();
$perso->nom('Sam');
# Affichage de "Nouveau nom : Sam"
$perso->nom('Luc');
# Affichage de
# "Ancien nom : Sam"
# "Nouveau nom : Luc"
Références faibles : weak_ref
modifierTout d'abord, petit point sur la façon dont Perl gère la mémoire.
En Perl, nous ne manipulons pas directement la mémoire de l'ordinateur, la gestion est déléguée directement à Perl. Quand on n'utilise plus une variable, ou un objet, il faut « libérer » la zone mémoire qu'elle occupe. Pour trouver ce qu'il faut libérer, on utilise un ramasse-miettes (Garbage Collector). Il y a différentes manières d'implémenter un ramasse-miettes, celle utilisée par Perl est le comptage de références.
Le principe est assez simple : pour chaque variable on va compter le nombre de fois qu'on a une référence qui pointe dessus, une fois qu'on n'en a plus, on ne peut plus accéder à cette zone mémoire. Par conséquent, on peut libérer la mémoire puisque la variable n’est de toutes manières plus utilisable. Pour plus d'informations, je conseille d'aller voir la page sur les ramasse-miettes sur wikipedia.
Soit le module Personnage tel que :
package Personnage; # on crée la classe « Personnage »
use Moose; # Utilisation de Moose pour créer une classe
has nom => ( 'is' => 'rw' );
has [ 'papa', 'maman' ] => ( 'is' => 'rw', weak_ref => 1, isa => 'Personnage' );
1; # Permet de s'assurer qu'une classe a bien été chargée
Soit le code utilisant ce module :
use Personnage; # pour utiliser notre module "Personnage"
my $enfant = Personnage->new( nom => "Philippe");
my $papa = Personnage->new( nom => "Serge" );
$enfant->papa( $papa );
{
my $maman = Personnage->new( nom => "Bernadette" );
$enfant->maman( $maman );
}
# l'objet $maman n'existe plus, il n'y a plus de référence sur cette variable dans le code
say $enfant->papa->nom; # affichera "Serge"
say $enfant->maman->nom; # affichera une erreur d'exécution
Héritage : extends
modifierComme nous avons vu en introduction de ce chapitre, l'héritage permet de créer des classes décrivant toujours un objet plus précis. Par exemple, pour un jeu vidéo on fait une classe "Personnage" puis une classe "Magicien". Un magicien est un personnage. Il hérite donc de toutes ses propriétés et en rajoute potentiellement.
Soit le module Personnage tel que :
package Personnage; # on crée la classe « Personnage »
use Moose; # Utilisation de Moose pour créer une classe
has nom => ( 'is' => 'rw' );
1; # Permet de s'assurer qu'une classe a bien été chargée
Maintenant on veut créer le magicien qui, en plus d'avoir un nom, pourra lancer une boule de feu. Soit le module Magicien :
package Magicien;
use strict;
use Moose; # Utilisation de Moose pour créer une classe
use v5.14;
extends "Personnage"; # on hérite de la classe Personnage
sub lancer_boule_de_feu() {
my ($self) = @_;
say "Le magicien " .
$self->nom . # le magicien est un personnage, donc il possède un nom
" lance une boule de feu !";
}
Maintenant on essaie notre classe :
use Magicien;
my $magicien = Magicien->new( nom => "George" ); # soit un magicien nommé "George"
$magicien->lancer_boule_de_feu(); # affichera "Le magicien George lance une boule de feu !"
Perl permet l'héritage multiple, ce qui veut dire qu'une classe peut hériter de plusieurs autres classes, voici la syntaxe :
extends "classe1", "classe2";
Modifier une méthode héritée (surcharge)
modifierLors d'un héritage, on souhaite parfois changer une méthode dont on hérite, cela s'appelle une surcharge. Pour cela nous n'avons qu'à recréer une méthode de même nom qui remplacera l'ancienne.
Modifier un attribut hérité : +
modifierOn souhaite parfois changer une option de l'attribut, comme par exemple rendre un attribut non modifiable (ro) ou donner une valeur par défaut à un attribut, pour cela on peut modifier la définition de l'attribut. Cela se fait simplement en rajoutant un "+" devant le nom de l'attribut dans notre classe fille (celle qui hérite).
package Magicien;
use Moose; # Utilisation de Moose pour créer une classe
extends "Personnage"; # on hérite de la classe Personnage
has '+nom' => ( is => 'rw', default => 'Gandalf le Gris' ); # un magicien a un nom par défaut
Attention cependant, il est déconseillé de redéfinir un attribut juste pour changer sa valeur par défaut, on préfèrera donner une valeur par défaut à un attribut dans le constructeur de la classe.
Les rôles : Moose::Role with requires
modifierUn rôle n'est pas à proprement parler une classe, mais plutôt une alternative à la hiérarchie imposée par la programmation orientée objet. Si nous reprenons des exemples déjà vus : un magicien est un personnage. Un magicien peut aussi être un guérisseur (qui soigne les autres personnages) mais ce n'est pas nécessairement le cas. On peut dire qu'il remplit la fonction de guérisseur, il a ce rôle.
Un rôle est défini comme une classe, avec éventuellement des attributs et des méthodes et utilise Moose::Role en lieu et place de Moose. Ce que nous allons faire généralement avec les rôles c'est de les utiliser pour indiquer des fonctions qui doivent apparaître dans la classe qui a ce rôle. Pour cela nous utilisons le mot clé "requires".
Soit le rôle Guerisseur :
package Guerisseur;
use Moose::Role; # Utilisation de Moose::Role pour définir un rôle
# toutes les classes ayant comme rôle "Guerisseur"
# devront avoir une méthode "soigner"
requires 'soigner';
1; # comme pour une classe
Notre classe Magicien :
package Magicien;
use Moose; # Utilisation de Moose pour créer une classe
extends "Personnage"; # on hérite de la classe Personnage
with "Guerisseur"; # on utilise le mot clé "with" pour indiquer qu'on possède un rôle
# la méthode qu'on _doit_ implémenter (rôle Guerisseur)
sub soigner() {
my ($self, $personnage_blesse) = @_;
say "Le magicien " . $self->nom . " soigne " . $personnage_blesse->nom ;
}
1;
Maintenant faisons un essai sur notre code :
use Magicien; # classe définie plus haut
use Guerrier; # classe ne faisant qu'hériter de Personnage
my $magicien = Magicien->new( nom => "George" );
my $guerrier = Guerrier->new( nom => "Jean-hubert" );
$magicien->soigner($guerrier); # affichera "Le magicien George soigne Jean-hubert"
Un rôle ne peut pas être instancié. Si on créer des méthodes ou qu'on défini des attributs dans le rôle, alors les classes qui ont ce rôle auront aussi ces attributs et ces méthodes, comme en cas d'héritage.