Oggetti

Programmare è un'attività di gestione. Più il programma è lungo, più dettagli vi trovate a dover gestire. L'unica possibilità di gestire con successo una tale complessità è un opportuno uso dell'astrazione (ossia la capacità di trattare cose simili in modo simile) e dell'incapsulamento (ossia il raggruppamento di dettagli che sono in relazione tra loro).

Il solo uso delle funzioni non è sufficiente per i programmi più grandi. Ci sono diverse tecniche per raggruppare le funzioni in unità di comportamenti correlati. Una di queste tecniche è l'orientamento agli oggetti (OO), o programmazione orientata agli oggetti (OOP), secondo la quale i programmi lavorano su oggetti—entità univoche e discrete con una propria identità.

Moose

Il sistema ad oggetti di default di Perl 5 è flessibile, ma minimale. Anche se potete creare cose eccellenti a partire da esso, tale sistema vi fornisce poca assistenza anche per i compiti basilari. Moose è un sistema ad oggetti completo per Perl 5 Vedete perldoc Moose::Manual per maggiori informazioni.. Fornisce dei default più semplici e delle funzionalità avanzate mutuate da linguaggi quali Smalltalk, Common Lisp e Perl 6. Il codice Moose è interoperabile con il sistema ad oggetti di default e rappresenta attualmente il modo migliore di scrivere codice orientato agli oggetti in Perl 5 moderno.

Classi

Un oggetto Moose è un'istanza concreta di una classe, che a sua volta è un modello che descrive i dati e il comportamento specifici dell'oggetto. Le classi usano i package (Package) per fornire dei namespace:

    package Gatto
    {
        use Moose;
    }

Questa classe Gatto sembrerebbe non fare nulla, ma questo è tutto ciò di cui Moose ha bisogno per definire una classe. Potete creare oggetti (o istanze) della classe Gatto con la seguente sintassi:

    my $brad = Gatto->new();
    my $jack = Gatto->new();

Così come può dereferenziare un riferimento, una freccia può invocare un metodo su un oggetto o su una classe.

Metodi

Un metodo è una funzione associata a una classe. Così come le funzioni appartengono a dei namespace, i metodi appartengono a delle classi, con due differenze. Per prima cosa, un metodo opera sempre su un invocante. Chiamare new() su Gatto invia un messaggio alla classe Gatto. Il nome della classe, Gatto, è l'invocante di new(). Quando chiamate un metodo su un oggetto, l'invocante è l'oggetto stesso:

    my $choco = Gatto->new();
    $choco->dormi_sulla_tastiera();

In secondo luogo, una chiamata a un metodo coinvolge sempre una strategia di dispatch, per mezzo della quale il sistema ad oggetti sceglie il metodo appropriato. Data la semplicità della classe Gatto, la strategia di dispatch è ovvia, ma gran parte della potenza dell'OO deriva da questa idea.

All'interno di un metodo, il primo argomento è l'invocante. Il nome idiomatico usato in Perl 5 è $self. Considerate che un Gatto (normalmente) miagola():

    package Gatto
    {
        use Moose;

        sub miagola
        {
            my $self = shift;
            say 'Miao!';
        }
    }

Ora tutte le istanze di Gatto possono svegliarvi al mattino perché non hanno ancora mangiato:

    my $strana_sveglia = Gatto->new();
    $strana_sveglia->miao() for 1 .. 3;

I metodi che accedono ai dati dell'invocante sono metodi d'istanza, perché dipendono dalla presenza dell'invocante appropriato per funzionare correttamente. I metodi (come miagola()) che non accedono ai dati d'istanza sono metodi di classe. Potete invocare i metodi di classe sulle classi e i metodi di classe e di istanza sulle istanze, ma non potete invocare metodi di istanza sulle classi.

I costruttori, che creano le istanze, sono ovviamente dei metodi di classe. Moose vi fornisce un costruttore di default.

I metodi di classe sono a tutti gli effetti delle funzioni globali di namespace. Dato che non accedono ai dati d'istanza, offrono pochi vantaggi rispetto alle funzioni di namespace. Gran parte del codice OO è giustamente basato sui metodi d'istanza, che hanno accesso ai dati d'istanza.

Attributi

Ciascun oggetto Perl 5 è unico. Gli oggetti possono contenere dei dati privati associati a ciascun singolo oggetto—detti attributi, dati d'istanza o stato dell'oggetto. Per definire un attributo, dichiaratelo come parte della classe:

    package Gatto
    {
        use Moose;

        has 'nome', is => 'ro', isa => 'Str';
    }

Parafrasato in italiano, questo codice dice che che "gli oggetti Gatto hanno un attributo nome. Tale attributo è di sola lettura ed è una stringa".

Moose fornisce la funzione has(), che dichiara un attributo. Il primo argomento, 'nome' nel nostro esempio, è il nome dell'attributo. La coppia di argomenti is => 'ro' specifica che l'attributo è read only, ovvero che non potete modificarlo dopo averlo inizializzato. Infine, la coppia isa => 'Str' specifica che il valore dell'attributo può essere solo una stringa.

In conseguenza all'esecuzione di has, Moose crea un metodo accessore chiamato nome() e vi permette di passare un parametro nome al costruttore di Gatto:

    for my $nome (qw( Tuxie Petunia Daisy ))
    {
        my $gatto = Gatto->new( nome => $nome );
        say "Creato un gatto chiamato ", $gatto->nome();
    }

Se passate qualcosa che non sia una stringa, Moose avrà da ridire. Va però ricordato che gli attributi non devono necessariamente avere un tipo. In tal caso, accettano tutto:

    package Gatto
    {
        use Moose;

        has 'nome', is => 'ro', isa => 'Str';
        has 'eta',  is => 'ro';
    }

    my $sbagliato = Gatto->new( nome => 'micio',
                                eta  => 'bizzarro' );

Specificando un tipo permettete a Moose di effettuare delle validazioni sui dati al posto vostro. Talvolta i benefici di una maggior rigidità sono inestimabili.

