Abstract
Un vecchio adagio sostiene che i design pattern siano "pezze" per i
linguaggi che non forniscono determinate potenzialità.
Proviamo ad indagare su questa idea.
Data di stesura: 15/10/2005
Data di pubblicazione:
04/11/2005
Ultima modifica: 04/04/2006
È notizia recente l'assegnazione da parte del SIGPLAN (ACM)
del "Programming Languages Achievement Award" alla Gang of Four, per il
libro Design Patterns, Elements of Reusable Object Oriented Software[1].
Ovviamente, come in ogni caso in cui qualcuno venga premiato, c'è
qualcuno che borbotta.
Deve essere qualcosa che ha a che fare con la sopravvivenza della specie,
perché è un comportamento che ho notato anche nei cani. Non che si diano premi
dell'ACM a quadrupedi, per quel che ne so, ma i cani sono gelosi lo stesso.
Alcuni dei mormorii erano legati all'idea che potete leggere nell'abstract,
idea piuttosto antica, almeno nella forma linguaggi dinamici vs linguaggi statici[2].
Cosa si intende dicendo che i design pattern sono "missing language feature"?
Beh, ad esempio il pattern (in pseudo assembly)
...
label loop:
compare v1, v2 ;
jump_if_zero done;
...
increment v1;
jump loop
label done:
...
sarebbe inutile in un linguaggio in cui esista una struttura di
controllo in stile "while".
La domanda che viene da porsi è allora: è vero che una larga parte dei DP
sono impliciti o comunque ridotti/semplificati in linguaggi di livello molto
alto?
Divagazione_2
Trovo che questo sia un periodo interessante nell'ambito della
programmazione.
Ci sono idee interessanti che stanno diffondendosi, approcci,
tecniche ed idiomi che passano da una comunità all'altra e, in generale, un sacco di cose interessanti da imparare.
Poi forse è sempre stato così, solo che non me ne ero accorto.
L'esempio più bello per me è parrot/perl6:
una virtual machine multilinguaggio che mira a supportare praticamente tutto
perl6 che ha feature ispirate da qualsiasi cosa sia stato provato in 50 anni di PLT
con un compilatore/interprete che è al momento scritto in un linguaggio sconosciuto ai più (Haskell, ma ad
esser precisi Haskell+alcune estensioni sperimentali..)
mentre nel frattempo viene scritto un backend che permette di far girare perl6 su un motore
(SpiderMonkey di Mozilla) scritto per JavaScript
e con un tizio che si chiama Ruby che sta lavorando per far girare Python sulla VM per Perl
Passiamo dunque alla fusione delle due divagazioni:
tempo fa si era discusso con i ragazzi di SIForge.org
dell'ipotesi di scrivere degli articoli introduttivi ai Design Pattern in
un'ottica multilinguaggio, e quindi ho pensato di provare a vedere che succede
affrontando i DP in linguaggi di livello molto alto, cercando di ripartire dai problemi piuttosto che dai
pattern già cristallizzati, e vedere se effettivamente questi hanno senso in ambiti diversi da quelli che c'erano nel testo
originale (C++ senza template e Smalltalk senza un approccio troppo dinamico)
Per seguire un tracciato, proverò a trattare i pattern descritti nel libro suddetto,
non so se ci saranno seguiti a questo articolo, anche se vorrei farli (e vorrei leggerne scritti da altri, hint hint)
ma finché non mi stufo proviamo ad andare con ordine.
Abstract Factory
Un pattern serve a risolvere un problema.
Il problema, in questo caso, è che vogliamo fornire un modo standard per creare famiglie di oggetti correlati senza
sapere a priori il "tipo effettivo" a cui appartengono.
L'esempio standard è quello di una Factory per creare componenti per
interfacce grafiche, evitando di cablare nel codice il supporto ad una
piattaforma specifica, ma è evidente come il concetto abbia una rilevanza
davvero molto ampia.
Vediamo un altro esempio, concreto:
supponiamo di voler creare un disegno, avendo delle classi base
per comporre immagini: Image, Line, Color, etc..
Potremmo scrivere una cosa come questa, ad esempio in php:
class Drawer {
function makeImage() {
$img= new Image();
$line=new Line($x1,$y1, $x2,$y2);
$color=new Color( "blue" );
$line->setColor= $color;
$img->add($line);
...
return $img
}
}
Il problema è che così il codice dipende da certe classi specifiche,
quindi se volessimo cambiare il tipo di immagine (magari SVG piuttosto che PNG)
dovremmo cambiare l'intero codice.
Una soluzione è usare una classe che sia responsabile di costruire tutti
i vari oggetti, che in un eccesso di fantasia chiameremo Factory .
In questo modo il codice dipende semplicemente da un'interfaccia, quella della classe
Factory (che sarà Astratta perché fa riferimento ad un'Immagine che esiste nel cielo platonico,
non ad una specifica, saranno poi le varie sottoclassi a fornire Immagini di un certo tipo):
class Drawer {
function makeImage(Factory $factory) {
$img = $factory->newImage();
$line= $factory->newLine($x1,$y1, $x2,$y2);
$color= $factory->newColor("blue");
$line->setColor= $color;
$img->add($line);
...
return $img
}
}
class PngFactory extends Factory{
function newImage() {
return new PngImage();
}
function newColor($name){
return new PngColor(...);
}
...
}
$factory= new PngFactory();
$drawer= new Drawer().makeImage($factory);
Ovviamente ci sono altre interfacce in gioco, cioè quelle dei vari
componenti Image, Color, Line: insieme ad ogni Factory
dovremo avere un pacchetto di "prodotti" che implementano le interfacce
necessarie a realizzare l'immagine.
E questo è sostanzialmente l'Abstract Factory Pattern.
La Fabbrica di Rubini
Il codice che è stato mostrato è sostanzialmente trasportabile in qualsiasi
linguaggio ad oggetti. Credo (non ho il libro sottomano per controllare) che
sia abbastanza simile a quello mostrato nel libro della GoF, che in effetti usa
codice piuttosto generico e non troppo idiomatico ne' negli esempi in C++ ne' in quelli in Smalltalk.
Dunque, facciamo un primo port del codice in Ruby.
class Drawer
def make_image( factory )
img = factory.new_image
line = factory.new_line( x1,y1, x2,y2 )
color = factory.new_color( "blue" )
line.color= color # questo è un metodo equivalente a setColor() in Java o C++
img.add( line )
...
return img
end
end
factory= PngFactory.new
drawer= Drawer.new.make_image(factory)
notate, che in ruby la creazione di un oggetto avviene richiamando il
metodo new() della classe, invece che un operatore.
In Smalltalk è lo stesso, in python, il metodo è __call__() ed in
Common Lisp è make-instance, ma la sostanza non cambia:
l'idea è che la creazione di un oggetto non è un operazione sintattica
ma è invece un normale metodo di cui è responsabile un oggetto.
Ora: sapendo che le classi sono normali oggetti, e che possiamo passarle
qua e là e richiamare dei loro metodi per crearne istanze è normale pensare
di cambiare le occorrenze di
factory.new_xyz( argomenti )
in
factory.xyz.new(argomenti)
Ricordo per chi non lo sapesse, che in ruby obj.field e obj.field=foo sono in
effetti chiamate a dei metodi esattamente come obj.getField()/obj.setField(foo) in Java/C++.
In questo modo non dobbiamo preoccuparci di scrivere metodi ad hoc, ma
semplicemente di avere dei modi per accedere alle classi.
D'altronde, ruby ha un meccanismo migliore dell'avere un
oggetto di classe Factory tanto per contenere degli oggetti,
i moduli.
I moduli in ruby, come anche in python, non solo sono molto utili per pacchettizzare il codice, ma sono anche
oggetti di prima classe, cioè oggetti manipolabili come tutti gli altri.
Un modulo è un namespace che può contenere costanti, variabili e metodi
e che può essere incluso in un altro oggetto, funzionando come mixin.
In questo modo l'oggetto ottiene magicamente tutta la roba che c'era nel
modulo, senza bisogno di reimplementare nulla, ed evitando di inquinare il codice
con metodi globali.
Dunque, invece di usare un parametro factory per
make_image, possiamo semplicemente includere un modulo
MyFactory nell'oggetto, e far riferimento ai nomi delle classi:
# codice di Drawer, fornisce la logica di creazione.
# non usiamo _nessun_ accorgimento
# per rendere la classe modulare,
# essa è identica alla prima versione in PHP
class Drawer
def make_image
img = Image.new
line = Line.new( x1,y1, x2,y2 )
...
return img
end
end
# codice del modulo, contiene le classi.
# Anche qui non dobbiamo fare niente di speciale
# nessun metodo pensato per l'uso specifico
# e nessun codice di boilerplate
module PNG
class Color
...
end
class Line
...
end
...
end
# codice per il setup, basandoci su file di configurazione o altro
Configuration.setup(Drawer, PNG)
drawer= Drawer.new.make_image
Va notato che ci sarà comunque un punto nel programma in cui si dovrà
stabilire quale factory vada usata, ma questo non inficia la possibilità
di minimizzare la dipendenza del codice da variabili cablate: possiamo
mettere le informazioni su quale classe usare in un file esterno,
passarle da riga di comando, mantenerle in un modulo specifico dedicato
alla configurazione, etc.
L'Abstract Factory Pattern in fondo non è complicato, è una semplice applicazione
del motto "programma pensando ad un'interfaccia, non ad un'implementazione",
quindi è naturale risolverlo in questo modo semplice.
L'allevamento di serpenti
La scomposizione funzionale che abbiamo visto non ha in effetti molto che la faccia dipendere dalla OOP.
Non abbiamo effettivamente bisogno di una Fabbrica di oggetti, basta che abbiamo delle funzioni che lo facciano
raccolte in un namespace.
Certo, la decomposizione in classi è ovvia se pensiamo in ambito OO,
ma non è certo l'unica, infatti abbiamo appena visto come avere moduli di prima classe permetta di
semplificare massicciamente il codice, ed abbiamo anche detto che anche python ha questa funzionalità.
Proviamo quindi a vedere esempio alternativo, non propriamente OO ma abbastanza pythonico .
Portare il codice iniziale in python significa sostanzialmente
prendere il codice ruby
cancellare gli "end"
aggiungere qualche parentesi e ":"
che non sarebbe interessante.
Vedremo dunque direttamente una versione basata sui moduli in python.
supponiamo di avere un file pngimage.py fatto più o meno così:
# invece di funzioni potrebbero essere classi
def new_image():
..
def new_color():
..
ed in un altro file :
import pngimage
def build_image(factory_module):
img= factory_module.new_image()
line= factory_module.new_line(x1,y1, x2,y2)
...
return img
build_image(pngimage)
Ma anche in questo caso, come nel caso del codice ruby, perché dovremmo
preoccuparci dell'argomento
factory?
Quello che possiamo fare è far riferimento semplicemente alle funzioni (o classi) di cui
abbiamo bisogno, ed usando il sistema dei moduli di python,
importare il modulo nella fase di setup assegnandogli il nome che vogliamo:
from pngimage import line, color..
ed il gioco è fatto.
In effetti, un esempio di questo approccio è visibile anche nella
libreria standard di python, basti pensare ai moduli "sys" ed "os", che
forniscono un'interfaccia astratta che corrisponde ad implementazioni
differenti sulle varie macchine.
La piccola bottega alchemica
Per spirito enciclopedico, aggiungiamo un'altra analisi interessante sul come risolvere questo problema.
In realtà poco di questo è farina del mio sacco [5], ma farò finta di nulla e ne parlerò
lo stesso.
Il Common Lisp possiede un "sottosistema" OO detto CLOS (Common Lisp Object System) , il quale
offre un approccio alla programmazione ad oggetti molto particolare ma anche molto potente ed interessante,
se ci sarà un seguito di questi articoli ne vedremo alcuni aspetti, ma per ora considerate solo che esso ci permetta di
definire delle classi e dei metodi sche lavorano su quelle classi.
Le funzionalità della classe vengono raccolte in funzioni generiche, ovverò funzioni che vengono definite più
volte ogni volta specificando un tipo diverso per le variabili, in modo vagamente simile all'overloading di Java o C++
ma molto più potente.
Ad ogni modo, per creare un oggetto si fa più o meno così:
(make-instance 'klasse )
dove 'klasse è un simbolo rappresentante il nome di una classe.
I lettori attenti potrebbero ricordare cosa significa questo, cioè che in CL la costruzione/inizializzazione di un
oggetto è una semplice funzione come le altre, il che ci permette di adottare la stessa tecnica già vista:
; questi sono metodi specializzati per essere chiamati solo se l'argomento è di tipo
; png-image,
(defmethod make-line ((image png-image))
(make-instance 'png-line))
(defmethod make-color ((image png-image))
(make-instance 'png-color))
(defun draw (image-type)
; let* introduce delle variabili locali
(let* ((my-image (make-instance image-type))
(my-line (make-line my-image))
(my-color (make-color my-image)))
(setf (color my-line) my-color)
(add my-line my-image)
my-image))
..(draw 'png-image)
Si noti che i metodi in CL possono subire specializzazione su tutti i parametri, mentre in linguaggi
OO tradizionali il dispatch avviene solamente sul primo argomento (che spesso è anche implicito, ed è il this
o self)
Sebbene il codice sia compatto, rimane il fatto che ci si sta portando dietro un parametro
image-type di cui potremmo fare a meno.
In precedenza avevamo risolto questo stesso problema usando delle variabili accessibili in scope più ampi della
funzione (visibilità globale, o a livello di classe o di modulo), e possiamo farlo anche in Common Lisp, anzi possiamo
farlo in un modo migliore grazie alle variabili speciali .
Una variabile speciale è sostanzialmente una variabile con visibilità dinamica invece che lessicale.
Ciò significa che se dichiaro una variabile globale *image-type*, la uso all'interno della funzione
draw ma poi chiamo draw dall'interno di scope differenti, la funzione vedrà valori
differenti nella variabile, e non solo quello che ha la variabile globale.
Se avete familiarità con il perl dovrebbe essere una cosa simile al trattare le variabile con local
Quindi il nostro codice potrà essere:
; usiamo direttamente le variabili speciali nei nostri metodi
; abbiamo così rimosso la necessità di usare un parametro
(defun make-image ()
(make-instance *image-type*))
(defun make-line ()
(make-instance *line-type*))
(defun make-color ()
(make-instance *color-type*))
; anche draw risulta semplificato dall'assenza di parametri
(defun draw ()
(let ((my-image (make-image))
(my-line (make-line))
(my-color (make-color)))
(setf (color my-line) my-color)
(add my-line my-image)
my-image))
; affinché tutto funzioni draw deve essere chiamato in un contesto in cui le
; variabili speciali abbiano un valore appropriato. Senza manomettere lo scope globale,
; le dichiariamo come locali con let
(let ((*image-type* 'png-image)
(*line-type* 'png-line)
(*color-type* 'png-color))
(draw))
Ci stiamo di nuovo avvicinando all'asintoto dove il pattern diventa invisibile, ma abbiamo ancora quelle brutte
assegnazioni.
Però possiamo ancora usare la caratteristica più importante del lisp, quello strumento incredibile che sono le macro.
Senza entrare nei dettagli, pensate alle macro in Common Lisp come ad uno strumento che permette di distillare
un'astrazione trasformando qualsiasi cosa in un costrutto sintattico. Nel nostro caso, possiamo far sparire quel brutto
let usando una sintassi come questa:
(with-image-type (png)
(draw))
che è decisamente più leggibile.
Veloci conclusioni
Sembra dunque che la teoria iniziale, per questo DP sia stata dimostrata.
L'obiettivo per il quale è nato l'Abstract Factory Pattern è raggiungibile in linguaggi di livello molto alto semplicemente
seguendo un paio di convenzioni, senza nessuna riga di codice di boilerplate (python, ruby), o costruendo
un'astrazione riusabile che nasconda la logica necessaria (la macro in Common Lisp)
Non so se questo sia valido anche per linguaggi in cui classi e moduli non sono oggetti manipolabili, ma
l'impressione che questa piccola ricerca fornisce è che la decomposizione OO sia un metodo per
raggiungere l'intento stabilito all'inizio, non parte dell'intento stesso.
Probabilmente è banale, lo so, ma ho l'impressione che i concetti banali vengano dimenticati spesso.
In effetti, a pensarci bene potremmo avere una decomposizione direttamente in termini di sole funzioni,
anche non avendo i moduli.
Basterebbe avere una funzione di ordine superiore che restituisca una lista di funzioni le quali implementino le varie
funzionalità. Una chiamata a tale funzione sarebbe equivalente alla fase di setup, ed il codice manterrebbe una
massima modularità con minimo sforzo, anche in assenza totale di una infrastruttura OO (e di first class module).
Sarebbe interessante anche commentare le soluzioni nei linguaggi più statici (Java con reflection, C++ con i template)
ed in quelli meno spiccatamente OO (tcl, perl), cercherò di farlo in futuri articoli, nel frattempo invito eventuali lettori a
provvedere, magari usando l'editor online per un micro
articolo.
Avrei anche un esempio in Haskell da commentare a riguardo, per confutare la tesi di Norvig che siano i linguaggi
dinamici a rendere invisibili i DP, ma è abbastanza lungo da meritarsi un articolo a se stante :)
Informazioni sull'autore
Gabriele Renzi, studente di Ingegneria Informatica, è un appassionato di programmazione e sistemi operativi. Scrive "hello world" in una dozzina di linguaggi ma non riesce ad andare oltre nemmeno in uno. Collabora con il Progetto Documentazione Italiana FreeBSD ed è membro del Gruppo Utenti Ruby Italia.
Parrot fa uso di molt tecniche interessanti per supportare in modo efficiente molti linguaggi,
tra cui threaded code (tipico nelle VM per Forth, non correlato ai thread), continuation passing style
(da tradizione Scheme-sca) generatori di codice e molto altro
http://www.sidhe.org/~dan/presentations/Parrot_Implementation.pdf
Una delle idee interessanti in perl6 è l'uso di operatori rappresnetati con caratteri unicode;. l'operatore "yen"
viene usato per fondere insieme due sequenze
elemento per elemento
http://dev.perl.org/perl6/doc/design/syn/S03.html
La mailing list italiana dedicata al lisp è piena di gente davvero competente, non come me, e potete leggere il
piccolo scambio di mail riguardo questo problema iscrivendovi e richiedendo l'archivio
(non disponibile su web al momento)
http://www.lispmachine.org