Giacomo Cerrai
Dati di classe in Perl
http://www.perl.it/documenti/articoli/2008/02/dati-statici-di.html
© Perl Mongers Italia. Tutti i diritti riservati.

Probabilmente, una delle cose più sorprendenti per chi proviene da altri linguaggi è la incredibile varietà di approcci all'object orientation che offre il Perl.
Come se non bastassero la libertà e flessibilità offerte dal linguaggio nel 'fai da te', esistono le soluzioni preconfezionate su CPAN, dalle più leggere e semplici a quelle più estese e complesse, dai metamodelli e class class generators (MOP, Moose) all'implementazione del Design By Contract (Class::Contract).
Anche per un perlista esperto, il numero di moduli dedicati all'OO su CPAN è qualcosa che lascia disorientati.

In questo articolo voglio parlare di un aspetto dell'OO che in Perl è relativamente trascurato, o quantomeno che non ha trovato per me le soluzioni che avrei voluto. Si tratta dei dati di classe, detti anche dati statici. Persino in Programming Perl Third Edition § 12.8 si può leggere, "No matter how you roll it, package-relative class data is always a bit awkward.".
Come noto, questi dati sono condivisi da tutti gli oggetti della classe e servono tipicamente a mantenere uno stato globale, informazioni comuni, o tenere traccia di quello che la classe sta facendo.

Il primo approccio

A prima vista questo non è un problema. In Perl esistono almeno due modi banali ed immediati di risolvere la questione. Le variabili di package e le variabili lessicali.
Darò per scontato che si utilizzi sempre use strict, anche perché, altrimenti, non credo potrebbe interessare una discussione sui dati statici.

Le variabili di package

Le variabili di package sono una scelta naturale vista la vicinanza dei concetti di classe e di package in Perl.
Queste variabili richiedono che voi le dichiariate con use vars, ormai deprecato, oppure con our (da Perl 5.6.0 in poi). our è senz'altro da preferire, ricorda my nell'uso ed è più pulito, ma i due metodi non sono del tutto equivalenti. Il primo ha scope di package/file mentre il secondo, come my, ha scope lessicale.
Alternativamente, potete qualificare col nome di package le vostre variabili, ma questo, come vedremo, è pericoloso.

Ecco un esempio di utilizzo:

    #!/usr/local/bin/perl
    package SomeClass;
    use strict;
    use warnings;

    # cosi ...
    use vars qw( $COUNTER );
    $COUNTER = 0;
    # ... oppure cosi
    our $COUNTER = 0;

    sub inc_counter { ++$COUNTER }
    sub get_counter { $COUNTER   }

    package main;
    SomeClass::inc_counter();
    SomeClass::inc_counter();
    print "SomeClass COUNTER: ", SomeClass::get_counter(), "\n";

Lanciando il programma otteniamo il risultato atteso:

    SomeClass COUNTER: 2

Se ci arrischiamo ad utilizzare variabili qualificate con il package name, di fatto stiamo rinunciando ai controlli a tempo di compilazione sui nomi delle variabili.
I risultati possono non essere piacevoli:

    #!/usr/local/bin/perl
    package SomeClass;
    use strict;
    use warnings;

    $SomeClass::COUNTER = 0;

    sub reset_counter { $SomeClass::CoUNTER = 0 }
    sub inc_counter   { ++$SomeClass::CoUNTER   }
    sub dec_counter   { --$SomeClass::COUNTER   }
    sub get_counter   { $SomeClass::COUNTER     }

    package main;
    SomeClass::inc_counter();
    SomeClass::inc_counter();
    print "SomeClass COUNTER: ", SomeClass::get_counter(), "\n";

Lanciando il programma abbiamo una sorpresa:

    SomeClass COUNTER: 0

Ovviamente il tutto va immaginato nel contesto di una classe con molto più codice, dove l'errore di battitura può essere assai meno evidente. Ma potrebbe anche non essere un errore di battitura. Da qualche parte utilizzo Counter ed altrove un altro sviluppatore utilizza COUNTER.

