| |||
| © 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. 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.". Il primo approccioA 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. Le variabili di packageLe variabili di package sono una scelta naturale vista la vicinanza dei concetti di classe e di package in Perl. 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.
#!/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 Il problema principale delle variabili di package, anche quando sono dichiarate appropriatamente con
print "SomeClass::COUNTER: $SomeClass::COUNTER\n";
Le variabili con scope lessicaleSe vogliamo proteggere i nostri dati di classe, una soluzione migliore può essere quella di utilizzare la dichiarazione
#!/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
...
{
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 problemaPurtroppo da tutto questo resta fuori un piccolo dettaglio, l'ereditarietà. 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. 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. 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. Adesso vorrei focalizzarmi sulla comodità di utilizzo del modulo. È vero o no che i programmatori Perl dovrebbero essere pigri? Supponiamo di avere una gerarchia di classi di tipo User che gestisca dati che
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?
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. Il modulo Package::Data::InheritableHo 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 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. 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 finaliSì è 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 | |||