Se dichiarate un attributo come leggibile e scrivibile (con is => rw), Moose crea un metodo mutatore che può modificare il valore dell'attributo:

    package Gatto
    {
        use Moose;

        has 'nome', is => 'ro', isa => 'Str';
        has 'eta',  is => 'ro', isa => 'Int';
        has 'dieta', is => 'rw';
    }

    my $grasso = Gatto->new( nome  => 'Ciccio',
                           eta   => 8,
                           dieta => 'Frutti di Mare' );

    say $grasso->nome(), ' mangia ', $grasso->dieta();

    $grasso->dieta( 'Crocchette a Basso Contenuto di Sodium' );
    say $grasso->nome(), ' ora mangia ', $grasso->dieta();

L'uso di un accessore ro come mutatore solleva l'eccezione Cannot assign a value to a read-only accessor at ... (NdT: "Non posso assegnare un valore a un accessore di sola lettura ...").

L'uso di ro oppure di rw è determinato da scelte di progetto, di convenienza e di purezza di stile. Moose non impone alcuna filosofia particolare a questo riguardo. Alcune persone suggeriscono di rendere ro tutti i dati d'istanza, in modo che dobbiate passare tali dati d'istanza al costruttore (Immutabilità). Nell'esempio della classe Gatto, eta() potrebbe continuare ad essere un accessore, ma il costruttore potrebbe ricevere l'anno di nascita del gatto e calcolare automaticamente l'età in base all'anno corrente. Questo approccio rafforza il codice di validazione e garantisce che tutti gli oggetti creati abbiano dati validi.

I dati d'istanza sono una prima dimostrazione dei benefici dell'orientamento agli oggetti. Un oggetto contiene dati correlati e può effettuare operazioni con tali dati. Una classe descrive sia i dati che le operazioni associate.

Incapsulamento

Moose vi permette di dichiarare quali sono gli attributi delle istanze di una classe (un gatto ha un nome) e quali sono gli attributi di tali attributi (non potete modificare il nome di un gatto; potete solo leggerlo). Moose stesso decide come memorizzare tali attributi. Se volete potete cambiare questo comportamento, ma permettere a Moose di gestire per voi la memorizzazione incoraggia l'incapsulamento, ovvero la capacità di mantenere i dettagli interni di un oggetto nascosti agli utenti esterni di tale oggetto.

Considerate un cambiamento nel modo un cui un Gatto gestisce la propria età. Invece di passare al costruttore un valore per l'età, dovete passare l'anno di nascita del gatto e calcolare l'età quando serve:

    package Gatto
    {
        use Moose;

        has 'nome',        is => 'ro', isa => 'Str';
        has 'dieta',       is => 'rw';
        has 'anno_nascita',  is => 'ro', isa => 'Int';

        sub eta
        {
            my $self = shift;
            my $anno = (localtime)[5] + 1900;

            return $anno - $self->anno_nascita();
        }
    }

Mentre è cambiata la sintassi per creare gli oggetti Gatto, la sintassi per usarli è rimasta invariata. Al di fuori di Gatto, eta() funziona esattamente come ha sempre fatto. Come essa funzioni internamente è un dettaglio interno alla classe Gatto.

Compatibilità e API

Potete preservare la vecchia sintassi per creare oggetti Gatto personalizzando il costruttore generato per Gatto in modo da permettere il passaggio di un parametro eta. Con esso, potete poi calcolare anno_nascita. Vedete perldoc Moose::Manual::Attributes.

Il calcolo dell'età ha un altro vantaggio. Un valore di default dell'attributo può aiutare a fare la cosa giusta quando qualcuno crea un nuovo oggetto Gatto senza passare un anno di nascita:

    package Gatto
    {
        use Moose;

        has 'nome',  is => 'ro', isa => 'Str';
        has 'dieta', is => 'rw', isa => 'Str';

        has 'anno_nascita',
            is      => 'ro',
            isa     => 'Int',
            default => sub { (localtime)[5] + 1900 };
    }

La parola chiave default nella dichiarazione di un attributo riceve un riferimento a funzione Potete anche usare direttamente un valore semplice come un numero o una stringa, ma usate un riferimento a funzione se avete bisogno di cose più complesse. che restituisce il valore di default per tale attributo quando viene costruito un nuovo oggetto. Se il codice che crea un oggetto non passa alcun valore per quell'attributo al costruttore, viene usato il valore di default. Se scrivete:

    my $gattino = Gatto->new( nome => 'Choco' );

... il nuovo gattino avrà età 0 fino al prossimo anno.

Polimorfismo

L'incapsulamento è utile, ma la vera potenza dell'orientamento agli oggetti è molto più ampia. Un programma OO ben progettato può gestire molti tipi di dati. Quando delle classi ben progettate incapsulano i dettagli specifici degli oggetti in modo appropriato, succede qualcosa di curioso: spesso il codice diventa meno specifico.

Definire i dettagli di ciò che il programma sa di ciascun Gatto (gli attributi) e di ciò che un Gatto può fare (i metodi) all'interno della classe Gatto permette al codice che deve gestire le istanze di Gatto di ignorare come un Gatto fa ciò che fa.

Considerate una funzione che visualizza i dettagli di un oggetto:

    sub mostra_stat_vitali
    {
        my $oggetto = shift;

        say 'Il mio nome e` ', $oggetto->nome();
        say 'La mia eta` e` ', $oggetto->eta();
        say 'Mangio ',         $oggetto->dieta();
    }

È ovvio che, dato il presente contesto, questa funzione opera correttamente su un oggetto Gatto. In effetti, essa opera correttamente su qualunque oggetto che abbia i tre accessori appropriati, indipendente da come tale oggetto fornisce i tre accessori e dal tipo dell'oggetto: Gatto, Bruco o PesceGatto. La funzione è sufficientemente generica da accettare come parametro valido qualunque oggetto che rispetti questa interfaccia.

La proprietà del polimorfismo significa che potete sostituire un oggetto di una classe con un oggetto di un'altra classe purché forniscano la stessa interfaccia esterna.

Duck Typing

Alcuni linguaggi e ambienti richiedono che esista una relazione formale tra due classi per permettere a un programma di sostituire le istanze di una con quelle dell'altra. Perl 5 fornisce dei modi di imporre questi controlli, ma non li richiede. Per il suo sistema ad-hoc di default è sufficiente che due istanze qualunque abbiano metodi con lo stesso nome per poterle trattare come equivalenti. Alcune persone lo definiscono duck typing, sostenendo che qualunque oggetto che può fare qua() è sufficientemente simile a un'anatra perché possiate trattarlo effettivamente come un'anatra.