Il problema principale delle variabili di package, anche quando sono dichiarate appropriatamente con our, è che sono per loro natura pubblica. Nessuno può impedire al codice utente, il nostro main, di scrivere:

    print "SomeClass::COUNTER: $SomeClass::COUNTER\n";

Le variabili con scope lessicale

Se vogliamo proteggere i nostri dati di classe, una soluzione migliore può essere quella di utilizzare la dichiarazione my.

    #!/usr/local/bin/perl
    package SomeClass;
    use strict;
    use warnings;

    my $COUNTER = 0;

    sub inc_counter { ++$COUNTER }
    sub get_counter { $COUNTER }

    package main;
    SomeClass::inc_counter();
    SomeClass::inc_counter();
    print "SomeClass COUNTER: ", SomeClass::get_counter(), "\n";

Apparentemente molto simile alla soluzione che utilizza our, ma adesso dal main non è più possibile accedere alla variabile COUNTER, che è diventata privata a tutti gli effetti.
Non solo, possiamo addirittura limitare la visibilità di COUNTER ad alcuni metodi della classe SomeClass, raggiungendo un livello di incapsulamento precluso ad altri linguaggi più blasonati. È sufficiente racchiudere in uno scope lessicale più ristretto la dichiarazione ed inserire all'interno di questo scope i soli metodi che vogliamo possano accedervi:

...
    {
     my $COUNTER = 0;
 
     sub inc_counter { ++$COUNTER }
     sub get_counter { $COUNTER }
    }

    sub cannot_see { print $COUNTER }
...

Se proviamo ad eseguire con questa modifica otteniamo qualcosa del tipo:

    Global symbol "$COUNTER" requires explicit package name at ...
    Execution of ./main.pl aborted due to compilation errors.

Il problema

Purtroppo da tutto questo resta fuori un piccolo dettaglio, l'ereditarietà.
Nessuna delle soluzioni viste permette di ereditare i dati di classe. È proprio da questo problema che deriva la grande varietà di soluzioni.

In generale, per ottenere ereditarietà in Perl, è necessario passare attraverso dei metodi. Questa infatti è l'unica forma supportata direttamente dal linguaggio. In altre parole, anziché accedere ai data member veri e propri si è costretti ad usare dei metodi accessor.

Da un certo punto di vista questa non sarebbe una grave mancanza. È ormai abbastanza accettato il fatto che i dati public e protected, che sono ereditabili, dovrebbero essere utilizzati il meno possibile in favore di quelli privati e laddove necessario rimpiazzati quantomeno con dei metodi accessor.
Tuttavia ci sarà pure una ragione se tutti i linguaggi più diffusi hanno scelto consistentemente di fornire data member con specifiche d'accesso pubblico/protetto/privato o equivalenti.
Inoltre, come nel caso d'esempio, non sempre i data member espongono lo stato della classe o dell'oggetto, a volte costituiscono essi stessi l'interfaccia, e non ha quindi senso tentare di precludervi l'accesso.

Tornando ai nostri metodi accessor, invece di tentare una soluzione fatta in casa diamo un'occhiata ad una delle soluzioni standard che fanno uso di questa tecnica, Class::Data::Inheritable.

Il modulo si usa in questo modo:

    package Base;
    use base qw(Class::Data::Inheritable);

    Base->mk_classdata(staticData1 => 1);

Promette bene. Proviamo ad utilizzarlo.

    #!/usr/local/bin/perl
    use strict;
    use warnings;

    package InheritableClass;
    use base qw( Class::Data::Inheritable );
    InheritableClass->mk_classdata(COUNTER => 0);

    sub inc_counter { InheritableClass->COUNTER(InheritableClass->COUNTER + 1) }
    sub get_counter { InheritableClass->COUNTER }
    sub show_counter {
        print "In InheritableClass COUNTER is ", InheritableClass->COUNTER, "\n";
    }

    package SomeDerivedClass;
    use base qw( InheritableClass );

    sub show_counter {
        print "In SomeDerivedClass COUNTER is ", SomeDerivedClass->COUNTER, "\n";
    }

    package main;
    InheritableClass->inc_counter;
    InheritableClass->inc_counter;
    InheritableClass->show_counter;
    SomeDerivedClass->show_counter;

