Ho rispolverato alcuni appunti cartacei relativi i Ruoli di Perl Moose che riporto qui.
- I ruoli rappresentano comportamenti condivisi fra le classi
- La classe fa ciò che dice il ruolo.
- I ruoli non sono classi; infatti i ruoli non si ereditano e non si istanziano.
- I ruoli vengono consumati da classi o da altri ruoli.
- I ruoli sono composti in una classe con la funzione with.
- Tutti i metodi, modificatori, attributi definiti in un ruolo sono aggiunti direttamente alla classe che consuma il ruolo.
- Attributi e metodi appariranno come se fossero definiti nella classe.
- Una sottoclasse della classe consumata (cioè con il ruolo) eredita tutti questi metodi/attributi.
Riferimenti:
- Moose::Role
- Moose::Manual::Roles
In Moose (e sistemi derivati – Mouse) si può fissare un tipo di dato per attributo (isa).
# points deve essere (is a) un Int(ero)
Questo controllo può diventare rognoso se il tipo di attributo è complesso: ad esempio DateTime.
Può capitare di dover inserire nell’attributo un oggetto DateTime che rappresenta una data ed un tempo, ma come posso inserire il dato direttamente senza calcolare l’oggetto (ad esempio in lettura da un file)?
Qui entrano in gioco coerce, subtype, ecc.
has ‘datetime’ => (
isa => ‘My::DateTime’, is => ‘rw’, required => 1,
default => sub { DateTime->now() }
);
Questo attributo deve essere un DateTime (per l’esattezza un My::DateTime, ma questo serve solo per non inquinare i namespace in cui lavora Perl), ma vuole gli vengano passati solo oggetti DateTime.
Nel caso decidessi di passare un numero intero all’attributo datetime (es: $self->datetime(1)) intendendendo unix time l’isa genererebbe un errore; comportamento identico se passassi una stringa come “2004-09-16T23:59:58″.
Grazie alla proprietà coerce è possibile “costringere” un dato in entrata a traformarsi in DateTime anche se parte come qualcosa di diverso.
coerce ‘My::DateTime’
=> from ‘Num’ => via { DateTime->from_epoch( epoch => $_ ) }
=> from ‘Str’ => via {
my ($Y,$M,$D,$h,$m,$s) = uc($_) =~ m/^(\d{4})-(\d{2})-(\d{2})T(;
return DateTime->new( year => $Y, month => $M, day => $D, hour ;
};
has ‘datetime’ => (
isa => ‘My::DateTime’, is => ‘rw’, required => 1,
coerce => 1,
default => sub { DateTime->now() }
);
In questo caso abbiamo definito due trasformazioni: in caso l’input sia un ‘Num’, esegui DateTime->from_epoch( epoch => $_ ); in caso sia una stringa passa il valore attraverso quella regex e ritorna un oggetto DateTime.
La cosa funzionava… poi mi è stato indicato DateTime::Format::ISO8601. Troppo bello.
=> as ‘Object’ => where { $_->isa(‘DateTime’) };
coerce ‘My::DateTime’
=> from ‘Num’ => via { DateTime->from_epoch(epoch => $_) }
=> from ‘Str’ => via { DateTime::Format::ISO8601->parse_datetime($_) };
has ‘datetime’ => (
isa => ‘My::DateTime’, is => ‘rw’, required => 1,
coerce => 1,
default => sub { DateTime->now() }
);
Facile, sintetico… elegante.
Smanettando con l’OOP in Perl mi capita di appoggiarmi a Moose/Mouse per una serie di comodità legate al constraint check del tipo, getter/setter automatici, ecc. – non apprezzo particolarmente i framework complicati, ma sicuramente ci sono contesti in cui li trovo comodi.
Avendo creato una classe con un attributo che conteneva un array di altri oggetti, stavo pensando a come gestire eventuali duplicati dei contenuti che non volevo. La situazione era più o meno così:
use Mouse;
use Player;
has ‘players’ => (
isa => ‘ArrayRef[Player]‘,
is => ‘rw’,
default => sub {[]}
);
before ‘add_player’ => sub {
my ($self, $passed_player) = @_;
# wanna check
};
sub add_player {
my ($self, $passed_player) = @_;
push @{$self->players}, $passed_player;
return $self;
}
sub add_players {
my ($self,@wannabe_players) = @_;
$self->add_player($_) for (@wannabe_players);
return $self;
}
#…
Nel before era mia intenzione mettere un check per controllare ed evitare i duplicati (controllo dell’elemento passato rispetto alla lista dei presenti).
La cosa in teoria sarebbe pure possibile, peccato che non ci sia modo di interrompere il flusso – quindi chiamato add_player che fa scattare il before che viene elaborato, non c’è modo di interrompere l’azione intrapresa: unica soluzione che mi era venuta in mente era fare un unshift che seguisse il push in caso di record già presente con un after (poco elegante, ma efficace).
Fatto sta che dopo una breve indagine mi è stato consigliato Set::Object che “This module implements a set of objects, that is, an unordered collection of objects without duplication.”.
La mia classe è diventata molto più snella:
use Mouse;
use Set::Object;
use Player;
has ‘players’ => (
isa => ‘Set::Object’,
is => ‘rw’,
default => sub { Set::Object->new() },
handles => {
add_player => ‘insert’,
add_players => ‘insert’,
}
);
#…
e in più sono agevolato da alcuni metodi di S::O particolarmente utili (come size, insert, e altri) – e infatti delego con handles senza vergogna.
In effetti devo ancora vedere se fa tutto quello che vorrei, ciò nonostante è una chicca.
Generalmente l’approccio della programmazione ad oggetti consiste nella scomposizione di un problema in sottoparti gestibili in maniera indipendente e strutturate in maniera gerarchica.
Un oggetto è una di queste parti indipendenti, e normalmente contiene tanto il codice quanto i dati da manipolare – in tal senso è autosufficiente.
La programmazione ad oggetti implica alcune feature di norma: incapsulamento, polimorfismo, ereditarietà.
L’incapsulamento è la caratteristica che permette all’oggetto di manipolare i propri dati attraverso li proprio codice senza interferenze esterne; questa strategia di programmazione evita utilizzi scorretti dei dati e porta l’oggetto ad essere una scatola nera autosufficiente.
Il polimorfismo indica che l’interfaccia usata per compiere determinate azioni vale per tutte le classi di oggetti simili: l’idea è quella di avere un unico sistema per accedere allo stesso tipo di dati.
L’ereditarietà è il procedimento per il quale un oggetto acquisisce le proprietà di un altro oggetto in base alla struttura della gerarchia (un oggetto figlio avrà le stesse caratteristiche del padre più qualcosa).
Nelle mie infinite esplorazioni, ho deciso di guardare un po’ meglio la parte Object Oriented di perl.
Gli oggetti in perl sono (normalmente) degli hash anonimi benedetti (a cui viene applicata la funzione bless – in effetti bless si puo’ applicare a qualunque tipo di dato).
Il fatto che un oggetto sia ANCHE un hash in perl mi ha creato un po’ di confusione nel momento in cui ho cominciato a script’are, e sono scivolato spesso in manipolazioni dirette della struttura dati.
In perl si puo’ (notoriamente) fare di tutto, ma è sempre meglio essere ordinati quando si progetta qualcosa.
In generale se si crea un oggetto NON si dovrebbe manipolarlo come una struttura dati qualunque, ma attraverso i metodi preposti dalla classe cui l’oggetto appartiene (questo semplifica anche la manipolazione attraverso l’ereditarietà).
In buona sostanza, avendo un oggetto con l’attributo nome, meglio accedervi chiamando $object->nome (chiamata al metodo) piuttosto che con $object->{nome} (chiamata alla chiave “nome” dell’hashref $oggetto).
E’ possibile creare facilmente metodi di accesso (accessor) e modifica (mutator) agli attributi dell’oggetto con singole sub (un solo metodo che capisce da solo cosa fare se ha un attributo è meglio di un metodo per la lettura ed uno per la scrittura, no?).
Su Advanced Perl li si gestisce con un operatore ternario.
(Se esistono parametri in input alla sub li assegna, in caso contrario ritorna semplicemente il valore. Non c’è bisogno di return perchè le funzioni in perl restituiscono l’ultimo valore scalare.)
In Programming Perl (e Pocket Perl) usiamo un semplice if che controlla l’esistenza di eventuali parametri, ed in caso affermativo li passa alla chiave giusta dell’hash.
Io ho deciso di fare come in Programming perl, ma usando la condizione post-fissa.
Il punto importante rimane la creazione dei metodi in modo da non dover manipolare i valori dell’hash.
Eventualmente può venire in soccorso Class::Accessor che crea i metodi standard da solo.