NOME
perltoot - Guida al Perl orientato agli oggetti, a cura di Tom
DESCRIZIONE
La programmazione orientata agli oggetti è alla moda di questi giorni. Alcuni manager preferirebbero gli oggetti all'uovo di Colombo. Perché? Che cosa c'è di così speciale in un oggetto? Che cosa è un oggetto innanzitutto?
Un oggetto non è nient'altro che un modo per infilare funzionamenti complessi in un grazioso pacchetto semplice da usare. (Questo è ciò che i professori chiamano astrazione). Alcune persone in gamba che non hanno di meglio da fare che sedere intorno ad un tavolo per settimane al fine di risolvere problemi veramente complicati, creano questi splendidi oggetti che possono essere utilizzati anche da persone normali. (Questo è ciò che i professori chiamano riutilizzo del software). Gli utenti (cioè i programmatori) possono giocare quanto vogliono con questo pacchettino, ma non debbono aprirlo e far confusione con ciò che c'è dentro. Esattamente come un costoso macchinario, il contratto dice che la garanzia è nulla una volta aperto il coperchio. Quindi non fatelo.
Il cuore degli oggetti è la classe, un piccolo namespace [spazio dei nomi, NdT] privato e protetto pieno di dati e funzioni. Una classe è un insieme di routine correlate che affrontano lo stesso ambito di problemi. Potete pensare ad essa come a un tipo definito dall'utente. Il meccanismo dei package del Perl, usato anche per moduli più tradizionali, è usato pure per moduli di classi. Gli oggetti "vivono" in una classe, ciò vuol dire che appartengono a un qualche package.
Più spesso che no, la classe fornisce l'utente di pacchettini. Questi pacchettini sono gli oggetti. Sanno a quale classe appartengono e come debbono comportarsi. Gli utilizzatori chiedono alla classe di fare qualcosa, come "dammi un oggetto". Oppure possono chiedere a uno di questi oggetti di fare qualcosa. Chiedere ad una classe di fare qualcosa per voi si chiama metodo di una classe. Chiedere ad un oggetto di fare qualcosa per voi si chiama metodo di un oggetto. Chiedere ad una classe (di solito) o ad un oggetto (talvolta) di restituirvi un oggetto si chiama costruttore, che è semplicemente un tipo particolare di metodo.
Fin qui siamo d'accordo, ma in che cosa differisce un oggetto da qualsiasi altro tipo di dato in Perl? Che cos'è un oggetto veramente; cioè, qual'è il suo tipo fondamentale? La risposta alla prima domanda è semplice. Un oggetto è differente da qualsiasi altro tipo di dato in Perl in una ed una sola maniera: potete dereferenziarlo utilizzando non solamente indici numerici o alfanumerici come con semplici array e hash, ma con chiamate esplicite di subroutine. In una parola, con i metodi.
La risposta alla seconda domanda è che un oggetto è un riferimento, e non un riferimento qualsiasi, badate bene, ma uno sul cui referente è stato eseguito un bless() [consacrazione, NdT] in una particolare classe (leggi: package). Che tipo di riferimento?? Beh, la risposta a questo è un pò meno concreta. Questo perché in Perl il progettista della classe può impiegare qualsiasi tipo di riferimento gli piaccia come tipo di dato intrinseco sottostante. Potrebbe essere uno scalare, un array o un riferimento ad un hash. Potrebbe anche trattarsi di un riferimento a un codice. Ma data la sua innata flessibilità, un oggetto è di solito un riferimento ad un hash.
Creare una Classe
Prima di creare una classe, dovete decidere che nome darle. Questo perché il nome della classe (package) decide il nome del file utilizzato per ospitarla, esattamente come con i normali moduli. Quindi, quella classe (package) dovrebbe fornire uno o più modi per generare oggetti. Alla fine, dovrebbe fornire meccanismi per permettere agli utilizzatori dei propri oggetti di manipolare indirettamente questi oggetti a distanza.
Per esempio, facciamo un semplice modulo classe Persona. Esso viene archiviato nel file Persona.pm. Se la classe si fosse chiamata Brava::Persona, sarebbe stata archiviata nel file Brava/Persona.pm, e il suo package sarebbe diventato Brava::Persona invece della sola Persona. (In un personal computer con un sistema operativo diverso da Unix o Plan 9, ma con qualcosa come Mac OS o VMS, il separatore di directory può essere diverso, ma il principio è lo stesso). Non assumete che ci sia alcuna relazione formale tra i moduli in base ai nomi delle loro directory. È esclusivamente una convenzione per raggruppare, e non ha effetto su ereditarietà, accessibilità delle variabili o qualsiasi altra cosa.
Per questo modulo non abbiamo intenzione di utilizzare Exporter, perché siamo una classe di buone maniere che non esporta assolutamente niente. Per fabbricare oggetti, una classe deve avere un metodo costruttore. Un costruttore restituisce non solo un normale tipo di dato, ma un oggetto completamente nuovo di quella classe. Di questa stregoneria si occupa la funzione bless(), il cui solo scopo è di abilitare il suo referente ad essere usato come un oggetto. Ricordatevi: il fatto di essere un oggetto significa solamente che i metodi possono essere chiamati attraverso di esso.
Benché un costruttore possa essere chiamato in qualsiasi modo vogliate, sembra che molti programmatori preferiscano chiamarli new() [nuovo, NdT]. Ad ogni modo, new() non è una parola riservata, e una classe non è obbligata a contenerla. Pare che alcuni programmatori utilizzino una funzione con lo stesso nome della classe come costruttore.
Rappresentazione di un Oggetto
Il meccanismo di gran lunga più utilizzato in Perl per rappresentare un record Pascal, uno struct C, o una classe C++ è un hash anonimo. Questo perché un hash ha un numero arbitrario di campi dati, ognuno convenientemente acceduto da un nome arbitrario di vostra creazione.
Se state solamente emulando qualcosa come uno struct, vi imbatterete in qualcosa come:
$rec = {
nome => "Gianni",
eta => 23,
compagni => [ "Norberto", "Raffaele", "Pino"],
};
Se ve la sentite, potete aggiungere un po' di distinzione visiva mettendo in maiuscolo le chiavi dell'hash:
$rec = {
NOME => "Gianni",
ETA => 23,
COMPAGNI => [ "Norberto", "Raffaele", "Pino"],
};
E in questo modo potete chiamare $rec->{NOME}
per ottenere "Gianni", o @{ $rec->{COMPAGNI} }
per ottenere "Norberto", "Raffaele", e "Pino".
Questo stesso modello è spesso usato per le classi, sebbene non sia considerato l'apice della correttezza della programmazione, da gente nuova al concetto di classe, prendere possesso di un oggetto, accedendo in modo sfacciato ai suoi dati membri direttamente. Parlando in generale, un oggetto dovrebbe essere considerato come una misteriosa struttura dati che potete accedere con i metodi di oggetto. Visivamente, le chiamate di metodi assomigliano alla dereferenziazione di un riferimento utilizzando un nome di una funzione anziché con parentesi graffe o tonde.
Interfaccia di una Classe
Alcuni linguaggi forniscono un'interfaccia sintattica formale per i metodi di una classe, ma non il Perl. Si affida a voi per leggere la documentazione di ogni classe. Se provate a chiamare un metodo indefinito di un oggetto, il Perl non protesterà, ma il programma attiverà una eccezione a tempo di esecuzione. Allo stesso modo, se chiamate un metodo che si aspetta un numero primo come argomento, con un numero non primo, non potete aspettarvi che il compilatore individui questa discrepanza. (Beh, potete aspettarvi ciò che volete, ma non accadrà).
Supponete di avere un utente ben educato della vostra classe Persona, qualcuno che ha letto la documentazione che spiega l'interfaccia stabilita. Vediamo come può essere utilizzata la classe Persona:
use Persona;
$egli = Persona->new();
$egli->nome("Gianni");
$egli->eta(23);
$egli->compagni( "Norberto", "Raffaele", "Pino" );
push @Ogni_Record, $egli; # salva l'oggetto in un array per dopo
printf "%s ha %d anni.\n", $egli->nome, $egli->eta;
print "I suoi compagni sono: ", join(", ", $egli->compagni), "\n";
printf "Il nome dell'ultimo record e` %s\n", $Ogni_Record[-1]->nome;
Come potete vedere, l'utente della classe non sa (o almeno, non ha alcun interesse a sapere) che l'oggetto ha una particolare implementazione invece di un'altra. L'interfaccia alla classe ed ai suoi oggetti avviene esclusivamente tramite metodi, e questo è tutto ciò di cui l'utilizzatore della classe dovrebbe mai occuparsi.
Costruttori e Metodi di Istanze
Eppure, qualcuno deve sapere che cosa c'è nell'oggetto. E quel qualcuno è la classe. Essa implementa metodi che il programmatore utilizza per accedere all'oggetto. Qui vedrete come implementare la classe Persona utilizzando l'idioma standard hash-riferimento-come-un-oggetto. Costruiremo un metodo di classe chiamato new() che agisce come costruttore, e tre metodi di oggetto chiamati nome(), eta() e compagni() per ottenere dati di singoli oggetti nascosti nel nostro hash anonimo.
package Persona;
use strict;
#########################################################
## il costruttore di oggetto (versione semplicistica) ##
#########################################################
sub new {
my $self = {};
$self->{NOME} = undef;
$self->{ETA} = undef;
$self->{COMPAGNI} = [];
bless($self); # ma vedi sotto
return $self;
}
#################################################
## metodi per accedere dati di singoli oggetti ##
## ##
## Con argomenti, impostano il valore. Senza ##
## di essi, recuperano il/i valore/i. ##
#################################################
sub nome {
my $self = shift;
if (@_) { $self->{NOME} = shift }
return $self->{NOME};
}
sub eta {
my $self = shift;
if (@_) { $self->{ETA} = shift }
return $self->{ETA};
}
sub compagni {
my $self = shift;
if (@_) { @{ $self->{COMPAGNI} } = @_ }
return @{ $self->{COMPAGNI} };
}
1; # cosi` il require o l'use ha successo
Abbiamo creato tre metodi per accedere ai dati di un oggetto, nome(), eta() e compagni(). Questi sono tutti sostanzialmente simili. Se chiamati con un argomento, impostano il campo appropriato; altrimenti restituiscono il valore contenuto in quel campo, ovvero il valore corrispondente a quella chiave dell'hash.
Progetti per il Futuro: Costruttori Migliori
Anche se a questo punto potete anche ignorarne il significato, un giorno vi dovrete preoccupare dell'ereditarietà. (Potete tranquillamente ignorarla per adesso e preoccuparvi dopo, se vi pare). Per assicurarvi che tutto funzioni alla perfezione, dovete usare la forma con doppio argomento di bless(). Il secondo argomento è la classe nella quale al referente verrà applicata la funzione bless(). Non assumendo la nostra classe come secondo argomento di default ed usando invece la classe ricevuta come argomento, creiamo in nostro costruttore ereditabile.
sub new {
my $classe = shift;
my $self = {};
$self->{NOME} = undef;
$self->{ETA} = undef;
$self->{COMPAGNI} = [];
bless ($self, $classe);
return $self;
}
Questo è tutto ciò che concerne i costruttori. Questi metodi danno vita agli oggetti, restituendo pacchetti graziosi e misteriosi all'utilizzatore, per essere usati in seguito con chiamate ai metodi.
Distruttori
Ogni storia ha un inizio ed una fine. L'inizio della storia di un oggetto è il suo costruttore, chiamato esplicitamente quando l'oggetto prende vita. Ma la fine della sua storia è il distruttore, un metodo chiamato implicitamente quando un oggetto perde la vita. Qualsiasi codice di clean-up [pulizia, NdT] dedicato ai singoli oggetti è posto nel distruttore, che deve (in Perl) essere chiamato DESTROY [distruggi, NdT].
Se i costruttori possono avere nomi arbitrari, perché i distruttori no? Perché mentre i costruttori sono chiamati esplicitamente, i distruttori non lo sono. La distruzione avviene automaticamente attraverso il sistema di garbage collection (GC) [raccolta di immondizia, NdT] del Perl, che è un sistema di GC fondato sui riferimenti, veloce ma talvolta pigro. Per sapere che cosa chiamare, il Perl insiste sul fatto che il distruttore sia chiamato DESTROY. La nozione del Perl sul tempo esatto per la chiamata al distruttore non è al momento ben definita, ecco perché i vostri distruttorinon dovrebbero fare affidamento sul quando vengono chiamati.
Perché DESTROY viene scritto completamente maiuscolo? Il Perl talvolta utilizza nomi di funzione completamente maiuscoli come convenzione per indicare che la funzione viene in qualche modo chiamata automaticamente dal Perl. Di questa categoria fanno parte BEGIN, END, AUTOLOAD, più tutti i metodi utilizzati da oggetti legati con la funzione tie(), descritti in perltie.
In linguaggi di programmazione orientati agli oggetti veramente buoni, l'utente non si cura di quando il distruttore viene chiamato. Accade quando ci si aspetta che accada. In linguaggi a basso livello del tutto privi di GC, non c'è modo di fare affidamento sul fatto che ciò avvenga al momento giusto, così il programmatore deve esplicitamente chiamare il distruttore per pulire memoria e stato, incrociando le dita che sia il tempo giusto per farlo. A differenza del C++, in Perl non è quasi mai necessario un distruttore di oggetto, e anche quando lo è, l'invocazione esplicita non è necessaria. Nel caso della nostra classe Persona, non abbiamo bisogno del distruttore perché il Perl si prende cura di cose di poco conto come la deallocazione della memoria.
La sola situazione nella quale il GC basato sui riferimenti del Perl non funziona è quando si trova di fronte a una struttura dati circolare, del tipo:
$questo->{QUALCOSA} = $questo;
In tal caso, dovete eliminare l'autoriferimento manualmente se volete che il vostro programma non sprechi memoria. Benché dichiaratamente incline all'errore, questo è il meglio che possiamo fare al momento. Nondimeno, è sicuro che al termine del vostro programma i distruttori degli oggetti siano chiamati adeguatamente. Quindi siete garantiti dal fatto che un oggetto alla fine viene propriamente distrutto, ad eccezione del caso unico in cui un programma non termina mai. (Se eseguite codice Perl immerso in un'altra applicazione la fase di GC avviene un po' più frequentemente - ogni qual volta un thread si esaurisce).
Altri Metodi di Oggetto
I metodi di cui abbiamo parlato finora sono stati costruttori o semplici "metodi per dati", interfacce a dati archiviati nell'oggetto. Questi sono più o meno come i dati membro di oggetti nell'accezione del C++, ad eccezione del fatto che non sono accessibili dall'esterno come dati. Al contrario, dovrebbero solamente accedere indirettamente ai dati dell'oggetto tramite i suoi metodi. Questa è una regola importante: in Perl, l'accesso ai dati di un oggetto dovrebbe essere esclusivamente effettuato attraverso i metodi.
Il Perl non pone restrizioni su chi può usare quali metodi. La distinzione pubblico-contro-privato è una convenzione, non sintassi. (Beh, a meno che non utilizziate il modulo Alias descritto sotto in "Dati membro come variabili"). Talvolta vedrete nomi di metodo iniziare o terminare con uno o due trattini bassi. Questa marcatura è una convenzione per indicare che i metodi sono privati per quella sola classe e talvolta per i suoi stretti conoscenti, le sue immediate sottoclassi. Ma questa distinzione non è forzata dal Perl in sé. Sta al programmatore seguire questo comportamento.
Non c'è ragione di limitare i metodi al semplice accesso ai dati. I metodi possono fare ogni cosa. Il punto chiave è che sono invocati per mezzo dell'oggetto o della classe. Diciamo che vorremmo metodi di oggetto che fanno più che recuperare o impostare un campo particolare.
sub esclama {
my $self = shift;
return sprintf "Ciao, sono %s, di %d anni, lavoro con %s",
$self->{NOME}, $self->{ETA}, join(", ", @{$self->{COMPAGNI}});
}
O forse anche uno come questo:
sub buon_compleanno {
my $self = shift;
return ++$self->{ETA};
}
Alcuni potrebbero obiettare che si dovrebbe procedere così:
sub esclama {
my $self = shift;
return sprintf "Ciao, sono %s, di %d anni, lavoro con %s",
$self->nome, $self->eta, join(", ", $self->compagni);
}
sub buon_compleanno {
my $self = shift;
return $self->eta( $self->eta() + 1 );
}
Ma dato che questi metodi sono tutti eseguiti nella stessa classe, ciò può non essere critico. Ci sono dei compromessi da fare. L'utilizzo diretto dell'hash è più veloce (circa un ordine di magnitudo più veloce, infatti), ed è più conveniente quando volete fare degli inserimenti in stringhe. Ma l'utilizzo di metodi (l'interfaccia esterna) occulta internamente non solo gli utenti della vostra classe ma anche voi stessi da cambiamenti nella rappresentazione dei vostri dati.
Dati della Classe
Che dire a proposito dei "dati della classe", elementi di dati comuni ad ogni oggetto di una classe? Per quale scopo li vorreste? Dunque, nella vostra classe Persona, potreste voler tener conto del totale della popolazione vivente. Come implementereste ciò?
Potreste creare una variabile globale chiamata $Persona::Censo. Ma forse la sola ragione per cui lo fareste è se voi voleste che sia possibile accedere ai dati della vostra classe direttamente. Qualcuno potrebbe semplicemente prendere $Persona::Censo e farne qualsiasi cosa. Forse questo va bene nella vostra progettazione. Potreste anche concepibilmente volerla rendere una variabile esportata. Per essere esportabile, una variabile deve essere globale a tutto il package. Se questo fosse un modulo tradizionale piuttosto che uno orientato agli oggetti, potreste farlo.
Benché ci si aspetti questo approccio nella maggior parte dei moduli tradizionali, è una forma generalmente considerata alquanto mediocre nella maggior parte dei moduli a oggetti. In un modulo a oggetti, dovreste costruire un velo protettivo per separare l'interfaccia dall'implementazione. Fornite così un metodo di classe per accedere ai dati della classe così come fornite metodi di oggetti per accedere ai dati di oggetti.
Così, potreste sempre mantenere $Censo come una variabile globale del package e affidarvi ad altro per onorare il contratto con il modulo e di conseguenza non mettere le mani nella sua implementazione. Potreste anche essere superscaltri e rendere $Censo una variabile legata con tie() così come viene descritto in perltie, in modo da intercettare tutti gli accessi.
Ma più spesso che no, vorrete solamente rendere il dato della vostra classe una variabile lessicale con campo di visibilità limitato al file. Per far ciò, ponete semplicemente questa in cima al file:
my $Censo = 0;
Anche se il campo di visibilità del my() si estingue normalmente con il blocco nel quale è dichiarato (in questo caso l'intero file richiamato con use o require), il deep binding [legame profondo, NdT] del Perl per le variabili lessicali garantisce che la variabile non sarà deallocata, rimanendo accessibile alle funzioni dichiarate all'interno dello stesso ambito. Però ciò non funziona con variabili globali alle quali si diano dati valori temporanei con local().
Indipendentemente dal fatto che vogliate lasciare $Censo come una variabile globale al package o ne facciate una lessicale con campo di visibilità limitato al file, dovrete implementare questi cambiamenti nel vostro costruttore Persona::new() :
sub new {
my $classe = shift;
my $self = {};
$Censo++;
$self->{NOME} = undef;
$self->{ETA} = undef;
$self->{COMPAGNI} = [];
bless ($self, $classe);
return $self;
}
sub popolazione {
return $Censo;
}
Adesso che abbiamo fatto ciò, sicuramente necessitiamo di un distruttore per il quale, quando una Persona è distrutta, il $Censo diminuisce. Possiamo fare in questo modo:
sub DESTROY { --$Censo }
Notate come non ci sia memoria da deallocare nel distruttore? Il Perl si prende cura di questo compito tutto da solo.
In alternativa, potreste utilizzare il modulo Class::Data::Inheritable [Inheritable = Ereditabile, NdT] da CPAN.
Accedere ai dati di una classe
In fin dei conti questo non sembra proprio un buon modo per continuare a manipolare dati di una classe. Una buona regola scalabile è che non dovete mai riferire direttamente ai dati di una classe da un metodo di oggetto. Altrimenti non state costruendo una classe scalabile ed ereditabile. L'oggetto deve rappresentare il punto d'incontro per tutte le operazioni, specialmente da un metodo di oggetto. Le variabili globali (dati di classi) sarebbero in un certo senso nel "package" sbagliato della vostra classe derivata. In Perl, i metodi vengono eseguiti nel contesto della classe nella quale sono stati definiti, non in quello dell'oggetto che gli ha attivati. Per cui, la visibilità in termini di namespace delle variabili globali al package nei metodi non ha alcuna relazione con l'ereditarietà.
Tutto chiaro? Forse no. Ok, diciamo che qualche altra classe ha "preso in prestito" (beh, ereditato) il metodo DESTROY come è stato definito sopra. Quando questi oggetti sono distrutti, la variabile originale $Censo viene alterata, non quella nel namespace del package della nuova classe. Forse questo è ciò che volete, ma probabilmente non lo è.
Ecco come aggiustare il tutto. Immagazzineremo un riferimento ai dati nel valore acceduto dalla chiave hash "_CENSO". Perché il trattino basso? Beh, più che altro perché un trattino basso iniziale evoca già una forte sensazione di magico a un programmatore C. È solamente una costruzione mnemonica per ricordarci che questo campo è speciale e non deve essere utilizzato come dato membro pubblico come lo sono NOME, ETA, e COMPAGNI. (Poiché stiamo sviluppando questo codice sotto la direttiva strict, prima della versione 5.004 del Perl dobbiamo racchiudere tra doppi apici il nome del campo).
sub new {
my $classe = shift;
my $self = {};
$self->{NAME} = undef;
$self->{ETA} = undef;
$self->{COMPAGNI} = [];
# dati "privati"
$self->{"_CENSO"} = \$Censo;
bless ($self, $classe);
++ ${ $self->{"_CENSO"} };
return $self;
}
sub popolazione {
my $self = shift;
if (ref $self) {
return ${ $self->{"_CENSO"} };
} else {
return $Censo;
}
}
sub DESTROY {
my $self = shift;
-- ${ $self->{"_CENSO"} };
}
Metodi di Debug
Per una classe è tipico possedere un meccanismo di debug. Per esempio, potreste voler vedere quando gli oggetti vengono creati o distrutti. Per far ciò aggiungiamo una variabile lessicale di debug con campo di visibilità limitato al file. Per questo, richiamiamo il modulo standard Carp per trasmettere i nostri messaggi warning e fatal. In tal modo i messaggi verranno restituiti con il nome del file ed il numero di linea del chiamante invece del vostro; se avessimo voluto che fossero dalla nostra prospettiva, avremmo semplicemente utilizzato die() e warn() direttamente invece di croak() e carp() rispettivamente.
use Carp;
my $Debugging = 0;
Adesso aggiungiamo un nuovo metodo di classe per accedere alla variabile.
sub debug {
my $classe = shift;
if (ref $classe) { confess "Metodo di classe chiamato come metodo di oggetto" }
unless (@_ == 1) { confess "utilizzo: NOMECLASSE->debug(livello)" }
$Debugging = shift;
}
Adesso aggiustiamo il DESTROY in modo da farlo lamentare un po' quando l'oggetto moribondo si estingue:
sub DESTROY {
my $self = shift;
if ($Debugging) { carp "Distruggo $self " . $self->nome }
-- ${ $self->{"_CENSO"} };
}
Si potrebbe concepibilmente costruire uno stato di debug per ogni oggetto. In questo modo potreste chiamare entrambi:
Persona->debug(1); # l'intera classe
$egli->debug(1); # solo l'oggetto
Per far ciò, il nostro metodo di debug deve comportarsi in maniera "bimodale", funzionando per classi e per oggetti. Di conseguenza, aggiustiamo i metodi debug() e DESTROY come segue:
sub debug {
my $self = shift;
confess "utilizzo: coso->debug(livello)" unless @_ == 1;
my $livello = shift;
if (ref($self)) {
$self->{"_DEBUG"} = $level; # solo me stesso
} else {
$Debugging = $level; # l'intera classe
}
}
sub DESTROY {
my $self = shift;
if ($Debugging || $self->{"_DEBUG"}) {
carp "Distruggo $self " . $self->nome;
}
-- ${ $self->{"_CENSO"} };
}
Che cosa succede se una classe derivata (che chiameremo Impiegato) eredita metodi dalla classe di base Persona? Allora Impiegato->debug()
, quando chiamato come metodo di una classe, manipolerà $Persona::Debugging invece di $Impiegato::Debugging.
Distruttori di Classi
Il distruttore di oggetti gestisce la terminazione di ogni oggetto distinto. Ma talvolta è necessario fare un po' di pulizia quando l'intera classe viene messa fuori uso, che al momento succede solo quando il programma termina. Per costruire un tale distruttore di classe, create una funzione di nome END nel package della sua classe. Funziona esattamente come la funzione END nei moduli tradizionali, ovvero viene chiamata ogniqualvolta il vostro programma termina a meno che non avvenga una eccezione o muoia a causa di un "uncaught signal" [segnale non catturato, NdT]. Per esempio,
sub END {
if ($Debugging) {
print "Tutte le persone se ne vanno adesso.\n";
}
}
Quando il programma termina, tutti i distruttori di classi (le funzioni END) dovranno essere chiamate nell'ordine opposto di quello con cui sono state chiamate (ordinamento LIFO).
Documentare l'Interfaccia
Ed eccolo qua: vi abbiamo solo mostrato l'implementazione di questa classe Persona. La sua interfaccia sarà la sua documentazione. Di solito questo significa inserirla nel formato pod ("plain old documentation") esattamente lì nello stesso file. Nel nostro esempio Persona, metteremmo la seguente documentazione ovunque nel file Persona.pm. Anche se sembra principalmente codice, non lo è. è documentazione immersa del tipo che verrebbe utilizzato dai programmi pod2man, pod2html, o pod2text. Il compilatore del Perl ignora totalmente la documentazione pod, così come i traduttori ignorano il codice. Segue un esempio di qualche documentazione pod che descrive l'interfaccia formale:
=head1 NOME
Persona - classe per implementare gente
=head1 SINOSSI
use Persona;
#######################
# metodi della classe #
#######################
$og = Persona->new;
$conta = Persona->popolazione;
##############################
# metodi per dati di oggetto #
##############################
### ottenere versioni ###
$chi = $og->nome;
$anni = $og->eta;
@amici = $og->compagni;
### impostare versioni ###
$og->name("Gianni");
$og->age(23);
$og->compagni( "Norberto", "Raffaele", "Pino" );
###########################
# altri metodi di oggetto #
###########################
$frase = $og->esclama;
$og->buon_compleanno;
=head1 DESCRIZIONE
La classe Persona implementa bla bla bla bla....
Questo è tutto ciò da sapere sulle questioni di interfaccia e implementazione. Un programmatore che apre il modulo e si arrischia a compiere modifiche ai piccoli ingranaggi che si trovavano chiusi dal contratto di interfaccia ha annulla la garanzia, e non dovreste preoccuparvi della sua sorte.
Aggregazione
Supponiamo che più tardi vogliate cambiare la classe per implementare nomi migliori. Forse vorrete supportare sia nomi (o nomi di battesimo, senza riguardo per le rispettive religioni) che nomi di famiglia (o cognomi), oltre a soprannomi e titoli. Se gli utilizzatori della vostra classe Persona accedono correttamente ad essa attraverso la sua interfaccia documentata, allora potrete facilmente cambiare l'implementazione sottostante. Se non lo fanno, allora perdono ed è loro la colpa della rottura del contratto e dell'annullamento della garanzia.
Per far ciò, creiamo un'altra classe chiamata Nomecompleto. Come sarà la classe Nomecompleto? Per rispondere a questa domanda, dovete prima immaginare come volete usarla. Potreste farlo in questo modo:
$egli = Persona->new();
$egli->nomecompleto->titolo("San");
$egli->nomecompleto->battesimo("Tommaso");
$egli->nomecompleto->cognome("D'Aquino");
$egli->nomecompleto->soprannome("Tommy");
printf "Il suo nome normale E<egrave> %s\n", $egli->nome;
printf "Ma il suo nome vero E<egrave> %s\n", $egli->nomecompleto->come_stringa;
Ok. Per far ciò, cambiamo Persona::new() in modo che supporti un campo per il nome completo come segue:
sub new {
my $classe = shift;
my $self = {};
$self->{NOMECOMPLETO} = Nomecompleto->new();
$self->{ETA} = undef;
$self->{COMPAGNI} = [];
$self->{"_CENSO"} = \$Censo;
bless ($self, $classe);
++ ${ $self->{"_CENSO"} };
return $self;
}
sub nomecompleto {
my $self = shift;
return $self->{NOMECOMPLETO};
}
Poi per supportare vecchio codice, definiamo Persona::nome() in questo modo:
sub nome {
my $self = shift;
return $self->{NOMECOMPLETO}->soprannome(@_)
|| $self->{NOMECOMPLETO}->battesimo(@_);
}
Ecco la classe Nomecompleto. Sceglieremo la stessa tecnica di usare un riferimento ad un hash per mantenere i campi dei dati, e metodi con il nome appropriato per accederli:
package Nomecompleto;
use strict;
sub new {
my $classe = shift;
my $self = {
TITOLO => undef,
BATTESIMO => undef,
COGNOME => undef,
SOPRANNOME => undef,
};
bless ($self, $classe);
return $self;
}
sub battesimo {
my $self = shift;
if (@_) { $self->{BATTESIMO} = shift }
return $self->{BATTESIMO};
}
sub cognome {
my $self = shift;
if (@_) { $self->{COGNOME} = shift }
return $self->{COGNOME};
}
sub soprannome {
my $self = shift;
if (@_) { $self->{SOPRANNOME} = shift }
return $self->{SOPRANNOME};
}
sub titolo {
my $self = shift;
if (@_) { $self->{TITOLO} = shift }
return $self->{TITOLO};
}
sub come_stringa {
my $self = shift;
my $nome = join(" ", @$self{'BATTESIME<ograve>, 'COGNOME<egrave>});
if ($self->{TITOLO}) {
$nome = $self->{TITOLO} . " " . $nome;
}
return $nome;
}
1;
Infine, ecco il programma di test:
#!/usr/bin/perl -w
use strict;
use Persona;
sub END { mostra_censo() }
sub mostra_censo () {
printf "Popolazione attuale: %d\n", Persona->popolazione;
}
Persona->debug(1);
mostra_censo();
my $egli = Persona->new();
$egli->nomecompleto->battesimo("Thomas");
$egli->nomecompleto->cognome("Aquinas");
$egli->nomecompleto->soprannome("Tommy");
$egli->nomecompleto->titolo("St");
$egli->eta(1);
printf "%s E<egrave> veramente %s.\n", $egli->nome, $egli->nomecompleto->come_stringa;
printf "%s ha %d anni.\n", $egli->nome, $egli->eta;
$egli->buon_compleanno;
printf "%s ha %d anni.\n", $egli->nome, $egli->eta;
mostra_censo();
Ereditarietà
Tutti i sistemi di programmazione orientata agli oggetti supportano una qualche nozione di ereditarietà. Ereditarietà significa permettere a una classe di addossarsi su di un'altra in modo da non dover scrivere lo stesso codice ancora e ancora. Si tratta di riutilizzo di software, quindi collegato con la Pigrizia, la principale virtù di un programmatore. (I meccanismi di import/export nei moduli tradizionali sono anch'essi una forma di riutilizzo del codice, ma più semplice della vera ereditarietà che troverete nei moduli oggetto).
A volte la sintassi di ereditarietà è costruita nel nucleo del linguaggio, e a volte no. Il Perl non ha una sintassi speciale per specificare la classe (o le classi) dalla quale ereditare. Invece, sta tutto rigorosamente nella semantica. Ogni package può avere una variabile chiamata @ISA ["IS A" in inglese significa "È UN", NdT], che gestisce l'ereditarietà (dei metodi). Se provate a invocare un metodo di un oggetto o una classe, e quel metodo non si trova nel package di quell'oggetto, allora il Perl scandisce gli altri package in @ISA per andare in cerca del metodo perduto.
Come le variabili speciali dedicate ai package riconosciute da Exporter (tipo @EXPORT, @EXPORT_OK, @EXPORT_FAIL, %EXPORT_TAGS, e $VERSION), l'array @ISA deve essere globale con campo di visibilità esteso al package e non una variabile lessicale con campo di visibilità limitato al file, creata con my(). La maggior parte delle classi hanno solo un elemento nel loro array @ISA. In questo caso, abbiamo quel che si chiama "ereditarietà singola".
Si consideri la seguente classe:
package Impiegato;
use Persona;
@ISA = ("Persona");
1;
Non un gran che, eh? Tutto ciò che fa per ora è caricare un'altra classe e dichiarare che questa erediterà metodi dall'altra classe se necessario. Non le abbiamo dato nessuno dei suoi metodi. Contiamo sul fatto che un Impiegato si comporti come una Persona.
L'impostazione di una classe vuota, in questa maniera, si chiama "test per la sottoclasse vuota"; cioè, creare una classe derivata che non fa altro che ereditare da una classe di partenza. Se la classe di partenza originale è stata progettata correttamente, allora la nuova classe derivata può essere utilizzata direttamente come un rimpiazzo per quella vecchia. Ciò significa che dovreste essere in grado di scrivere un programma come questo:
use Impiegato;
my $impieg = Impiegato->new();
$impieg->nome("Gianni");
$impieg->eta(23);
printf "%s ha %d anni.\n", $impieg->nome, $impieg->eta;
Per correttezza di progettazione, intendiamo sempre utilizzare la forma a due argomenti di bless(), evitando l'accesso diretto a dati globali, e non esportando alcunché. Se tornate indietro a guardare la funzione Persona::new() che abbiamo definito, siamo stati cauti a farlo. Ci sono un po' di dati del package utilizzati nel costruttore, ma il riferimento per far ciò è archiviato nell'oggetto stesso e tutti gli altri metodi accedono ai dati del package attraverso quel riferimento, per cui dovremmo essere a posto.
Che cosa intendiamo per funzione Persona::new() -- non è in realtà un metodo? Beh, in principio sì. Un metodo è solo una funzione che accetta come primo argomento il nome di una classe (package) o un oggetto (riferimento al quale è stata applicata la funzione bless()). Persona::new() è la funzione che sia il metodo Persona->new()
che il metodo Impiegato->new()
finiscono per chiamare. Si osservi che benché una chiamata di metodo appaia quasi come una chiamata di funzione, esse non sono assolutamente la stessa cosa, e se trattate le due alla stessa maniera, rimarrete presto con niente di più che programmi che non funzionano. Anzitutto, le effettive convenzioni di chiamata sottostanti sono differenti: chiamate di metodi prendono un argomento extra. Secondariamente, le chiamate di funzione non effettuano l'ereditarietà mentre i metodi sì.
Chaiata di metodo Chiamata di funzione risultante
----------- ------------------------
Persona->new() Persona::new("Persona")
Impiegato->new() Persona::new("Impiegato")
Quindi non utilizzate chiamate di funzione quando intendete chiamare un metodo.
Se un impiegato è solo una Persona, questo non è molto interessante. Quindi aggiungiamo altri metodi. Daremo ai nostri impiegati campi di dato per accedere al loro salario, al loro identificatore di impiegato, e alla loro data di inizio.
Se siete un po' stanchi di creare tutti questi metodi quasi identici solo per accedere ai dati dell'oggetto, non disperate. Più tardi descriveremo molti comodi meccanismi per abbreviare tutto ciò. Nel frattempo, vediamo il modo diretto:
sub salario {
my $self = shift;
if (@_) { $self->{SALARIO} = shift }
return $self->{SALARIO};
}
sub id_numero {
my $self = shift;
if (@_) { $self->{ID} = shift }
return $self->{ID};
}
sub data_inizio {
my $self = shift;
if (@_) { $self->{DATA_INIZIO} = shift }
return $self->{DATA_INIZIO};
}
Metodi overridden [scavalcati, NdT]
Che cosa accade quando sia la classe derivata che la classe base definiscono lo stesso metodo? Beh, in questo caso ottenete la versione di quel metodo della classe derivata. Per esempio, vogliamo che il metodo compagni() invocato da un impiegato, agisca un po' diversamente. Invece di restituire la lista dei nomi dei compagni, restituiamo una stringa leggermente diversa. Così facendo, questo:
$impieg->compagni("Pietro", "Paolo", "Maria");
printf "I suoi compagni sono: %s\n", join(", ", $impieg->compagni);
produrrà:
I suoi compagni sono: COMPAGNO=PIETRO, COMPAGNO=PAOLO, COMPAGNO=MARIA
Per far ciò, aggiungete solamente questa definizione nel file Impiegato.pm:
sub compagni {
my $self = shift;
if (@_) { @{ $self->{COMPAGNI} } = @_ }
return map { "COMPAGNO=\U$_" } @{ $self->{COMPAGNI} };
}
Con questo, abbiamo appena dimostrato quel concetto altisonante conosciuto in alcuni ambienti come polimorfismo. Abbiamo prelevato la forma ed il comportamento di un oggetto esistente, e quindi l'abbiamo alterato per soddisfare le nostre necessità. Questa è una forma di Pigrizia. (Venire polimorfizzati è anche ciò che accade quando un mago decide che la forma di una rana sarebbe più adatta a voi).
Di quando in quando vorrete che la chiamata di un metodo attivi sia la versione della classe derivata (detta anche "sottoclasse") che la versione della classe base (detta anche "superclasse"). In pratica, costruttori e distruttori vogliono fare presumibilmente questo, ed ha anche senso, probabilmente, nel metodo debug() che abbiamo mostrato in precedenza.
Per far ciò, aggiungiamo questo a Impiegato.pm:
use Carp;
my $Debugging = 0;
sub debug {
my $self = shift;
confess "utilizzo: thing->debug(level)" unless @_ == 1;
my $livello = shift;
if (ref($self)) {
$self->{"_DEBUG"} = $livello;
} else {
$Debugging = $livello; # l'intera classe
}
Persona::debug($self, $Debugging); # non provate a farlo
}
Come vedete, continuiamo a chiamare la funzione debug() del package Persona. Ma ciò è troppo debole per una buona progettazione. Che accade se Persona non ha una funzione debug(), ma sta ereditando il suo metodo debug() da qualche altra parte? Sarebbe leggermente meglio fare
Persona->debug($Debugging);
Ma anche questo è eccessivamente hard-coded [cablato, NdT]. è alquanto meglio fare
$self->Persona::debug($Debugging);
che è uno strano modo per dire di iniziare a cercare il metodo debug() in Persona. Questa strategia viene usata più spesso per metodi di oggetto overridden che per metodi di classe overridden.
Qui c'è ancora qualcosa che va aggiustata. Abbiamo scritto per esteso il nome della superclasse. Questo in particolare non va bene se cambiate le classi dalle quali ereditate o ne aggiungete altre. Qui fortunatamente la pseudoclasse SUPER ci viene in aiuto.
$self->SUPER::debug($Debugging);
In questo modo inizia a cercare nella lista @ISA della mia classe. Comunque questo ha senso solamente all'interno di una chiamata di metodo. Non provate ad accedere a niente in SUPER:: da nessun altra parte, perché questo non esiste al di fuori di una chiamata di metodo overridden. Osservate che SUPER
si riferisce alla superclasse del package corrente, non alla superclasse di $self
.
Qui lee cose si stanno facendo un po' complicare. Abbiamo fatto qualcosa che non andava fatto? Come prima, un modo per verificare se stiamo progettando una classe decente è per mezzo di una sottoclasse vuota di test. Dato che abbiamo già una classe Impiegato che stiamo provando a verificare, sarebbe meglio avere una nuova sottoclasse vuota che deriva da Impiegato. Eccone una:
package Capo;
use Impiegato; # :-)
@ISA = qw(Impiegato);
E qui vediamo il programma di verifica:
#!/usr/bin/perl -w
use strict;
use Capo;
Capo->debug(1);
my $capo = Capo->new();
$capo->nomecompleto->titolo("Don");
$capo->nomecompleto->cognome("Pichon Alvarez");
$capo->nomecompleto->battesimo("Federico Jesus");
$capo->nomecompleto->soprannome("Fred");
$capo->eta(47);
$capo->compagni("Franco", "Felice", "Fausto");
printf "%s ha %d anni.\n", $capo->nomecompleto->come_stringa, $capo->age;
printf "I suoi compagni sono: %s\n", join(", ", $capo->compagni);
Eseguendolo, vedremo che tutto va ancora bene. Se volete scaricare dalla memoria il vostro oggetto in un formato carino, qualcosa come il modo in cui il comando 'x' funziona nel debugger, potete usare il modulo Data::Dumper da CPAN in questo modo:
use Data::Dumper;
print "Ecco il capo:\n";
print Dumper($capo);
Che ci mostra qualcosa del genere:
Ecco il capo:
$VAR1 = bless( {
_CENSO => \1,
NOMECOMPLETO => bless( {
TITOLO => 'Don',
COGNOME => 'Pichon Alvarez',
SOPRANNOME => 'Fred',
BATTESIMO => 'Federico Jesus'
}, 'Nomecompleto' ),
ETA => 47,
COMPAGNI => [
'Franco',
'Felice',
'Fausto'
]
}, 'CapE<ograve> );
Hm.... qui manca qualcosa. Dove sono il salario, la data di inizio, e il numero di identificazione? Beh, non abbiamo mai assegnato loro alcun valore, nemmeno undef, quindi non saltano fuori nelle chiavi dell'hash. La classe Impiegato non ha un metodo new() di per sé, ed il metodo new() in Persona è a conoscenza di Impiegati. (E nemmeno dovrebbe saperlo: la corretta progettazione orientata agli oggetti impone che a una sottoclasse sia permesso di conoscere la sua immediata superclasse, ma mai il contrario). Quindi aggiustiamo Impiegato::new() in questa maniera:
sub new {
my $class = shift;
my $self = $class->SUPER::new();
$self->{SALARIO} = undef;
$self->{ID} = undef;
$self->{DATA_INIZIO} = undef;
bless ($self, $class); # nuova abilitazione, "riconsacrazione"
return $self;
}
Adesso se scaricate dalla memoria un oggetto Impiegato o Capo, vedrete che adesso appariranno nuovi campi.
Ereditarietà Multipla
Ok, pur correndo il rischio di confondere i principianti e annoiare i guru della programmazione orientata agli oggetti, è tempo di confessare che il sistema degli oggetti del Perl include quella controversa nozione conosciuta come ereditarietà multipla, o per brevità EM. Ciò significa che invece di avere una sola classe padre che a sua volta può di per sé avere una classe padre, ecc., potete direttamente ereditare da due o più genitori. È vero che alcuni utilizzi di EM possono mettervi nei guai, sebbene si spera non del tutto con il Perl come con linguaggi orientati agli oggetti in modo dubbio come il C++.
Il modo in cui funziona in realtà è molto semplice: basta includere più nomi di package nela lista @ISA. Quando per il Perl viene il tempo di andare alla ricerca di metodi per il vostro oggetto, va a vedere nell'ordine in ognuno di questi package. Beh, una specie. È in realtà una prima ricerca in profondità (depth-first) totalmente ricorsiva. Considerate un gruppo di liste @ISA come queste:
@Prima::ISA = qw( Alfa );
@Seconda::ISA = qw( Beta );
@Terza::ISA = qw( Prima Seconda );
Se avete un oggetto della classe Terza:
my $ogg = Terza->new();
$ogg->gira();
Come troviamo un metodo gira (o un metodo new() per quel caso)? A causa del fatto che la ricerca è prima in profondità, le classi verranno scandite nel seguente ordine: Terza, Prima, Alfa, Seconda, e Beta.
In pratica, si sono visti pochi moduli di classe fare veramente uso della EM. Quasi sempre viene scelto un contenimento semplice di una classe anziché l'EM. Ecco perché il nostro oggetto Persona conteneva un oggetto Nomecompleto. Questo non significa che era un Nomecompleto.
Ad ogni modo, c'è un caso particolare nel quale l'EM in Perl è frequentemente utilizzata: quando si prendono in prestito i metodi di classe di un'altra classe. Questo è abbastanza comune specialmente se si ha a che fare con classi per le quali di solito non ci interessano gli oggetti di per sé, come Exporter, DynaLoader, AutoLoader, e SelfLoader. Queste classi non forniscono costruttori; esistono solo per il fatto che voi possiate far ereditare i loro metodi di classe. (Non è esattamente chiaro perché qui è stata utilizzata l'ereditarietà invece dell'inclusione tradizionale di moduli).
Per esempio, ecco l'array @ISA del modulo POSIX:
package POSIX;
@ISA = qw(Exporter DynaLoader);
Il modulo POSIX non è in realtà un modulo orientato agli oggetti, ma allora non lo sono neppure Exporter o DynaLoader. Danno solo in prestito i comportamenti delle loro classi a POSIX.
Perché l'ereditarietà non viene molto utilizzata per i metodi di oggetto? Una ragione è che può avere effetti collaterali complessi. Per prima cosa, il vostro grafo di ereditarietà (non più un albero) può confluire all'indietro alla stessa classe di base. Sebbene il Perl ci protegga dall'ereditarietà ricorsiva, avere semplicemente genitori che sono imparentati attraverso un avo un comune, sebbene suoni incestuoso, non è proibito. Cosa accadrebbe se nella nostra classe Terza di cui sopra volessimo che il metodo new() chiamasse anche entrambi i costruttori overridden nelle sue classi genitrici? La notazione SUPER troverebbe solo il primo. In più, che accadrebbe se entrambe le classi Alfa e Beta avessero un avo in comune, chiamato Nullo? Se continuaste a percorrere l'albero di ereditarietà chiamando metodi overridden, finireste col chiamare Nullo::new() due volte, il che potrebbe proprio essere una cattiva idea.
UNIVERSAL: La Radice di Tutti Gli Oggetti
Non sarebbe conveniente se tutti gli oggetti avessero come radice un'unica classe base in comune? In questo modo potreste dare ad ogni oggetto dei metodi comuni senza dover aggiungerli a tutti quelli inclusi nelle liste @ISA. Beh, pare che ciò sia possibile. Non lo potete vedere, ma il Perl assume tacitamente ed irrevocabilmente che ci sia un elemento extra alla fine di @ISA: la classe UNIVERSAL. Nella versione 5.003, non vi si trovava alcun metodo predefinito, ma potevate aggiungerne a vostro piacimento.
Comunque, dalla versione 5.004 (o qualche release sovversiva, come la 5.003_08), UNIVERSAL contiene già alcuni metodi. Questi sono integrati nel vostro codice oggetto del Perl, quindi non necessitano di tempo extra per il caricamento. I metodi predefiniti comprendono isa(), can(), e VERSION(). isa() vi dice se un oggetto o una classe "è" un'altra senza dover voi stessi attraversare la gerarchia:
$ha_io = $fd->isa("IO::Handle");
$discende_da_handle = IO::Socket->isa("IO::Handle");
Il metodo can(), chiamato in quell'oggetto o classe, ci informa se il suo argomento stringa è un metodo invocabile di quella classe. Infatti, restituisce un riferimento della funzione di quel metodo:
$il_suo_metodo_print = $ogg->can('come_stringE<agrave>);
Infine, il metodo VERSION controlla se la classe (o la classe dell'oggetto) ha una variabile globale di package chiamata $VERSION che sia grande abbastanza, come in:
Qualche_Modulo->VERSION(3.0);
$la_sua_versione = $ogg->VERSION();
Ad ogni modo, di solito non siamo noi stessi a chiamare VERSION. (Ricordate che il nome di una funzione totalmente maiuscolo è una convenzione del Perl per indicare che la funzione sarà automaticamente chiamata dal Perl in una qualche maniera). In questo caso, ciò accade quando scrivete:
use Qualche_Modulo 3.0;
Se volevate aggiungere il controllo della versione nella vostra classe Persona introdotta precedentemente, aggiungete questo a Persona.pm:
our $VERSION = '1.1';
e quindi in Impiegato.pm potete scrivere
use Persona 1.1;
E si assicurerà che abbiate almeno a disposizione quel numero di versione o più alto. Ciò non è lo stesso che caricare il numero di versione corretto. Non esiste al momento nessun meccanismo per l'installazione concorrente di versioni multiple di un modulo. Deplorevolmente.
Rappresentazioni Alternative di Oggetti
Non è obbligatorio implementare oggetti come riferimenti a hash. Un oggetto può essere qualsiasi tipo di riferimento fintantoché il suo referente sia stato debitamente consacrato con bless(). Ciò significa che uno scalare, una lista, e un riferimento a codice possono esserne il bersaglio.
Uno scalare funzionerebbe nel caso in cui l'oggetto abbia solo un semplice dato da mantenere. Un array potrebbe funzionare in molti casi, ma ciò implicherebbe complicazioni per l'ereditarietà dato che dovreste inventare nuovi indici per la classe derivata.
Array come oggetti
Se l'utilizzatore della vostra classe onora il contratto e rispetta l'interfaccia pubblicata, allora se ve la sentite potete cambiare la sua implementazione sottostante. Ecco qui un'altra implementazione che si adegua alle stesse specifiche di interfaccia. Questa volta utilizzeremo un riferimento ad una lista invece di un riferimento ad un hash per rappresentare un oggetto.
package Persona;
use strict;
my($NOME, $ETA, $COMPAGNI) = ( 0 .. 2 );
####################################################
## il costruttore di oggetto (versione con lista) ##
####################################################
sub new {
my $self = [];
$self->[$NOME] = undef; # questo non e` necessario
$self->[$ETA] = undef; # nemmeno questo
$self->[$COMPAGNI] = []; # ma questo non lo e`, veramente
bless($self);
return $self;
}
sub nome {
my $self = shift;
if (@_) { $self->[$NOME] = shift }
return $self->[$NOME];
}
sub eta {
my $self = shift;
if (@_) { $self->[$ETA] = shift }
return $self->[$ETA];
}
sub compagni {
my $self = shift;
if (@_) { @{ $self->[$COMPAGNI] } = @_ }
return @{ $self->[$COMPAGNI] };
}
1; # quindi il require o l'use hanno successo
Potete supporre che l'accesso alla lista sia molto più veloce dell'accesso all'hash, ma sono in realtà comparabili. L'array è leggermente più veloce, ma non più del dieci o quindici per cento, anche quando rimpiazzate le variabili come $ETA con numeri letterali, come 1. Una differenza maggiore tra i due approcci può trovarsi nell'utilizzo della memoria. La rappresentazione di un hash richiede più memoria della rappresentazione di un array perché è necessario allocare la memoria sia per le chiavi che per i valori. Ad ogni modo, non è così male, specialmente dalla versione 5.004, la memoria è allocata solo una volta per una data chiave di hash, qualunque sia il numero degli hash che abbiano quella chiave. Ci si aspetta che in un futuro non ben identificabile anche queste differenze si assottiglieranno per via del fatto che saranno escogitate rappresentazioni più efficienti.
Ancora, il piccolo margine in termini di velocità (e un po' più grande in termini di memoria) è abbastanza da convincere alcuni programmatori a scegliere la rappresentazione con array per classi semplici. C'è ancora un piccolo problema di scalabilità, però, perché più in là nella vita, quando ve la sentirete di creare sottoclassi, troverete semplicemente che gli hash funzionano meglio.
Chiusure come Oggetti
Utilizzare un riferimento a codice per rappresentare un oggetto offre delle possibilità affascinanti. Possiamo creare una nuova funzione anonima (chiusura) che da sola in tutto il mondo può vedere i dati dell'oggetto. Questo perché mettiamo i dati in un hash anonimo che è visibile lessicalmente solo alla chiusura che creiamo, consacriamo con bless(), e restituiamo come oggetto. I metodi di questo oggetto effettuano la chiamata della chiusura come una semplice chiamata di subroutine, passandole il campo che ci interessa. (Sì, la doppia chiamata di funzione è lenta, ma se volevate la velocità, non avreste utilizzato per niente gli oggetti, eh? :-)
L'utilizzo sarà simile al precedente:
use Persona;
$egli = Persona->new();
$egli->nome("Gianni");
$egli->eta(23);
$egli->compagni( [ "Norberto", "Raffaele", "Pino" ] );
printf "%s ha %d anni.\n", $egli->nome, $egli->eta;
print "I suoi compagni sono: ", join(", ", @{$egli->compagni}), "\n";
ma l'implementazione sarà radicalmente differente, forse anche in modo sublime:
package Persona;
sub new {
my $classe = shift;
my $self = {
NOME => undef,
ETA => undef,
COMPAGNI => [],
};
my $chiusura = sub {
my $campo = shift;
if (@_) { $self->{$campo} = shift }
return $self->{$campo};
};
bless($chiusura, $classe);
return $chiusura;
}
sub nome { &{ $_[0] }("NOME", @_[ 1 .. $#_ ] ) }
sub eta { &{ $_[0] }("ETA", @_[ 1 .. $#_ ] ) }
sub compagni { &{ $_[0] }("COMPAGNI", @_[ 1 .. $#_ ] ) }
1;
Dato che questo oggetto si nasconde dietro a un riferimento a codice, risulterà probabilmente un po' misterioso per coloro che sono abituati ai linguaggi di programmazione procedurali standard o orientati agli oggetti piuttosto che ai linguaggi di programmazione funzionali dai quali le chiusure provengono. L'oggetto creato e restituito dal metodo new() non è di per sé un riferimento a dei dati, come abbiamo visto in precedenza. È un riferimento anonimo a codice che ha al suo interno l'accesso a una specifica versione (binding lessicale e instanziazione) dei dati dell'oggetto, che sono immagazzinati nella variabile privata $self. Sebbene questa sia la stessa funzione ogni volta, contiene versioni differenti di $self.
Quando un metodo come $egli->nome("Gianni")
viene invocato, il suo argomento zero-esimo implicito è l'oggetto che invoca -- esattamente come per tutte le chiamate di metodi. Ma in questo caso è il nostro riferimento a codice (qualcosa come un puntatore a funzioni in C++, ma con deep binding di variabili lessicali). Non c'è molto da fare con un riferimento a codice oltre a invocarlo, quindi questo è semplicememnte ciò che facciamo quando diciamo &{$_[0]}
. Questa è semplicemente una normale chiamata di funzione, non una invocazione di metodo. L'argomento iniziale è la stringa "NOME", ed ogni altro argomento rappresenta tutto ciò che viene passato al metodo stesso.
Una volta che eseguiamo il codice all'interno della chiusura che è stata creata con new(), il riferimento all'hash $self diventa visibile. La chiusura carpisce il suo primo argomento ("NOME" in questo caso perché ciò è quanto il metodo nome() gli ha passato), ed utilizza tale stringa come indice nell'hash privato nascosto nella sua unica versione di $self.
Niente al mondo permetterà ad alcuno al di fuori del metodo in esecuzione di accedere a questi dati nascosti. Beh, quasi niente. Potreste percorrere a singoli passi il programma utilizzando il debugger e trovare i pezzi quando siete nel metodo, ma gli altri non hanno scampo.
Ecco, se questo non eccita gli appassionati di Scheme [un linguaggio multi-paradigma simile al Lisp, NdT], allora non so cosa lo farà. La traduzione di queste tecniche in C++, Java o in qualsiasi altro linguaggio statico-ottuso è lasciata come frivolo esercizio per gli affezionati di questi campi.
Potreste addirittura aggiungere un po' di stranezze per mezzo della funzione caller() e imporre alla chiusura di agire se non chiamata attraverso il suo stesso package. Questo indubbiamnte soddisferebbe qualche fastidiosa faccenda che riguarda i puristi della programmazione e puritani affini.
Se vi stavate chiedendo quando entra in gioco l'Arroganza, la terza principale virtù di un programmatore, qui lo vedrete. (Più seriamente, Arroganza è solo l'orgoglio della maestria che deriva dall'aver scritto un bel po' di codice ben progettato).
AUTOLOAD: Metodi Proxy
L'utilizzo dell'Autoload [autocaricamento, NdT] è un modo per intercettare chiamate a metodi non definiti. Una routine di autoload può scegliere di creare una nuova funzione sul posto, caricata da disco o forse solo valutata con eval() in loco. Questa strategia di definizione-sul-posto è il perché del nome autoload.
Ma questo è solo uno dei possibili approcci. Un altro è semplicemente di fare in modo che il metodo autocaricato stesso fornisca il servizio richiesto. Se utilizzato in questa maniera, potreste pensare ai metodi autocaricati come metodi "proxy".
Quando il Perl tenta di chiamare una funzione indefinita in un determinato package e non la trova, cerca una funzione di nome AUTOLOAD in quello stesso package. Se esiste, viene chiamata dandole gli stessi parametri della funzione originale. Il nome della funzione per intero è salvato nella variabile globale di nome $AUTOLOAD dello stesso package. Una volta chiamata, la funzione può fare ciò che vuole, compreso definire una nuova funzione con il nome giusto, e poi fare una cosa elegante tipo un goto
ad essa, eliminando se stessa dallo stack di chiamata.
Che cosa ha che fare questo con gli oggetti? Dopo tutto, stiamo sempre parlando di funzioni, non metodi. Beh, dato che un metodo è semplicemente una funzione con un argomento extra e un po' di eleganza semantica riguardo alla sua localizzazione, possiamo utilizzare la tecnica di autoload anche per i metodi. Comunque, il Perl non inizia a cercare il metodo AUTOLOAD finché non ha esaurito la ricerca ricorsiva attraverso @ISA. Alcuni programmatori hanno anche definito un metodo UNIVERSAL::AUTOLOAD per intrappolare chiamate di metodo non risolte per ogni tipo di oggetto.
Autoload per Metodi di Dati
Probabilmente inizierete ad essere un po' sospettosi riguardo al modo in cui il codice veniva duplicato quando vi avevamo mostrato la classe Persona, ed in seguito la classe Impiegato. I metodi utilizzati per accedere ai campi dell'hash apparivano virtualmente identici. Questo avrebbe dovuto stuzzicare quella grande virtù del programmatore, l'Impazienza, ma al momento la Pigrizia aveva vinto, e così non ci sono state conseguenze. I metodi proxy possono aggiustare la situazione.
Invece di scrivere una nuova funzione ogni volta che vogliamo un nuovo campo dati, utilizziamo il meccanismo dell'autoload per generare (in realtà imitare) metodi sul posto. Per verificare che stiamo accedendo ad un membro valido, controlleremo con un campo _consentito
, che è un riferimento a un hash lessicale con campo di visibilità limitato al file (come un C file static) di campi consentiti in questo record di nome %campi. Perché il trattino basso? Per la stesso motivo per il quale lo abbiamo utilizzato per _CENSO: come un promemoria che significa "solo per uso interno".
Ecco come appariranno il codice per l'inizializzazione del modulo ed il costruttore di classe utilizzando quest'approccio:
package Persona;
use Carp;
our $AUTOLOAD; # e` globale rispetto al package
my %campi = (
nome => undef,
eta => undef,
compagni => undef,
);
sub new {
my $classe = shift;
my $self = {
_consentito => \%campi,
%campi,
};
bless $self, $classe;
return $self;
}
Se volessimo valori di default per il nostro record, potremmo inserirli dove abbiamo scritto undef
nell'hash %campi.
Avete notato come abbiamo salvato un riferimento ai dati della nostra classe nell'oggetto stesso? Ricordate che è importante accedere ai dati della classe attraverso l'oggetto stesso invece di permettere ad ogni metodo di accedere direttamente a %campi, altrimenti non potreste avere una ereditarietà decente.
Però, la vera magia va a risiedere nel nostro metodo proxy, che gestirà tutte le chiamate a metodi non definiti per gli oggetti della classe Persona (o sottoclassi di Persona): è chiamato AUTOLOAD. Ancora, è scritto tutto in maiuscolo perché viene invocato implicitamente dal Perl stesso, non direttamente dall'utilizzatore.
sub AUTOLOAD {
my $self = shift;
my $tipo = ref($self)
or croak "$self non e` un oggetto";
my $nome = $AUTOLOAD;
$nome =~ s/.*://; # toglie il pezzo relativo ai nomi dei package
unless (exists $self->{_consentito}->{$nome} ) {
croak "Non posso accedere al campo `$nome' nella classe $tipo";
}
if (@_) {
return $self->{$nome} = shift;
} else {
return $self->{$nome};
}
}
Eccezionale vero? Tutto ciò che dobbiamo fare per aggiungere nuovi campi dati è modificare %campi. Non dobbiamo scrivere nessuna nuova funzione.
Avrei potuto evitare totalmente il campo _consentito
, ma volevo dimostrare come immagazzinare un riferimento ai dati di una classe nell'oggetto così da non dover accedere ai dati della classe direttamente da un metodo dell'oggetto.
Metodi per dati con autoload ereditato
Per quanto riguarda l'ereditarietà? Possiamo definire similmente la nostra classe Impiegato? Sì, se continuiamo ad aver cautela.
Ecco come essere cauti:
package Impiegato;
use Persona;
use strict;
our @ISA = qw(Persona);
my %campi = (
id => undef,
salario => undef,
);
sub new {
my $classe = shift;
my $self = $class->SUPER::new();
my($elemento);
foreach $elemento (keys %campi) {
$self->{_consentito}->{$elemento} = $campi{$elemento};
}
@{$self}{keys %campi} = values %campi;
return $self;
}
Una volta che abbiamo fatto ciò, non abbiamo nemmeno più bisogno di una funzione AUTOLOAD nel package Impiegato, perché cattureremo la versione di Persona attraverso l'ereditarietà, e funzionerà tutto alla perfezione.
Strumenti metaclassici
Anche se i metodi proxy possono fornire un approccio più conveniente per rendere le classi più simili a struct invece che scrivere in modo tedioso metodi per dati come funzioni, lascia sempre un po' a desiderare. Una cosa è che significa dover gestire chiamate senza senso che non intendete catturare tramite il vostro proxy. Significa anche che dovete essere molto cauti nel gestire l'ereditarietà, come spiegato sopra.
I programmatori Perl hanno affrontato la questione creando diverse classi per costruire classi. Queste metaclassi sono classi che creano altre classi. Un paio di queste che meritano attenzione sono Class::Struct e Alias. Queste e altre metaclassi collegate si possono trovare nella directory dei moduli su CPAN.
Class::Struct
Una delle più vecchie è Class::Struct. Infatti, la sua sintassi e la sua interfaccia vennero abbozzate molto prima che la versione numero 5 del Perl si materializzasse. Quel che fa è darvi un modo per "dichiarare" una classe per avere oggetti i quali campi siano di un tipo specifico. La funzione che fa ciò si chiama, non abbastanza sorprendentemente, struct(). Dato che le strutture o i record non sono tipi base in Perl, ogni volta che volete creare una classe per fornire oggetti con dati simili a record, voi stessi dovrete definire un metodo new(), e in più dovrete separare i metodi per l'accesso ai dati per ogni campo di tale record. Vi annoierete presto di questo precedimento. La funzione Class::Struct::struct() allevia questo tedio.
Ecco un semplice esempio del suo utilizzo:
use Class::Struct qw(struct);
use Lavoretto; # definito dall'utente; vedi sotto
struct 'FedericE<ograve> => {
uno => '$',
molti => '@',
professione => 'LavorettE<ograve>, # non invoca Lavoretto->new()
};
$ob = Federico->new(professione => Lavoretto->new());
$ob->uno("hmmmm");
$ob->molti(0, "alla");
$ob->molti(1, "via");
$ob->molti(2, "cosi`");
print "Imposta solo: ", $ob->molti(2), "\n";
$ob->professione->salario(10_000);
Potete dichiarare dei tipi nello struct in modo che siano tipi di base del Perl o tipi definiti dall'utente (classi). I tipi dell'utente saranno inizializzati chiamando il metodo new() di quella classe.
Osservate che l'oggetto Lavoretto
non viene creato automaticamente dal metodo new() della classe Federico
, quindi dovete specificare un oggetto Lavoretto
quando create un'istanza di Federico
.
Ecco un esempio reale di utilizzo con la generazione di stuct. Mettiamo che vogliate sostituire l'idea del Perl di gethostbyname() e gethostbyaddr() in modo che restituiscano oggetti che si comportano come le strutture del C. Non ci interessano dettagli complessi della programmazione orientata agli oggetti. Tutto ciò che vogliamo per questi oggetti è che essi si comportino come struct nell'accezione del C.
use Socket;
use Net::hostent;
$h = gethostbyname("perl.com"); # restituisce l'oggetto
printf "Il vero nome di perl.com's e` %s, indirizzo %s\n",
$h->name, inet_ntoa($h->addr);
Ecco come far ciò utilizzando il modulo Class::Struct. Il nocciolo della questione sarà questa chiamata:
struct 'Net::hostent' => [ # osservate le parentesi quadre
name => '$',
aliases => '@',
addrtype => '$',
'length' => '$',
addr_list => '@',
];
la quale crea metodi di oggetto con nomi e tipi elencati. Crea inoltre per noi un metodo new().
Avremmo potuto anche implementare il nostro oggetto in questa maniera:
struct 'Net::hostent' => { # osservate le parentesi graffe
name => '$',
aliases => '@',
addrtype => '$',
'length' => '$',
addr_list => '@',
};
in modo che Class::Struct utilizzasse un hash anonimo come tipo di oggetto, anziché array anonimo. L'array è più veloce e più piccolo ma l'hash funziona meglio se ad un certo punto volete implementare l'ereditarietà. Dato che per quest'oggetto simile a uno struct si prevede l'ereditarietà, questa volta opteremo per velocità e dimensioni migliori a scapito della flessibilità.
Ecco l'intera implementazione:
package Net::hostent;
use strict;
BEGIN {
use Exporter ();
our @EXPORT = qw(gethostbyname gethostbyaddr gethost);
our @EXPORT_OK = qw(
$h_name @h_aliases
$h_addrtype $h_length
@h_addr_list $h_addr
);
our %EXPORT_TAGS = ( FIELDS => [ @EXPORT_OK, @EXPORT ] );
}
our @EXPORT_OK;
# Class::Struct vieta l'utilizzo di @ISA
sub import { goto &Exporter::import }
use Class::Struct qw(struct);
struct 'Net::hostent' => [
name => '$',
aliases => '@',
addrtype => '$',
'length' => '$',
addr_list => '@',
];
sub addr { shift->addr_list->[0] }
sub populate (@) {
return unless @_;
my $hob = new(); # Questo viene fatto da Class::Struct!
$h_name = $hob->[0] = $_[0];
@h_aliases = @{ $hob->[1] } = split ' ', $_[1];
$h_addrtype = $hob->[2] = $_[2];
$h_length = $hob->[3] = $_[3];
$h_addr = $_[4];
@h_addr_list = @{ $hob->[4] } = @_[ (4 .. $#_) ];
return $hob;
}
sub gethostbyname ($) { populate(CORE::gethostbyname(shift)) }
sub gethostbyaddr ($;$) {
my ($addr, $addrtype);
$addr = shift;
require Socket unless @_;
$addrtype = @_ ? shift : Socket::AF_INET();
populate(CORE::gethostbyaddr($addr, $addrtype))
}
sub gethost($) {
if ($_[0] =~ /^\d+(?:\.\d+(?:\.\d+(?:\.\d+)?)?)?$/) {
require Socket;
&gethostbyaddr(Socket::inet_aton(shift));
} else {
&gethostbyname;
}
}
1;
Abbiamo intrufolato un bel po' di altri concetti accanto alla semplice creazione dinamica della classe, del tipo sostituzione di funzioni di base, qualche importazione/esportazione, prototipazione di funzioni, scorciatoie a chiamate di funzioni con &qualcosa
, e rimpiazzo di funzioni con goto &qualcosa
. Tutto ciò ha un senso dalla prospettiva dei moduli tradizionali, ma come potete vedere, possiamo anche utilizzarli in un modulo a oggetti.
Potete esaminare altre sostituzioni di funzioni di base implementate con oggetti e simili a struct nella release 5.004 del Perl in File::stat, Net::hostent, Net::netent, Net::protoent, Net::servent, Time::gmtime, Time::localtime, User::grent, e User::pwent. Questi moduli hanno un componente finale interamente in minuscolo, per convenzione riservato alle direttive del compilatore, dato che influiscono sulla compilazione e cambiano una funzione integrata. Hanno anche tipi di nomi che un programmatore C si aspetterebbe.
Dati membro come variabili
Se avete familiarità con gli oggetti del C++, allora siete abituati ad accedere ai dati membro di un oggetto come semplici variabili da un metodo. Il modulo Alias provvede a questo e anche a un bel po' di più, come alla disponibilità di metodi privati che possono essere invocati dall'oggetto ma non da chi è al di fuori di esso.
Ecco un esempio di creazione di una Persona utilizzando il modulo Alias. Quando aggiornate queste magiche variabili di istanza, i valori degli hash vengono aggiornati automaticamente. Conveniente, vero?
package Persona;
# questo E<egrave> lo stesso di prima...
sub new {
my $classe = shift;
my $self = {
NOME => undef,
ETA => undef,
COMPAGNI => [],
};
bless($self, $classe);
return $self;
}
use Alias qw(attr);
our ($NOME, $ETA, $COMPAGNI);
sub nome {
my $self = attr shift;
if (@_) { $NOME = shift; }
return $NOME;
}
sub eta {
my $self = attr shift;
if (@_) { $ETA = shift; }
return $ETA;
}
sub compagni {
my $self = attr shift;
if (@_) { @COMPAGNI = @_; }
return @COMPAGNI;
}
sub esclama {
my $self = attr shift;
return sprintf "Ciao, sono %s, ho %d anni, lavoro con %s",
$NOME, $ETA, join(", ", @COMPAGNI);
}
sub buon_compleanno {
my $self = attr shift;
return ++$ETA;
}
L'esigenza della dichiarazione con our
deriva dal fatto che Alias manipola variabili globali al package con lo stesso nome dei campi. Per utilizzare variabili globali mentre use strict
è effettivo dovete dichiararle in precedenza. Queste variabili di package sono localizzate nel blocco che racchiude la chiamata ad attr() come se si stia utilizzando un local() su di esse. Ad ogni modo, ciò significa che sono sempre considerate variabili globali con valori temporanei, proprio come ogni altra local().
Sarebbe carino combinare Alias con qualcosa del tipo Class::Struct o Class::MethodMaker.
ANNOTAZIONI
Terminologia degli oggetti
Nella varia letteratura sulla programmazione orientata agli oggetti, sembra che siano utilizzate molte parole differenti per descrivere solamente pochi concetti distinti. Se non siete già un programmatore di oggetti, allora non dovete preoccuparvi di tutte queste parole eleganti. Ma se lo siete, allora potreste voler sapere come ottenere gli stessi concetti in Perl.
Per esempio, è comune chiamare un oggetto una istanza di una classe e chiamare i metodi di questi oggetti metodi di istanza. I campi dati relativi a ogni oggetto sono spesso chiamati dati di istanza o attributi di oggetto, e i campi dati comuni a tutti i membri di una classe sono dati di classe, attributi di classe, oppure membri dati statici.
Inoltre, classe base, classe generica e superclasse denotano tutti la stessa nozione, mentre classe derivata, classe specifica e subclasse denotano le altre imparentate.
I programmatori C++ hanno metodi statici e metodi virtuali, ma il Perl ha solamente metodi di classe e metodi di oggetto. In realtà il Perl ha solo i metodi. Se un metodo viene usato per una classe o per un oggetto è solo a seconda del suo impiego. Potreste accidentalmente chiamare un metodo di classe (che si aspetta un argomento stringa) su un oggetto (che si aspetta un riferimento) o viceversa.
Da una prospettiva C++, tutti i metodi del Perl sono virtuali. Questo, a proposito, è il perché non viene mai effettuata la verifica con i prototipi delle funzioni nella lista degli argomenti mentre ciò si può fare con funzioni definite dall'utente.
Dato che una classe è di per sé qualcosa di un oggetto, le classi del Perl possono essere considerate come rappresentazione sia di una filosofia "classe come meta-oggetto" (chiamata anche fabbrica di oggetti) che di un idea di "classe come definizione di tipo" (comportamento dichiarante, non meccanismo definente). Il C++ supporta quest'ultima nozione, ma non la precedente.
SI VEDA ANCHE
I file di documentazione seguenti forniranno indubbiamente una maggiore conoscenza di base per questo argomento:
perlmod, perlref, perlobj, perlbot, perltie, e overload.
perlboot è un'introduzione più gradevole e più garbata alla programmazione orientata agli oggetti.
perltooc fornisce più dettagli sui dati di classi.
Alcuni moduli che potrebbero risultare interessanti sono Class::Accessor, Class::Class, Class::Contract, Class::Data::Inheritable, Class::MethodMaker e Tie::SecureHash
AUTORE E COPYRIGHT
Copyright (c) 1997, 1998 Tom Christiansen Tutti i diritti sono riservati.
Questa documentazione è gratuita; può essere distribuita e/o modificata secondo gli stessi termini del Perl.
Indipendentemente dalla sua distribuzione, tutto il codice di esempio in questo documento è di pubblico dominio. Avete il permesso e siete incoraggiati a usare il codice nei vostri programmi per svago o per profitto come ritenete opportuno. Un semplice commento nel codice per citare l'autore sarebbe cortese ma non è richiesto.
Ringraziamenti
Grazie a Larry Wall, Roderick Schertler, Gurusamy Sarathy, Dean Roehrich, Raphael Manfredi, Brent Halsey, Greg Bacon, Brad Appleton, e a molti altri per i loro utili commenti.
TRADUZIONE
Versione
La versione su cui si basa questa traduzione è ottenibile con:
perl -MPOD2::IT -e print_pod perltoot
Per maggiori informazioni sul progetto di traduzione in italiano si veda http://pod2it.sourceforge.net/ .
Traduttore
Traduzione a cura di Raffaello Galli <galliraf at googlemail punto com>.
Revisore
Revisione a cura di dree.