mostra_stat_vitali() si preoccupa soltanto che un invocante valido supporti tre metodi—nome(), eta() e dieta()—che non ricevono argomenti e restituiscono qualcosa che può essere concatenato in contesto stringa. Potreste avere centinaia di classi diverse nel vostro codice, non legate da relazioni ovvie, che funzionano tutte con questo metodo purché si conformino al comportamento atteso.

Immaginate di dover passare in rassegna tutti gli animali di uno zoo senza poter usare una funzione polimorfa. I benefici della genericità dovrebbero essere evidenti. Inoltre, gli eventuali dettagli che differenziano il calcolo dell'età di un gattopardo e di una piovra possono essere specificati nelle relative classi—dove tali dettagli sono importanti.

Naturalmente, la mera esistenza di un metodo nome() o di un metodo eta() non ha di per sé stessa implicazioni sul comportamento di un oggetto. Un oggetto Cane potrebbe avere un accessore taglia() tramite il quale potete scoprire che $rodney è di taglia piccola e $lucky è di taglia media. Un oggetto Formaggio potrebbe avere un metodo taglia() che vi permette di preparare delle fette di $fontina da mettere in un panino. taglia() può essere un accessore in una classe e non in un'altra:

    # di che taglia e` il cane?
    my $dimensioni = $zeppie->taglia();

    # e` ora di uno spuntino
    $formaggio->taglia();

Talvolta è utile sapere che cosa fa un oggetto e qual è il significato di ciò che fa.

Ruoli

Un ruolo è una collezione di comportamenti e variabili di stato con un nome Vedete i documenti di progetto di Perl 6 sui ruoli all'URL http://feather.perl6.nl/syn/S14.html e le ricerche sui trait di Smalltalk all'URL http://scg.unibe.ch/research/traits per avere abbondanti dettagli.. Mentre una classe organizza i comportamenti e lo stato in un modello per la creazione di oggetti, un ruolo organizza e assegna un nome a una collezione di comportamenti e variabili di stato. Potete istanziare una classe, ma non un ruolo. Un ruolo rappresenta qualcosa che la classe fa.

Dato un Animale che ha una taglia e un Formaggio che potete tagliare, una differenza potrebbe essere che Animale ha il ruolo di EssereVivente, mentre il Formaggio ha il ruolo di Cibo:

    package EssereVivente
    {
        use Moose::Role;

        requires qw( nome eta dieta taglia );
    }

Qualunque cosa abbia questo ruolo deve fornire i metodi nome(), eta(), dieta e taglia(). La classe Gatto deve esplicitare il fatto che ha tale ruolo:

    package Gatto
    {
        use Moose;

        has 'nome',   is => 'ro', isa => 'Str';
        has 'dieta',  is => 'rw', isa => 'Str';
        has 'taglia', is => 'ro', isa => 'Str';

        has 'anno_nascita',
            is      => 'ro',
            isa     => 'Int',
            default => sub { (localtime)[5] + 1900 };

        with 'EssereVivente';

        sub eta { ... }
    }

La linea che inizia con with fa sì che Moose componga il ruolo EssereVivente nella classe Gatto. La composizione garantisce che tutti gli attributi e i metodi del ruolo facciano parte della classe. EssereVivente richiede a ogni classe in cui viene composto di fornire dei metodi nome(), eta(), dieta() e taglia(). Gatto soddisfa questo vincolo. Se invece EssereVivente venisse composto in una classe che non fornisce tali metodi, Moose solleverebbe un'eccezione.

L'Ordine Conta!

La parola chiave with usata per applicare dei ruoli a una classe deve comparire dopo le dichiarazioni di attributi in modo che la composizione possa identificare i metodi accessori generati.

Adesso tutte le istanze di Gatto restituiranno un valore vero quando domandate loro se forniscono il ruolo EssereVivente. Gli oggetti Formaggio invece dovrebbero restituire falso:

    say 'Vivo!'      if $fuffy->DOES('EssereVivente');
    say 'Ammuffito!' if $formaggio->DOES('EssereVivente');

Questa tecnica di progettazione separa le capacità delle classi e degli oggetti dall'implementazione di tali classi e oggetti. Il calcolo dell'età a partire dall'anno di nascita nella classe Gatto potrebbe essere a sua volta un ruolo:

    package CalcolaEta::Da::AnnoNascita
    {
        use Moose::Role;

        has 'anno_nascita',
            is      => 'ro',
            isa     => 'Int',
            default => sub { (localtime)[5] + 1900 };

        sub eta
        {
            my $self = shift;
            my $anno = (localtime)[5] + 1900;

            return $anno - $self->anno_nascita();
        }
    }

L'estrazione di questo ruolo da Gatto rende il suo utile comportamento disponibile ad altre classi. Ora possiamo comporre entrambi i ruoli in Gatto:

    package Gatto
    {
        use Moose;

        has 'nome',   is => 'ro', isa => 'Str';
        has 'dieta',  is => 'rw';
        has 'taglia', is => 'ro', isa => 'Str';

        with 'EssereVivente',
             'CalcolaEta::Da::AnnoNascita';
    }

Notate che il metodo eta() di CalcolaEta::Da::AnnoNascita soddisfa il vincolo posto dal ruolo EssereVivente. Notate anche che ogni controllo relativo al fatto che Gatto abbia il ruolo di EssereVivente restituisce un valore vero. Estrarre il metodo eta() in un ruolo ha modificato soltanto i dettagli di come Gatto calcola un'età. Il fatto che sia un EssereVivente rimane valido. Gatto può decidere se implementare l'età da sé oppure ottenerla da qualcun altro. Ciò che conta è che esso fornisca un metodo eta() per soddisfare i vincoli di EssereVivente.

Così come il polimorfismo indica che potete gestire nello stesso modo oggetti diversi che hanno uno stesso comportamento, questo allomorfismo indica che un oggetto può implementare uno stesso comportamento in diversi modi.