Eseguendo otteniamo:

$ ./someclass_inheritable.pl
In InheritableClass COUNTER is 2
In SomeDerivedClass COUNTER is 2

Fin qui tutto bene, il modulo fa proprio quello che ci aspettiamo. Leggendo la documentazione del modulo però scopriamo che la semantica implementata potrebbe non essere quella che vorremmo. Di certo non è quella 'classica', quella del C++ e di Java per intenderci.
Infatti, assegnare un valore ad un data member in una classe derivata equivale a ridefinirlo. In altre parole, da quel momento in poi la classe padre e quella derivata non condivideranno più quel campo. Questo normalmente si ottiene ridichiarando il data member nella classe derivata.

Aggiungiamo in fondo alla parte main le seguenti righe:

    SomeDerivedClass->COUNTER(10);                  # A
    #SomeDerivedClass->mk_classdata(COUNTER => 10);  # B
    InheritableClass->show_counter;
    SomeDerivedClass->show_counter;

Eseguendo sia la versione A che quella B, otteniamo:

$ ./someclass_inheritable.pl
In InheritableClass COUNTER is 2
In SomeDerivedClass COUNTER is 2
In InheritableClass COUNTER is 2
In SomeDerivedClass COUNTER is 10

Proprio cosi, impostare un valore equivale a ridefinire. Le istruzioni A e B sono equivalenti.
Ci sarebbero altri aspetti da indagare, ovvero come si comportino i dati statici rispetto al polimorfismo. Ma visto che ci siamo fermati al primo passo è inutile fare indagini più intricate. È una cosa discussa spesso nella letteratura Perl, ovvero quale sia la semantica che vogliamo per i dati di classe. In effetti prima di incontrare il Perl non mi era mai nemmeno venuto in mente che i dati statici potessero avere una semantica diversa.
Personalmente trovo questa appena vista meno utile di quella classica, ed anche meno intuitiva, ma potrebbe dipendere dal mio bagaglio. Però non trascuro le difficoltà di spiegare la cosa ai colleghi non perlisti, o apprendisti tali provenienti da altri linguaggi. In un certo senso questa semantica viola il principio della 'least surprise' ovvero il significato di un costrutto del linguaggio dovrebbe essere quello atteso. E, nel bene o nel male, i linguaggi OO più largamente utilizzati hanno fatto un'altra scelta.

Adesso vorrei focalizzarmi sulla comodità di utilizzo del modulo. È vero o no che i programmatori Perl dovrebbero essere pigri?
Piu' precisamente mi interessa analizzare l'efficacia di utilizzo nella realizzazione di codice di classe. Quindi gli stralci di codice che seguono si riferiscono per l'appunto all'implementazione di classi e non al codice che scriverebbero gli utenti per utilizzarle.

