Patrones Modernos de Desarrollo en Perl
Patrones idiomáticos de Perl 5.36+ y mejores prácticas para construir aplicaciones robustas y mantenibles.
Cuándo Activarlo
- Al escribir código o módulos nuevos en Perl
- Al revisar código Perl para verificar el cumplimiento de normas idiomáticas
- Al refactorizar código Perl heredado hacia estándares modernos
- Al diseñar la arquitectura de un módulo Perl
- Al migrar código anterior a 5.36 hacia Perl moderno
Cómo Funciona
Aplica estos patrones como una inclinación hacia los valores predeterminados modernos de Perl 5.36+: firmas, módulos explícitos, manejo enfocado de errores y límites testeables. Los ejemplos siguientes están pensados para copiarse como puntos de partida y luego ajustarse según la aplicación real, la pila de dependencias y el modelo de despliegue que tengas delante.
Principios Fundamentales
1. Usar el Pragma v5.36
Un único use v5.36 reemplaza el antiguo código repetitivo y habilita strict, warnings y firmas de subrutinas.
# Bien: Preámbulo moderno
use v5.36;
sub saludar($nombre) {
say "¡Hola, $nombre!";
}
# Mal: Código repetitivo heredado
use strict;
use warnings;
use feature 'say', 'signatures';
no warnings 'experimental::signatures';
sub saludar {
my ($nombre) = @_;
say "¡Hola, $nombre!";
}
2. Firmas de Subrutinas
Usa firmas para mayor claridad y verificación automática de aridad.
use v5.36;
# Bien: Firmas con valores por defecto
sub conectar_bd($host, $puerto = 5432, $tiempo_espera = 30) {
# $host es obligatorio, los otros tienen valores por defecto
return DBI->connect("dbi:Pg:host=$host;port=$puerto", undef, undef, {
RaiseError => 1,
PrintError => 0,
});
}
# Bien: Parámetro slurpy para argumentos variables
sub mensaje_log($nivel, @detalles) {
say "[$nivel] " . join(' ', @detalles);
}
# Mal: Desempaquetado manual de argumentos
sub conectar_bd {
my ($host, $puerto, $tiempo_espera) = @_;
$puerto //= 5432;
$tiempo_espera //= 30;
# ...
}
3. Sensibilidad al Contexto
Comprende el contexto escalar frente al de lista, un concepto fundamental de Perl.
use v5.36;
my @items = (1, 2, 3, 4, 5);
my @copia = @items; # Contexto de lista: todos los elementos
my $cuenta = @items; # Contexto escalar: cantidad (5)
say "Items: " . scalar @items; # Forzar contexto escalar
4. Desreferenciación Postfija
Usa la sintaxis de desreferenciación postfija para mejorar la legibilidad con estructuras anidadas.
use v5.36;
my $datos = {
usuarios => [
{ nombre => 'Alicia', roles => ['admin', 'usuario'] },
{ nombre => 'Roberto', roles => ['usuario'] },
],
};
# Bien: Desreferenciación postfija
my @usuarios = $datos->{usuarios}->@*;
my @roles = $datos->{usuarios}[0]{roles}->@*;
my %primero = $datos->{usuarios}[0]->%*;
# Mal: Desreferenciación circunfija (más difícil de leer en cadenas)
my @usuarios = @{ $datos->{usuarios} };
my @roles = @{ $datos->{usuarios}[0]{roles} };
5. El Operador isa (5.32+)
Verificación de tipo infija — reemplaza blessed($o) && $o->isa('X').
use v5.36;
if ($obj isa 'Mi::Clase') { $obj->hacer_algo }
Manejo de Errores
Patrón eval/die
use v5.36;
sub parsear_config($ruta) {
my $contenido = eval { path($ruta)->slurp_utf8 };
die "Error de configuración: $@" if $@;
return decode_json($contenido);
}
Try::Tiny (Manejo Confiable de Excepciones)
use v5.36;
use Try::Tiny;
sub obtener_usuario($id) {
my $usuario = try {
$db->resultset('User')->find($id)
// die "Usuario $id no encontrado\n";
}
catch {
warn "Error al obtener usuario $id: $_";
undef;
};
return $usuario;
}
try/catch Nativo (5.40+)
use v5.40;
sub dividir($x, $y) {
try {
die "División por cero" if $y == 0;
return $x / $y;
}
catch ($e) {
warn "Error: $e";
return;
}
}
POO Moderna con Moo
Prefiere Moo para POO ligera y moderna. Usa Moose solo cuando su metaprotocolo sea necesario.
# Bien: Clase Moo
package Usuario;
use Moo;
use Types::Standard qw(Str Int ArrayRef);
use namespace::autoclean;
has nombre => (is => 'ro', isa => Str, required => 1);
has correo => (is => 'ro', isa => Str, required => 1);
has edad => (is => 'ro', isa => Int, default => sub { 0 });
has roles => (is => 'ro', isa => ArrayRef[Str], default => sub { [] });
sub es_admin($self) {
return grep { $_ eq 'admin' } $self->roles->@*;
}
sub saludar($self) {
return "Hola, soy " . $self->nombre;
}
1;
# Uso
my $usuario = Usuario->new(
nombre => 'Alicia',
correo => '[email protected]',
roles => ['admin', 'usuario'],
);
# Mal: Hashref bendecido (sin validación, sin acceso)
package Usuario;
sub new {
my ($clase, %args) = @_;
return bless \%args, $clase;
}
sub nombre { return $_[0]->{nombre} }
1;
Roles con Moo
package Rol::Serializable;
use Moo::Role;
use JSON::MaybeXS qw(encode_json);
requires 'TO_HASH';
sub to_json($self) { encode_json($self->TO_HASH) }
1;
package Usuario;
use Moo;
with 'Rol::Serializable';
has nombre => (is => 'ro', required => 1);
has correo => (is => 'ro', required => 1);
sub TO_HASH($self) { { nombre => $self->nombre, correo => $self->correo } }
1;
Palabra Clave class Nativa (5.38+, Corinna)
use v5.38;
use feature 'class';
no warnings 'experimental::class';
class Punto {
field $x :param;
field $y :param;
method magnitud() { sqrt($x**2 + $y**2) }
}
my $p = Punto->new(x => 3, y => 4);
say $p->magnitud; # 5
Expresiones Regulares
Capturas Nombradas y Bandera /x
use v5.36;
# Bien: Capturas nombradas con /x para legibilidad
my $re_log = qr{
^ (?<marca_tiempo> \d{4}-\d{2}-\d{2} \s \d{2}:\d{2}:\d{2} )
\s+ \[ (?<nivel> \w+ ) \]
\s+ (?<mensaje> .+ ) $
}x;
if ($linea =~ $re_log) {
say "Hora: $+{marca_tiempo}, Nivel: $+{nivel}";
say "Mensaje: $+{mensaje}";
}
# Mal: Capturas posicionales (difíciles de mantener)
if ($linea =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+\[(\w+)\]\s+(.+)$/) {
say "Hora: $1, Nivel: $2";
}
Patrones Precompilados
use v5.36;
# Bien: Compilar una vez, usar muchas veces
my $re_correo = qr/^[A-Za-z0-9._%+-]+\@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
sub validar_correos(@correos) {
return grep { $_ =~ $re_correo } @correos;
}
Estructuras de Datos
Referencias y Acceso Profundo Seguro
use v5.36;
# Referencias a hash y array
my $config = {
base_de_datos => {
host => 'localhost',
puerto => 5432,
opciones => ['utf8', 'sslmode=require'],
},
};
# Acceso profundo seguro (devuelve undef si falta algún nivel)
my $puerto = $config->{base_de_datos}{puerto}; # 5432
my $ausente = $config->{cache}{host}; # undef, sin error
# Rebanadas de hash
my %subconjunto;
@subconjunto{qw(host puerto)} = @{$config->{base_de_datos}}{qw(host puerto)};
# Rebanadas de array
my @primeros_dos = $config->{base_de_datos}{opciones}->@[0, 1];
# Bucle for con múltiples variables (experimental en 5.36, estable en 5.40)
use feature 'for_list';
no warnings 'experimental::for_list';
for my ($clave, $valor) (%$config) {
say "$clave => $valor";
}
E/S de Archivos
Open de Tres Argumentos
use v5.36;
# Bien: Open de tres argumentos con autodie (módulo del núcleo, elimina 'or die')
use autodie;
sub leer_archivo($ruta) {
open my $fh, '<:encoding(UTF-8)', $ruta;
local $/;
my $contenido = <$fh>;
close $fh;
return $contenido;
}
# Mal: Open de dos argumentos (riesgo de inyección de comandos, ver perl-security)
open FH, $ruta; # NUNCA hagas esto
open FH, "< $ruta"; # Sigue siendo malo: datos de usuario en la cadena de modo
Path::Tiny para Operaciones con Archivos
use v5.36;
use Path::Tiny;
my $archivo = path('config', 'app.json');
my $contenido = $archivo->slurp_utf8;
$archivo->spew_utf8($nuevo_contenido);
# Iterar directorio
for my $hijo (path('src')->children(qr/\.pl$/)) {
say $hijo->basename;
}
Organización de Módulos
Diseño Estándar de Proyecto
MiApp/
├── lib/
│ └── MiApp/
│ ├── App.pm # Módulo principal
│ ├── Config.pm # Configuración
│ ├── DB.pm # Capa de base de datos
│ └── Util.pm # Utilidades
├── bin/
│ └── miapp # Script de entrada
├── t/
│ ├── 00-load.t # Pruebas de compilación
│ ├── unit/ # Pruebas unitarias
│ └── integration/ # Pruebas de integración
├── cpanfile # Dependencias
├── Makefile.PL # Sistema de construcción
└── .perlcriticrc # Configuración de linting
Patrones de Exportación
package MiApp::Util;
use v5.36;
use Exporter 'import';
our @EXPORT_OK = qw(recortar);
our %EXPORT_TAGS = (todas => \@EXPORT_OK);
sub recortar($str) { $str =~ s/^\s+|\s+$//gr }
1;
Herramientas
Configuración de perltidy (.perltidyrc)
-i=4 # Sangría de 4 espacios
-l=100 # Longitud de línea de 100 caracteres
-ci=4 # Sangría de continuación
-ce # else acurrucado
-bar # Llave de apertura en la misma línea
-nolq # No sangrar cadenas largas con comillas
Configuración de 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
Gestión de Dependencias (cpanfile + carton)
cpanm App::cpanminus Carton # Instalar herramientas
carton install # Instalar dependencias desde cpanfile
carton exec -- perl bin/miapp # Ejecutar con dependencias locales
# cpanfile
requires 'Moo', '>= 2.005';
requires 'Path::Tiny';
requires 'JSON::MaybeXS';
requires 'Try::Tiny';
on test => sub {
requires 'Test2::V0';
requires 'Test::MockModule';
};
Referencia Rápida: Modismos Modernos de Perl
| Patrón Heredado | Reemplazo Moderno |
|---|---|
use strict; use warnings; | use v5.36; |
my ($x, $y) = @_; | sub foo($x, $y) { ... } |
@{ $ref } | $ref->@* |
%{ $ref } | $ref->%* |
open FH, "< $archivo" | open my $fh, '<:encoding(UTF-8)', $archivo |
hashref bendecido | Clase Moo con tipos |
$1, $2, $3 | $+{nombre} (capturas nombradas) |
eval { }; if ($@) | Try::Tiny o try/catch nativo (5.40+) |
BEGIN { require Exporter; } | use Exporter 'import'; |
| Operaciones manuales de archivo | Path::Tiny |
blessed($o) && $o->isa('X') | $o isa 'X' (5.32+) |
builtin::true / false | use builtin 'true', 'false'; (5.36+, experimental) |
Antipatrones
# 1. Open de dos argumentos (riesgo de seguridad)
open FH, $nombre_archivo; # NUNCA
# 2. Sintaxis de objeto indirecto (análisis ambiguo)
my $obj = new Foo(bar => 1); # Mal
my $obj = Foo->new(bar => 1); # Bien
# 3. Dependencia excesiva de $_
map { procesar($_) } grep { validar($_) } @items; # Difícil de seguir
my @validos = grep { validar($_) } @items; # Mejor: dividir
my @resultados = map { procesar($_) } @validos;
# 4. Deshabilitar strict refs
no strict 'refs'; # Casi siempre está mal
${"Mi::Paquete::$variable"} = $valor; # Usa un hash en su lugar
# 5. Variables globales como configuración
our $TIEMPO_ESPERA = 30; # Mal: global mutable
use constant TIEMPO_ESPERA => 30; # Mejor: constante
# Mejor aún: atributo Moo con valor por defecto
# 6. eval de cadena para cargar módulos
eval "require $modulo"; # Mal: riesgo de inyección de código
eval "use $modulo"; # Mal
use Module::Runtime 'require_module'; # Bien: carga segura de módulos
require_module($modulo);
Recuerda: El Perl moderno es limpio, legible y seguro. Deja que use v5.36 maneje el código repetitivo, usa Moo para objetos y prefiere los módulos probados de CPAN sobre soluciones hechas a mano.