Un uso pervasivo dell'allomorfismo può ridurre le dimensioni delle vostre classi e incrementare la quantità di codice che condividono. Vi permette anche di dare un nome a una specifica collezione di comportamenti—una cosa molto utile per testare le capacità anziché le implementazioni.

Per confrontare i ruoli con altre tecniche di progettazione quali i mixin, l'ereditarietà multipla e il monkeypatching, vedete http://www.modernperlbooks.com/mt/2009/04/the-why-of-perl-roles.html.

Ruoli e DOES()

Quando componete un ruolo in una classe, tale classe e le sue istanze restituiscono un valore vero se chiamate DOES() su di esse:

    say 'Questo e` un Gatto vivo!'
        if $micio->DOES( 'EssereVivente' );

Ereditarietà

Il sistema ad oggetti di Perl 5 supporta l'ereditarietà, che stabilisce una relazione tra due classi in base alla quale una delle due classi specializza l'altra. La classe derivata si comporta come la superclasse—ha lo stesso numero e tipo di attributi e può usare gli stessi metodi. Anche se una classe derivata può avere dati e comportamenti aggiuntivi, potete sempre utilizzare una sua istanza dove il codice si aspetta un'istanza della superclasse. In un certo senso, una sottoclasse fornisce il ruolo implicato dall'esistenza della sua superclasse.

Ruoli e Ereditarietà

È meglio usare i ruoli o l'ereditarietà? I ruoli garantiscono composizioni sicure, un miglior controllo sui tipi, una maggior fattorizzazione del codice e un controllo più granulare sui nomi e i comportamenti, ma l'ereditarietà è più familiare agli sviluppatori con esperienza in altri linguaggi. Usate l'ereditarietà quando una classe è realmente un'estensione di un'altra. Usate invece un ruolo quando una classe necessita di comportamenti aggiuntivi ed è possibile assegnare un nome significativo alla collezione di tali comportamenti.

Considerate una classe SorgenteLuminosa che fornisce due attributi pubblici (attiva e candele) e due metodi (accendi e spegni):

    package SorgenteLuminosa
    {
        use Moose;

        has 'candele', is      => 'ro',
                       isa     => 'Int',
                       default => 1;

        has 'attiva',  is      => 'ro',
                       isa     => 'Bool',
                       default => 0,
                       writer  => '_imposta_attiva';

        sub accendi
        {
            my $self = shift;
            $self->_imposta_attiva(1);
        }

        sub spegni
        {
            my $self = shift;
            $self->_imposta_attiva(0);
        }
    }

(Notate che l'opzione writer di attiva crea un accessore privato utilizzabile all'interno della classe per impostare il valore di tale attributo).

Ereditarietà e Attributi

Una sottoclasse di SorgenteLuminosa potrebbe definire una maxi-candela che fornisce una quantità di luce cento volte superiore:

    package MaxiCandela
    {
        use Moose;

        extends 'SorgenteLuminosa';

        has '+candele', default => 100;
    }

extends riceve una lista di nomi di classi da usare come superclassi della classe corrente. Se questa fosse l'unica istruzione nella classe qui sopra, gli oggetti MaxiCandela si comporterebbero esattamente come gli oggetti SorgenteLuminosa. In particolare, avrebbero gli attributi candele e attiva e i metodi accendi() e spegni().

Il + all'inizio di un nome di attributo (come candele) indica che la classe corrente tratta quell'attributo in modo speciale. In questo caso maxi-candela sovrascrive il valore di default della sorgente luminosa, in modo che ogni nuova MaxiCandela abbia una potenza di 100 candele.

Quando invocate il metodo accendi() o spegni() su un oggetto MaxiCandela, Perl lo cerca anzitutto nella classe MaxiCandela, e quindi in ciascuna superclasse. Nel nostro esempio, tali metodi si trovano nella classe SorgenteLuminosa.

L'ereditarietà di attributi funziona in modo simile (vedete perldoc Class::MOP).

Ordine di Dispatch dei Metodi

L'ordine di dispatch dei metodi—o ordine di risoluzione dei metodi (MRO dall'inglese method resolution order, NdT)—è banale per le classi con un'unica superclasse. Si cerca nella classe dell'oggetto, quindi nella sua superclasse e così via finchè non si trova il metodo oppure si arriva al termine della catena di superclassi. Le classi che ereditano da diverse superclassi (ereditarietà multipla)—Hovercraft estende sia Barca che Automobile—richiedono un dispatch più sofisticato. Ragionare sull'ereditarietà multipla è complicato; quando è possibile, evitatela.

Perl 5 adotta una strategia di risoluzione in profondità. Cerca nella prima superclasse e, ricorsivamente, in tutte le superclassi di tale superclasse prima di cercare nelle superclassi successive. La direttiva mro (Direttive) fornisce strategie alternative, inclusa la strategia MRO C3 che cerca in tutte le superclassi immediate di una data classe prima di cercare nelle loro superclassi.

Vedete perldoc mro per maggiori dettagli.

Ereditarietà e Metodi

Come per gli attributi, le sottoclassi possono sovrascrivere dei metodi. Immaginate una luce che non potete spegnere:

    package Lightstick
    {
        use Moose;

        extends 'SorgenteLuminosa';

        sub spegni {}
    }

Chiamare spegni() su un lightstick non ha alcun effetto, nonostante il corrispondente metodo in SorgenteLuminosa ne abbia. Il dispatch di metodi trova il metodo della sottoclasse, che non sempre è ciò che desideravate. Se invece questa era davvero la vostra intenzione, usate l'override di Moose per esprimerla chiaramente.

All'interno di un metodo che ne sovrascrive un altro, la funzione super() di Moose vi permette di chiamare il metodo sovrascritto:

    package SorgenteLuminosa::Pignola
    {
        use Carp 'carp';
        use Moose;

        extends 'SorgenteLuminosa';

        override accendi => sub
        {
            my $self = shift;

            carp "Non posso accendere una luce gia` accesa!"
                if $self->attiva;

            super();
        };

        override spegni => sub
        {
            my $self = shift;

            carp "Non posso spegnere una luce gia` spenta!"
                unless $self->enabled;

            super();
        };
    }