Supponiamo di avere una gerarchia di classi di tipo User che gestisca dati che
si trovano su vari sistemi, database di vario tipo o dump o altro ancora, e
che per varie ragioni si sia deciso di utilizzare proprio variabili di classe
per nominare in modo uniforme i campi sui quali lavorare.
Utilizzando Class::Data::Inheritable nelle vostre classi vi trovate a scrivere:

  package User;
  use base qw(Class::Data::Inheritable);

  # i nomi dei nostri campi
  User->mk_classdata(USERNAME         => 'username',
                     COMMON_NAME      => 'common_name',
                     USER_ID          => 'uid',
                     LASTACCESS_DATE  => 'last_access_date',
                    );

  package FancyUser;
  use base qw(User);
  FancyUser->mk_classdata(USERNAME    => 'uname',     # ridefinito
                          ADDRESS     => 'address',
                          PREFERENCES => 'preferences',
                          LANGUAGE    => 'language',
                         );
  ...
  sub some_method {
      ...
      my $name = $tempdata->{FancyUser->COMMON_NAME} or croak "Error: missing '" . FancyUser->COMMON_NAME "' info";
      my $identity = $tempdata->{FancyUser->USERNAME} . ':' . $tempdata->{FancyUser->USER_ID};
      ...
      foreach my $field (FancyUser->USERNAME, FancyUser->COMMON_NAME,
                         FancyUser->USER_ID, FancyUser->LASTACCESS_DATE) {
      ...
      $self->do_something(...,
                   fields => {
                               FancyUser->USERNAME        => 'pippo12',
                               FancyUser->COMMON_NAME     => 'il mio nome',
                               FancyUser->USER_ID         => '12345',
                               FancyUser->LASTACCESS_DATE => '23/05/2006',
                               FancyUser->PREFERENCES     => 'viaggi',
                             },
                  );
  }

La sintassi è pesante. Siamo lontani dalla naturalezza con la quale si dovrebbe poter accedere ai datamember di classe. A parte il tempo necessario a digitare, diventa illeggibile rapidamente e il codice poco leggibile aumenta la probabilità di bug. Senza contare che i perlisti additano altri linguaggi per la loro verbosità, vedi Java. E dovremmo poi trovarci a scrivere tutta questa roba inutile?
Oltretutto, come ulteriore danno, abbiamo dovuto cablare ripetutamente nel codice il nome della classe FancyUser in quanto con la sintassi FIELDNAME() non avremmo avuto ereditarietà nemmeno per i metodi e non avremmo quindi potuto accedere ad attributi quali COMMON_NAME, USER_ID e LASTACCESS_DATE.
Quello che vorrei è semplicemente ciò a cui sono abituato in altri linguaggi, qualcosa del tipo:

  package User;
  # i nomi dei nostri campi
  class_data $USERNAME        = 'username';
  class_data $COMMON_NAME     = 'common_name';
  class_data $USER_ID         = 'uid';
  class_data $LASTACCESS_DATE = 'last_access_date';

  package FancyUser;
  use base qw(User);
  class_data $USERNAME    = 'uname';      # ridefinito
  class_data $ADRESS      = 'address';
  class_data $PREFERENCES = 'preferences';
  class_data $LANGUAGE    = 'language';

  ...
  sub some_method {
      ...
      my $name = $tempdata->{$COMMON_NAME} or croak "Error: missing '$COMMON_NAME' info";
      my $identity = "$tempdata->{$USERNAME}:$tempdata->{$USER_ID}";
      ...
      foreach my $field ($USERNAME, $COMMON_NAME, $USER_ID, $LASTACCESS_DATE) { 
      ...
      $self->do_something(...,
                   fields => {
                               $USERNAME        => 'somename',
                               $COMMON_NAME     => 'My Name',
                               $USER_ID         => '12345',
                               $LASTACCESS_DATE => '23/05/2006',
                               $PREFERENCES     => 'viaggi',
                             },
                  );
  }

Con il 'piccolo' ulteriore vantaggio che la chiamata a do_something() non richiede cinque (ma potenzialmente molte di più) ulteriori chiamate di metodo solo per ottenere i nomi dei campi utilizzati.

Una soluzione?

A ben vedere, quello che vorrei sono variabili di package ereditabili. Non mi sembra di chiedere molto eppure non sembra cosi facile. Però, se ci pensiamo, c'è un modulo che fa già buona parte di quello che cerchiamo: Exporter.
D'accordo non sarà ereditarietà, però mette nel nostro package tutte le variabili che vogliamo. Dobbiamo aggiungere un po' di Perl automagic e potremmo riuscire a completare l'opera.

Il modulo Package::Data::Inheritable

Ho realizzato un modulo che estende Exporter, e l'ho pubblicato su CPAN con il nome di Package::Data::Inheritable, che gestisce automaticamente o quasi, l'import/export degli attributi che vogliamo ereditare. Si fa prima a mostrarlo al lavoro che a descriverlo. Con questo modulo l'esempio di cui sopra diventa:

  package User;
  use base qw(Package::Data::Inheritable);

  BEGIN {
      # i nomi dei nostri campi
      User->pkg_inheritable('$USERNAME'        => 'username');
      User->pkg_inheritable('$COMMON_NAME'     => 'common_name');
      User->pkg_inheritable('$USER_ID'         => 'uid');
      User->pkg_inheritable('$LASTACCESS_DATE' => 'last_access_date');
  }

  package FancyUser;
  use base qw(User);
  BEGIN {
      inherit User;
      FancyUser->pkg_inheritable('$USERNAME'    => 'uname');     # ridefinito
      FancyUser->pkg_inheritable('$ADRESS'      => 'address');
      FancyUser->pkg_inheritable('$PREFERENCES' => 'preferences');
      FancyUser->pkg_inheritable('$LANGUAGE'    => 'language');
  }

  ...
  ...
  sub some_method {
      ...
      my $name = $tempdata->{$COMMON_NAME} or croak "Error: missing '$COMMON_NAME' info";
      my $identity = "$tempdata->{$USERNAME}:$tempdata->{$USER_ID}";
      ...
      foreach my $field ($USERNAME, $COMMON_NAME, $USER_ID, $LASTACCESS_DATE) { 
      ...
      $self->do_something(...,
                   fields => {
                               $USERNAME        => 'somename',
                               $COMMON_NAME     => 'My Name',
                               $USER_ID         => '12345',
                               $LASTACCESS_DATE => '23/05/2006',
                               $PREFERENCES     => 'viaggi',
                             },
                  );
  }

Visto che le variabili di classe in questione normalmente non saranno soggette a variazioni durante l'esecuzione le possiamo dichiarare come costanti, sostituendo le chiamate a pkg_inheritable() con chiamate a pkg_const_inheritable().
Purtroppo le costanti sono supportate da questo modulo solo per variabili scalari.

Ci siamo avvicinati al risultato desiderato. La sintassi per dichiarare le variabili potrebbe essere migliorata ma non è questa la mancanza principale. Il vero problema di questo approccio è che le variabili di package per loro natura non sono incapsulabili.
La soluzione classica è quella di nominare quelle intese come private utilizzando un prefisso '_' e definire poi gli accessor con tutti i controlli del caso, senza il prefisso. Restiamo però sempre dipendenti dalla correttezza del codice utente nel non bypassare i nostri metodi.
In verità la mancanza di incapsulamento è un difetto comune anche a Class::Data::Inheritable ed altri moduli, anche se per ragioni diverse. Infatti, spesso si trascura il fatto che passare da un metodo accessor/mutator in sé e per sé fornisce ben poco incapsulamento se poi chiunque può utilizzare tale metodo per leggere e scrivere i nostri data member di classe.
In altre parole, se utilizziamo Class::Data::Inheritable, o moduli simili che non permettano di specificare limitazioni sugli accessi, e vogliamo incapsulare veramente le nostre variabili dobbiamo ridefinire i metodi generati dal modulo ed aggiungere controlli al loro interno, perché cosi come sono lasciano totale libertà al codice utente.

Un ulteriore problema è quello delle variabili costanti, o se preferite readonly, di tipo non scalare. Questo problema comunque affligge tutte le variabili Perl, scalari o meno, e non solo i dati di classe. La soluzione migliore attualmente disponibile credo sia il modulo Readonly, il quale però non permette l'ereditarietà e per essere efficiente richiede l'installazione del modulo Readonly::XS.

Considerazioni finali

Sì è vero, questa è una di quelle cose per le quali vorrei un miglior supporto da parte del linguaggio e che ti fanno guardare con trepidazione all'arrivo di Perl6. Ma al tempo stesso, il fatto che in Perl5 si riesca quasi ad ottenere il risultato voluto semplicemente scrivendo un modulo, è davvero sorprendente.

Giacomo Cerrai


Riferimenti:
- perltooc - Tom's OO Tutorial for Class Data in Perl
- Class::Data::Inheritable
- Package::Data::Inheritable