Modèles de Développement Perl Moderne
Idiomes Perl 5.36+ et bonnes pratiques pour construire des applications robustes et maintenables.
Quand Activer
- Écrire du nouveau code ou des modules Perl
- Réviser du code Perl pour la conformité aux idiomes
- Refactoriser du code Perl legacy vers les standards modernes
- Concevoir l'architecture de modules Perl
- Migrer du code pré-5.36 vers le Perl moderne
Comment Ça Fonctionne
Appliquer ces modèles comme un biais vers les paramètres par défaut du Perl moderne 5.36+ : signatures, modules explicites, gestion d'erreur ciblée et limites testables. Les exemples ci-dessous sont conçus pour être copiés comme points de départ, puis adaptés à l'application, à la pile de dépendances et au modèle de déploiement que vous avez sous les yeux.
Principes Fondamentaux
1. Utiliser le Pragma v5.36
Un simple use v5.36 remplace l'ancien code standard et active strict, warnings et les signatures de sous-routines.
# Bien : Préambule moderne
use v5.36;
sub saluer($nom) {
say "Bonjour, $nom !";
}
# Mal : Code standard legacy
use strict;
use warnings;
use feature 'say', 'signatures';
no warnings 'experimental::signatures';
sub saluer {
my ($nom) = @_;
say "Bonjour, $nom !";
}
2. Signatures de Sous-routines
Utiliser les signatures pour la clarté et la vérification automatique du nombre d'arguments.
use v5.36;
# Bien : Signatures avec valeurs par défaut
sub connecter_db($hote, $port = 5432, $timeout = 30) {
# $hote est obligatoire, les autres ont des valeurs par défaut
return DBI->connect("dbi:Pg:host=$hote;port=$port", undef, undef, {
RaiseError => 1,
PrintError => 0,
});
}
# Bien : Paramètre slurpy pour arguments variables
sub logger_message($niveau, @details) {
say "[$niveau] " . join(' ', @details);
}
# Mal : Dépaquetage manuel des arguments
sub connecter_db {
my ($hote, $port, $timeout) = @_;
$port //= 5432;
$timeout //= 30;
# ...
}
3. Sensibilité au Contexte
Comprendre le contexte scalaire vs liste — un concept central de Perl.
use v5.36;
my @elements = (1, 2, 3, 4, 5);
my @copie = @elements; # Contexte liste : tous les éléments
my $nombre = @elements; # Contexte scalaire : nombre (5)
say "Éléments : " . scalar @elements; # Forcer le contexte scalaire
4. Déréférencement Postfixé
Utiliser la syntaxe de déréférencement postfixé pour la lisibilité avec des structures imbriquées.
use v5.36;
my $donnees = {
utilisateurs => [
{ nom => 'Alice', roles => ['admin', 'utilisateur'] },
{ nom => 'Bob', roles => ['utilisateur'] },
],
};
# Bien : Déréférencement postfixé
my @utilisateurs = $donnees->{utilisateurs}->@*;
my @roles = $donnees->{utilisateurs}[0]{roles}->@*;
my %premier = $donnees->{utilisateurs}[0]->%*;
# Mal : Déréférencement circonfixe (plus difficile à lire en chaîne)
my @utilisateurs = @{ $donnees->{utilisateurs} };
my @roles = @{ $donnees->{utilisateurs}[0]{roles} };
5. L'Opérateur isa (5.32+)
Vérification de type infixe — remplace blessed($o) && $o->isa('X').
use v5.36;
if ($obj isa 'Ma::Classe') { $obj->faire_quelque_chose }
Gestion d'Erreur
Pattern eval/die
use v5.36;
sub analyser_config($chemin) {
my $contenu = eval { path($chemin)->slurp_utf8 };
die "Erreur de configuration : $@" if $@;
return decode_json($contenu);
}
Try::Tiny (Gestion d'Exception Fiable)
use v5.36;
use Try::Tiny;
sub recuperer_utilisateur($id) {
my $utilisateur = try {
$db->resultset('Utilisateur')->find($id)
// die "Utilisateur $id introuvable\n";
}
catch {
warn "Échec de la récupération de l'utilisateur $id : $_";
undef;
};
return $utilisateur;
}
try/catch Natif (5.40+)
use v5.40;
sub diviser($x, $y) {
try {
die "Division par zéro" if $y == 0;
return $x / $y;
}
catch ($e) {
warn "Erreur : $e";
return;
}
}
POO Moderne avec Moo
Préférer Moo pour une POO légère et moderne. Utiliser Moose uniquement lorsque son métaprotocole est nécessaire.
# Bien : Classe Moo
package Utilisateur;
use Moo;
use Types::Standard qw(Str Int ArrayRef);
use namespace::autoclean;
has nom => (is => 'ro', isa => Str, required => 1);
has email => (is => 'ro', isa => Str, required => 1);
has age => (is => 'ro', isa => Int, default => sub { 0 });
has roles => (is => 'ro', isa => ArrayRef[Str], default => sub { [] });
sub est_admin($self) {
return grep { $_ eq 'admin' } $self->roles->@*;
}
sub saluer($self) {
return "Bonjour, je suis " . $self->nom;
}
1;
# Utilisation
my $utilisateur = Utilisateur->new(
nom => 'Alice',
email => '[email protected]',
roles => ['admin', 'utilisateur'],
);
# Mal : Hashref béni (pas de validation, pas d'accesseurs)
package Utilisateur;
sub new {
my ($classe, %args) = @_;
return bless \%args, $classe;
}
sub nom { return $_[0]->{nom} }
1;
Rôles Moo
package Role::Serialisable;
use Moo::Role;
use JSON::MaybeXS qw(encode_json);
requires 'VERS_HASH';
sub vers_json($self) { encode_json($self->VERS_HASH) }
1;
package Utilisateur;
use Moo;
with 'Role::Serialisable';
has nom => (is => 'ro', required => 1);
has email => (is => 'ro', required => 1);
sub VERS_HASH($self) { { nom => $self->nom, email => $self->email } }
1;
Mot-clé Natif class (5.38+, Corinna)
use v5.38;
use feature 'class';
no warnings 'experimental::class';
class Point {
field $x :param;
field $y :param;
method magnitude() { sqrt($x**2 + $y**2) }
}
my $p = Point->new(x => 3, y => 4);
say $p->magnitude; # 5
Expressions Régulières
Captures Nommées et Drapeau /x
use v5.36;
# Bien : Captures nommées avec /x pour la lisibilité
my $log_re = qr{
^ (?<horodatage> \d{4}-\d{2}-\d{2} \s \d{2}:\d{2}:\d{2} )
\s+ \[ (?<niveau> \w+ ) \]
\s+ (?<message> .+ ) $
}x;
if ($ligne =~ $log_re) {
say "Heure : $+{horodatage}, Niveau : $+{niveau}";
say "Message : $+{message}";
}
# Mal : Captures positionnelles (difficiles à maintenir)
if ($ligne =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+\[(\w+)\]\s+(.+)$/) {
say "Heure : $1, Niveau : $2";
}
Patterns Précompilés
use v5.36;
# Bien : Compiler une fois, utiliser plusieurs fois
my $email_re = qr/^[A-Za-z0-9._%+-]+\@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
sub valider_emails(@emails) {
return grep { $_ =~ $email_re } @emails;
}
Structures de Données
Références et Accès Profond Sécurisé
use v5.36;
# Références de hachage et de tableau
my $config = {
base_de_donnees => {
hote => 'localhost',
port => 5432,
options => ['utf8', 'sslmode=require'],
},
};
# Accès profond sécurisé (retourne undef si un niveau manque)
my $port = $config->{base_de_donnees}{port}; # 5432
my $manquant = $config->{cache}{hote}; # undef, pas d'erreur
# Tranches de hachage
my %sous_ensemble;
@sous_ensemble{qw(hote port)} = @{$config->{base_de_donnees}}{qw(hote port)};
# Tranches de tableau
my @deux_premiers = $config->{base_de_donnees}{options}->@[0, 1];
# Boucle for multi-variable (expérimental en 5.36, stable en 5.40)
use feature 'for_list';
no warnings 'experimental::for_list';
for my ($cle, $valeur) (%$config) {
say "$cle => $valeur";
}
Entrées/Sorties Fichier
Ouverture à Trois Arguments
use v5.36;
# Bien : Ouverture à trois arguments avec autodie (module cœur, élimine 'or die')
use autodie;
sub lire_fichier($chemin) {
open my $fh, '<:encoding(UTF-8)', $chemin;
local $/;
my $contenu = <$fh>;
close $fh;
return $contenu;
}
# Mal : Ouverture à deux arguments (risque d'injection shell, voir perl-security)
open FH, $chemin; # NE JAMAIS faire ça
open FH, "< $chemin"; # Toujours mauvais — données utilisateur dans la chaîne de mode
Path::Tiny pour les Opérations sur Fichiers
use v5.36;
use Path::Tiny;
my $fichier = path('config', 'app.json');
my $contenu = $fichier->slurp_utf8;
$fichier->spew_utf8($nouveau_contenu);
# Parcourir un répertoire
for my $enfant (path('src')->children(qr/\.pl$/)) {
say $enfant->basename;
}
Organisation des Modules
Structure de Projet Standard
MonApp/
├── lib/
│ └── MonApp/
│ ├── App.pm # Module principal
│ ├── Config.pm # Configuration
│ ├── DB.pm # Couche base de données
│ └── Util.pm # Utilitaires
├── bin/
│ └── monapp # Script d'entrée
├── t/
│ ├── 00-load.t # Tests de compilation
│ ├── unit/ # Tests unitaires
│ └── integration/ # Tests d'intégration
├── cpanfile # Dépendances
├── Makefile.PL # Système de construction
└── .perlcriticrc # Configuration de linting
Patterns d'Exportation
package MonApp::Util;
use v5.36;
use Exporter 'import';
our @EXPORT_OK = qw(trim);
our %EXPORT_TAGS = (all => \@EXPORT_OK);
sub trim($str) { $str =~ s/^\s+|\s+$//gr }
1;
Outillage
Configuration perltidy (.perltidyrc)
-i=4 # Indentation de 4 espaces
-l=100 # Longueur de ligne de 100 caractères
-ci=4 # Indentation de continuation
-ce # else accolé
-bar # Accolade ouvrante sur la même ligne
-nolq # Ne pas désindenter les chaînes longues
Configuration perlcritic (.perlcriticrc)
severity = 3
theme = core + pbp + security
[InputOutput::RequireCheckedSyscalls]
functions = :builtins
exclude_functions = say print
[Subroutines::ProhibitExplicitReturnUndef]
severity = 4
[ValuesAndExpressions::ProhibitMagicNumbers]
allowed_values = 0 1 2 -1
Gestion des Dépendances (cpanfile + carton)
cpanm App::cpanminus Carton # Installer les outils
carton install # Installer les dépendances depuis cpanfile
carton exec -- perl bin/monapp # Exécuter avec les dépendances locales
# cpanfile
requires 'Moo', '>= 2.005';
requires 'Path::Tiny';
requires 'JSON::MaybeXS';
requires 'Try::Tiny';
on test => sub {
requires 'Test2::V0';
requires 'Test::MockModule';
};
Référence Rapide : Idiomes Perl Modernes
| Pattern Legacy | Remplacement Moderne |
|---|---|
use strict; use warnings; | use v5.36; |
my ($x, $y) = @_; | sub foo($x, $y) { ... } |
@{ $ref } | $ref->@* |
%{ $ref } | $ref->%* |
open FH, "< $file" | open my $fh, '<:encoding(UTF-8)', $file |
hashref béni | Classe Moo avec types |
$1, $2, $3 | $+{nom} (captures nommées) |
eval { }; if ($@) | Try::Tiny ou try/catch natif (5.40+) |
BEGIN { require Exporter; } | use Exporter 'import'; |
| Opérations manuelles sur fichiers | Path::Tiny |
blessed($o) && $o->isa('X') | $o isa 'X' (5.32+) |
builtin::true / false | use builtin 'true', 'false'; (5.36+, expérimental) |
Anti-patrons
# 1. Ouverture à deux arguments (risque de sécurité)
open FH, $nom_fichier; # NE JAMAIS
# 2. Syntaxe d'objet indirect (analyse ambiguë)
my $obj = new Foo(bar => 1); # Mal
my $obj = Foo->new(bar => 1); # Bien
# 3. Dépendance excessive à $_
map { traiter($_) } grep { valider($_) } @elements; # Difficile à suivre
my @valides = grep { valider($_) } @elements; # Mieux : décomposer
my @resultats = map { traiter($_) } @valides;
# 4. Désactiver strict 'refs'
no strict 'refs'; # Presque toujours une erreur
${"Mon::Package::$var"} = $valeur; # Utiliser un hachage à la place
# 5. Variables globales comme configuration
our $TIMEOUT = 30; # Mal : global mutable
use constant TIMEOUT => 30; # Mieux : constante
# Meilleur : attribut Moo avec valeur par défaut
# 6. eval de chaîne pour le chargement de module
eval "require $module"; # Mal : risque d'injection de code
eval "use $module"; # Mal
use Module::Runtime 'require_module'; # Bien : chargement sécurisé
require_module($module);
Rappelez-vous : Le Perl moderne est propre, lisible et sûr. Laissez use v5.36 gérer le code standard, utilisez Moo pour les objets, et préférez les modules éprouvés du CPAN aux solutions faites maison.