Questa sottoclasse genera un messaggio quando provate ad accendere o spegnere una sorgente di luce che si trova già in quello stato. La funzione super() fa il dispatch all'implementazione del metodo corrente più vicina nella catena delle superclassi, seguendo il normale ordine di risoluzione di Perl 5.

Tutta la potenza di Moose

I modificatori di metodo di Moose permettono di fare altre cose simili—e molto di più. Vedete perldoc Moose::Manual::MethodModifiers.

Ereditarietà e isa()

Il metodo Perl isa() restituisce vero se il suo invocante è o estende una classe con un dato nome. Tale invocante può essere il nome di una classe o una sua istanza:

    say 'Sembrerebbe una SorgenteLuminosa'
        if $applique->isa( 'SorgenteLuminosa' );

    say 'Gli ominidi non emettono luce'
        unless $gorilla->isa( 'SorgenteLuminosa' );

Moose e il Sistema OO di Perl 5

Moose fornisce molte funzionalità in più del sistema OO di default di Perl 5. Vale sicuramente la pena di usarlo anche se, in linea di principio, potete implementare voi stessi tutto ciò che Moose vi mette a disposizione (Riferimenti Blessed) oppure assemblarlo alla meno peggio a partire da diverse distribuzioni CPAN. Moose rappresenta un tutto coerente ed è corredato di documentazione di buona qualità. Molti progetti importanti lo usano con successo, e la sua comunità di sviluppatori è attenta e matura.

Moose si occupa dei costruttori, dei distruttori, degli accessori e dell'incapsulamento. A voi spetta il compito di dichiarare ciò che volete, ma ciò che ne ottenete è codice sicuro e facile da usare. Gli oggetti Moose possono estendere e funzionare con oggetti del sistema base di Perl 5.

Moose permette anche la metaprogrammazione—ovvero la manipolazione dei vostri oggetti attraverso lo stesso Moose. Se vi capitasse di chiedervi quali metodi sono disponibili in una classe o in un oggetto, oppure quali attributi sono supportati da un oggetto, queste informazioni sono a vostra disposizione:

    my $metaclasse = Pantaloni::Corti->meta();

    say 'le istanze di Pantaloni::Corti hanno gli attributi:';

    say $_->name for $metaclasse->get_all_attributes;

    say 'le istanze di Pantaloni::Corti supportano i metodi:';

    say $_->fully_qualified_name
        for $metaclasse->get_all_methods;

Potete anche scoprire quali sono le classi derivate da una data classe:

    my $metaclasse = Pantaloni->meta();

    say 'Pantaloni e` superclasse di:';

    say $_ for $metaclasse->subclasses;

Vedete perldoc Class::MOP::Class per maggiori informazioni sulle operazioni delle metaclassi e perldoc Class::MOP per informazioni sulla metaprogrammazione in Moose.

Moose e il suo protocollo di meta-oggetti (MOP dall'inglese meta-object protocol, NdT) offre una sintassi migliore per dichiarare e lavorare con le classi e gli oggetti in Perl 5. Questo codice è valido in Perl 5:

    use MooseX::Declare;

    role EssereVivente { requires qw( name eta dieta ) }

    role CalcolaEta::Da::AnnoNascita
    {
        has 'anno_nascita',
            is      => 'ro',
            isa     => 'Int',
            default => sub { (localtime)[5] + 1900 };

        method eta
        {
            return (localtime)[5] + 1900
                                  - $self->anno_nascita();
        }
    }

    class Gatto with EssereVivente
              with CalcolaEta::Da::AnnoNascita
    {
        has 'nome',  is => 'ro', isa => 'Str';
        has 'dieta', is => 'rw';
    }

La distribuzione CPAN MooseX::Declare usa Devel::Declare per aggiungere sintassi specifica per Moose. Le parole chiave class, role e method riducono la quantità di copia e incolla necessario per scrivere del codice orientato agli oggetti di buona qualità in Perl 5. Notate in particolare la natura dichiarativa di questo esempio e l'assenza di my $self = shift; in eta().

Anche se Moose non fa parte dei pacchetti preinstallati con Perl 5, la sua popolarità garantisce che esso è disponibile per molti sistemi operativi. Anche le distribuzioni di Perl 5 Strawberry Perl e ActivePerl lo includono. Anche se Moose è un modulo CPAN anziché una libreria core, la sua purezza di stile e semplicità lo rendono essenziale per la programmazione moderna in Perl.

Snellire l'Alce

Moose non è una libreria leggera, ma è molto potente. Il modulo CPAN Any::Moose aiuta a ridurre i costi delle funzionalità che non usate.

Riferimenti Blessed

Il sistema ad oggetti base di Perl 5 è deliberatamente minimale. È basato su tre sole regole:

Potete costruire tutto il resto su queste tre regole, ma esse sono tutto ciò che vi viene offerto come default. Questo minimalismo può essere poco adatto a progetti di grandi dimensioni—in particolare, le possibilità di avere una maggiore astrazione con la metaprogrammazione (Generazione di Codice) sono scomode e limitate. Moose (Moose) è una scelta migliore per programmi moderni con più di un paio di centinaia di linee di codice, sebbene ci sia moltissimo codice legacy che usa ancora il sistema OO di default di Perl 5.

Il terzo e ultimo pezzo del sistema OO base di 5 sono i riferimenti blessed. L'istruzione predefinita bless associa il nome di una classe ad un riferimento. Tale riferimento diventa così un invocante valido, e Perl esegue il dispatch di metodi su di esso usando la classe associata.

Un costruttore è un metodo che crea un riferimento e ne fa il bless. Per convenzione, il nome dei costruttori è new(), ma non è obbligatorio. Quasi sempre i costruttori sono metodi di classe.

bless riceve due argomenti, un riferimento e un nome di classe, e restituisce il riferimento. Il riferimento può essere vuoto e non è neanche richiesto che la classe esista già. Potete usare bless anche al di fuori di un costruttore o di una classe, ma tutti i programmi eccetto i più semplici dovrebbero usarlo all'interno di un vero costruttore. La forma canonica di un costruttore è simile a questa:

    sub new
    {
        my $classe = shift;
        bless {}, $classe;
    }

Un costruttore come questo è progettato per ricevere il nome della classe come invocante del metodo. Potreste anche specificare il nome della classe nel codice, ma perdereste in flessibilità. Un costruttore parametrico permette infatti il riutilizzo del codice con l'ereditarietà, la delega e l'esportazione.

Il tipo del riferimento usato è rilevante solo per quanto riguarda il modo in cui l'oggetto memorizza i propri dati di istanza. Non ha altri effetti sull'oggetto risultante. I riferimenti a hash sono i più comuni, ma potete fare il bless di qualunque tipo di riferimento:

    my $ogg_array   = bless [], $classe;
    my $ogg_scalare = bless \$scalare, $classe;
    my $ogg_sub     = bless \&una_sub, $classe;

Le classi Moose definiscono gli attributi degli oggetti in modo dichiarativo, ma il sistema OO di default di Perl 5 non è altrettanto solerte. Una classe che rappresenta giocatori di basket memorizzandone il numero e la posizione potrebbe usare un costruttore come questo:

    package Giocatore
    {
        sub new
        {
            my ($classe, %attr) = @_;
            bless \%attr, $classe;
        }
    }

... e creare i giocatori con:

    my $joel  = Giocatore->new( numero    => 10,
                                posizione => 'centro' );

    my $dante = Giocatore->new( numero    => 33,
                                posizione => 'attacco' );

I metodi della classe possono accedere agli attributi dell'oggetto direttamente come elementi dell'hash:

    sub formatta
    {
        my $self = shift;
        return '#'          . $self->{numero}
             . ' gioca in ' . $self->{posizione};
    }

... ma la stessa cosa può essere fatta da qualunque pezzo di codice, con la conseguenza che una modifica nella rappresentazione interna dell'oggetto può invalidare del codice esterno alla classe. L'uso di metodi accessori è più sicuro:

    sub numero    { return shift->{numero}    }
    sub posizione { return shift->{posizione} }

... e così iniziate a scrivere a mano ciò che Moose vi fornisce gratuitamente. Meglio ancora, nascondendo il codice di generazione degli accessori, Moose incoraggia le persone a usarli al posto degli accessi diretti. E addio tentazioni.

Ricerca di Metodi e Ereditarietà

Dato un riferimento blessed, una chiamata di metodo come questa:

    my $numero = $joel->numero();

... cerca anzitutto il nome della classe associata al riferimento blessed $joel—in questo caso, Giocatore. Quindi, Perl cerca una funzione Ricordate che Perl 5 non distingue tra funzioni di un namespace e metodi. di nome numero() in Giocatore. Se tale funzione non esiste e se Giocatore estende una classe, Perl cerca nella superclasse (e così via) finchè non trova una funzione numero(). Quando numero() è stata trovata, viene chiamata come metodo con invocante $joel.

Mantenere l'Ordine nei Namespace

Il modulo CPAN namespace::autoclean può aiutare ad evitare collisioni accidentali tra le funzioni importate e i metodi.

Mentre Moose fornisce l'istruzione extends per specificare le relazioni di ereditarietà, Perl 5 usa una variabile globale di package di nome @ISA. Il dispatcher di metodi scorre l'array @ISA di ciascuna classe per trovare i nomi delle sue superclassi. Se GiocatoreInfortunato estende Giocatore, potreste scrivere:

    package GiocatoreInfortunato
    {
        @GiocatoreInfortunato::ISA = 'Giocatore';
    }

È preferibile usare la direttiva parent (Direttive) Il vecchio codice potrebbe usare la direttiva base, ma in Perl 5.10 parent ha rimpiazzato base.:

    package GiocatoreInfortunato
    {
        use parent 'Giocatore';
    }

Moose ha il proprio metamodello per memorizzare informazioni di ereditarietà estese che offrono funzionalità aggiuntive.

Potete ereditare da diverse superclassi:

    package GiocatoreInfortunato;
    {
        use parent qw( Giocatore Ospedale::Paziente );
    }

... sebbene vadano tenuti presenti gli avvertimenti dati prima sulla complessità dell'ereditarietà multipla e del dispatch di metodi. Considerate piuttosto l'uso dei ruoli (Ruoli) oppure i modificatori di metodo di Moose.

AUTOLOAD

Se non trova alcun metodo appropriato nella classe dell'invocante e nelle sue superclassi, Perl 5 cerca una funzione AUTOLOAD() (AUTOLOAD) in ciascuna classe seguendo l'ordine di risoluzione selezionato. Ciascuna AUTOLOAD() trovata viene invocata e può fornire o declinare il metodo desiderato.

AUTOLOAD() rende molto difficile capire l'ereditarietà multipla.

Sovrascrittura di Metodi e SUPER

Come in Moose, anche nel sistema OO base di Perl potete sovrascrivere dei metodi. Al contrario di Moose, però, il sistema base non fornisce meccanismi per indicare la vostra intenzione di sovrascrivere un metodo di una superclasse. Peggio ancora, ogni funzione che predichiarate, dichiarate o importate nella classe derivata potrebbe sovrascrivere accidentalmente un metodo della superclasse semplicemente perché hanno lo stesso nome. Anche quando vi dimenticate di usare il sistema di override di Moose, almeno esso esiste. Nel sistema OO base di Perl 5, invece, tale protezione è del tutto assente.

Per sovrascrivere un metodo in una classe derivata, dichiarate un metodo con lo stesso nome del metodo nella superclasse. All'interno di un metodo che ne sovrascrive un altro, chiamate il metodo sovrascritto usando il suggerimento SUPER:: per farne il dispatch:

    sub sovrascritto
    {
        my $self = shift;
        warn 'Chiamata a sovrascritto() nella classe derivata!';
        return $self->SUPER::sovrascritto( @_ );
    }

Il prefisso SUPER:: al nome del metodo indica al dispatcher di metodi di chiamare un metodo sovrascritto col nome appropriato. Potete passare dei vostri argomenti al metodo sovrascritto, ma gran parte del codice riusa semplicemente @_. In quest'ultimo caso, occorre ricordare di fare lo shift dell'invocante.

Il Problema di SUPER::

SUPER:: ha una funzionalità dannosa: fa il dispatch alla superclasse del package in cui il metodo che sovrascrive è stato compilato. Se avete importato tale metodo da un altro package, Perl non avrà problemi a fare il dispatch alla superclasse sbagliata. Il desiderio di mantenere la retrocompatibilità ha fatto sì che questa funzionalità dannosa fosse mantenuta. Il modulo CPAN SUPER offre una soluzione. La funzione super() di Moose è invece del tutto esente da questo problema.

Strategie per Sopravvivere ai Riferimenti Blessed

Se i riferimenti blessed vi sembrano minimali e complicati e confusi, ebbene: lo sono davvero. Moose rappresenta un enorme miglioramento, quindi usatelo ogni volta che vi è possibile. Se invece vi trovate a fare la manutenzione di codice che usa i riferimenti blessed oppure proprio non siete ancora riusciti a convincere il vostro gruppo ad abbracciare completamente Moose, potete aggirare alcuni dei problemi dei riferimenti blessed con un'opportuna disciplina.

Riflessione

La riflessione (o introspezione) è il processo di interrogare un programma a proposito di sé stesso mentre viene eseguito. Trattando il codice come un dato, è possibile gestirlo nello stesso modo in cui si gestiscono i dati; questo principio è alla base della generazione di codice (Generazione di Codice).

La classe Moose Class::MOP (Class::MOP) semplifica molti casi di utilizzo della riflessione nei sistemi ad oggetti. Se usate Moose, il suo sistema di metaprogrammazione vi sarà di aiuto; altrimenti molti altri idiomi del Perl 5 base vi aiuteranno a ispezionare e manipolare i programmi in esecuzione.

Verificare se un Modulo è stato Caricato

Se conoscete il nome di un modulo, potete verificare se Perl ritiene di averlo caricato ispezionando l'hash %INC. Infatti, quando Perl 5 carica del codice con use o require, memorizza un elemento in %INC la cui chiave è il path del file contenente il modulo da caricare e il cui valore è il path completo su disco di tale modulo. In altre parole, il caricamento di Modern::Perl ha il seguente effetto:

    $INC{'Modern/Perl.pm'} =
        '.../lib/site_perl/5.12.1/Modern/Perl.pm';

Ovviamente, i dettagli del path completo variano in base alla propria installazione. Per verificare che Perl sia riuscito a caricare un modulo, occorre convertire il nome del modulo in un nome di file in forma canonica e controllare l'esistenza di tale chiave in %INC:

    sub modulo_caricato
    {
        (my $nomemod = shift) =~ s!::!/!g;
        return exists $INC{ $nomemod . '.pm' };
    }

Come @INC, anche %INC può essere manipolato in qualunque parte del codice. Alcuni moduli (come Test::MockObject o Test::MockModule) manipolano %INC per validi motivi ma, a seconda del vostro livello di paranoia, potreste anche decidere di controllare voi stessi che il path e i contenuti del package siano quelli attesi.

La funzione is_class_loaded() del modulo CPAN Class::Load incapsula questo controllo di %INC.

Verificare l'Esistenza di un Package

Per controllare se un package esiste nel vostro programma—ovvero se, da qualche parte, del codice ha eseguito una direttiva package con un certo nome—controllate che il package erediti da UNIVERSAL. Tutto ciò che estende UNIVERSAL deve in qualche modo fornire il metodo can(). Se il package non esiste, Perl solleverà un'eccezione di chiamante non valido, quindi ponete la chiamata all'interno di un blocco eval:

  say "$pkg esiste" if eval { $pkg->can( 'can' ) };

In alternativa, potete consultare le tabelle dei simboli del Perl.

Verificare l'Esistenza di una Classe

Dato che Perl 5 non fa una vera distinzione tra package e classi, il meglio che si possa fare senza Moose è controllare se esiste un package con il nome di una data classe. È possibile controllare con can() che il package fornisca new(), ma non vi è alcuna garanzia che gli eventuali new() trovati siano dei metodi e tanto meno dei costruttori.

Verificare il Numero di Versione di un Modulo

I moduli non devono fornire necessariamente un numero di versione, ma ogni package eredita il metodo VERSION() dalla superclasse universale UNIVERSAL (Il Package UNIVERSAL):

    my $ver_mod = $modulo->VERSION();

VERSION() restituisce il numero di versione del modulo, se è definito; altrimenti restituisce undef. Il metodo restituisce undef anche nel caso in cui il modulo non esista.

Verificare l'Esistenza di una Funzione

Per controllare se una funzione esiste in un package, chiamate can() come metodo di classe sul nome del package:

  say "$funz() esiste" if $pkg->can( $funz );

Perl solleverà un'eccezione a meno che $pkg sia un invocante valido; se avete dubbi sulla sua validità, ponete la chiamata in un blocco eval. Attenzione che una funzione implementata in termini di AUTOLOAD() (AUTOLOAD) potrebbe dare una risposta errata se il suo package non ha predichiarato la funzione oppure non ha sovrascritto can() in modo corretto. In questo caso, significa che tale package ha un bug.

Per determinare se la import() di un modulo ha importato una funzione nel namespace corrente, usate la seguente tecnica:

    say "$funz() importata!" if __PACKAGE__->can( $funz );

Come per l'esistenza di un package, potete anche esaminare voi stessi la tabella dei simboli, se avete abbastanza pazienza per farlo.

Verificare l'Esistenza di un Metodo

Non esistono tecniche di riflessione infallibili per distinguere tra una funzione e un metodo.

Consultare le Tabelle dei Simboli

Una tabella dei simboli di Perl 5 è uno speciale tipo di hash, in cui le chiavi sono i nomi dei simboli globali di un package e i valori sono typeglob. Un typeglob è una struttura dati interna che può contenere uno o più dei seguenti valori: uno scalare, un array, un hash, un filehandle e una funzione.

Potete accedere a una tabella dei simboli come hash aggiungendo due volte "due punti" al nome del package. Per esempio, la tabella dei simboli per il package TritaScimmie è accessibile come %TritaScimmie::.

Tramite l'operatore exists è possibile verificare l'esistenza di specifici nomi di simboli all'interno di una tabella dei simboli (o anche manipolare la tabella aggiungendo o rimuovendo simboli). Attenzione però che alcuni cambiamenti interni nel Perl 5 hanno variato i dettagli di che cosa viene memorizzato nei typeglob, quando e perché.

Per maggiori dettagli potete vedere la sezione "Symbol Tables" in perldoc perlmod, ma utilizzate preferibilmente le altre tecniche di riflessione descritte in questa sezione. Se davvero siete costretti a manipolare le tabelle dei simboli e i typeglob, considerate piuttosto di usare il modulo CPAN Package::Stash.

OO Avanzato in Perl

Creare e usare oggetti in Perl 5 con Moose (Moose) è facile. Invece, progettare dei buoni programmi non lo è affatto. Dovete trovare il giusto compromesso tra l'eccesso e il difetto di progettazione. Soltanto l'esperienza pratica può aiutarvi a comprendere le tecniche fondamentali di progettazione, ma potete farvi guidare da alcuni principi.

Preferite la Composizione all'Ereditarietà

I progetti OO fatti da principianti tendono a utilizzare eccessivamente l'ereditarietà per il riutilizzo del codice e il polimorfismo. Il risultato è spesso una gerarchia di classi troppo profonda, in cui le responsabilità sono sparpagliate nei posti sbagliati. La manutenzione di questo codice risulta molto difficile—come si fa a sapere dove aggiungere o modificare un comportamento? Che cosa succede quando una parte di codice è in conflitto con del codice dichiarato altrove?

L'ereditarietà è solo uno strumento tra tanti. Un'Automobile potrebbe estendere Veicolo::A::Ruote (con una relazione is-a), ma potrebbe più propriamente contenere diversi oggetti Ruota come attributi di istanza (una relazione has-a).

Decomporre classi complesse in entità più piccole e specifiche (che siano classi o ruoli) migliora l'incapsulamento e riduce la possibilità che una singola classe o ruolo cresca troppo. Delle entità più piccole, più semplici e più incapsulate sono più facili da capire, testare e manutenere.

Principio di Singola Responsabilità

Quando progettate il vostro sistema ad oggetti, considerate le responsabilità di ciascuna entità. Per esempio, un oggetto Impiegato potrebbe rappresentare informazioni specifiche sul nome di una persona, gli indirizzi per contattarla e altri dati personali, mentre un oggetto Lavoro potrebbe rappresentare i compiti lavorativi all'interno di un'azienda. Separare queste entità in base alla loro responsabilità permette alla classe Impiegato di considerare solo il problema della gestione di informazioni su chi è la persona e alla classe Lavoro di rappresentare che cosa fa quella persona (per esempio, due oggetti Impiegato potrebbero condividere un Lavoro).

Affidando a ogni classe un'unica responsabilità, si migliora l'incapsulamento dei suoi dati e comportamenti specifici e si riduce la quantità di accoppiamento tra classi.

Non Ripetetevi

La complessità e le duplicazioni rendono più difficili sia lo sviluppo che la manutenzione. Il principio DRY (Don't Repeat Yourself—Non ripetetevi, NdT) è un ammonimento a cercare di eliminare le duplicazioni all'interno del sistema. La duplicazione interessa anche i dati oltre al codice. Invece di ripetere informazioni di configurazione, informazioni sugli utenti e altri dati nel vostro sistema, create un'unica rappresentazione canonica di tali dati da cui potete poi estrarre quelli che vi servono nel formato opportuno.

Questo principio aiuta a ridurre la possibilità che parti importanti del vostro sistema diventino disallineate e vi aiuta a trovare una rappresentazione ottimale del sistema e dei suoi dati.

Principio di Sostituzione di Liskov

Il principio di sostituzione di Liskov suggerisce che dovrebbe essere possibile sostituire una classe o un ruolo con una loro specializzazione senza violare l'API dell'originale. In altre parole, tali specializzazioni dovrebbero essere altrettanto o più generali riguardo a ciò che si aspettano e almeno altrettanto specifiche riguardo a ciò che producono.

Immaginate due classi, Dessert e la sua derivata TortaDiNoci. Se tali classi osservano il principio di sostituzione di Liskov, potete rimpiazzare ciascun uso di un oggetto Dessert con un oggetto TortaDiNoci nel codice di test, e tutti i test dovrebbero passare Vedete "IS-STRICTLY-EQUIVALENT-TO-A" di Reg Braithwaite all'URL http://weblog.raganwald.com/2008/04/is-strictly-equivalent-to.html per maggiori dettagli..

Sottotipi e Coercizioni

Moose vi permette di dichiarare e usare dei tipi e di estenderli con dei sottotipi per dare descrizioni ancora più specifiche di ciò che i vostri dati rappresentano e di come si comportano. Tali annotazioni di tipo sono d'aiuto per verificare che i dati su cui volete operare in funzioni o metodi specifici siano appropriati e vi permettono anche di specificare i meccanismi con cui fare la coercizione dei dati da un tipo ad un altro.

Vedete Moose::Util::TypeConstraints e MooseX::Types per maggiori informazioni.

Immutabilità

I principianti della programmazione OO trattano spesso gli oggetti come se fossero agglomerati di record che utilizzano dei metodi per leggere e scrivere dei valori interni. Questa tecnica semplicistica porta alla sventurata tentazione di spargere le responsabilità dell'oggetto nell'intero sistema.

Un oggetto ben progettato si aspetta che gli diciate che cosa deve fare e non come deve farlo. Come regola pratica, se vi trovate ad accedere ai dati d'istanza dell'oggetto (anche se attraverso i metodi accessori), potreste avere troppa visibilità sui suoi dettagli interni.

Un approccio che aiuta a prevenire questo comportamento è quello di considerare gli oggetti come immutabili. Dopo aver fornito i dati necessari ai loro costruttori vietate ulteriori modifiche di queste informazioni dall'esterno della classe. Non esponete alcun metodo per cambiare i dati d'istanza. Gli oggetti costruiti in questo modo, una volta creati, saranno sempre validi e non potranno divenire inconsistenti a causa di manipolazioni esterne. Seguire questo approccio richiede un'enorme dose di disciplina, ma i sistemi risultanti sono robusti, testabili e manutenibili.

Alcuni stili di progettazione si spingono fino a proibire la modifica dei dati d'istanza dentro alla classe stessa, ma quest'obiettivo è molto più difficile da raggiungere.