Articoli Manifesto Tools Links Canali Libri Contatti ?
Design / Design Patterns

La babele dei design

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
di Gabriele Renzi Discuti sul forum   Stampa

Divagazione_1

È 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)

  1.     ... 
  2. label loop: 
  3.     compare v1, v2 ; 
  4.     jump_if_zero done; 
  5.     ... 
  6.     increment v1; 
  7.     jump loop 
  8. label done: 
  9.     ... 
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
  • idee implementative assortite[3]
  • 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

Divagazione_1 ¥[4] Divagazione_2

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:

  1.     class Drawer { 
  2.       function makeImage() { 
  3.         $img= new Image(); 
  4.         $line=new Line($x1,$y1, $x2,$y2); 
  5.         $color=new Color( "blue" ); 
  6.         $line->setColor= $color; 
  7.         $img->add($line); 
  8.          ... 
  9.         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):

  1.     class Drawer { 
  2.       function makeImage(Factory $factory) { 
  3.         $img =  $factory->newImage(); 
  4.         $line=  $factory->newLine($x1,$y1, $x2,$y2); 
  5.         $color= $factory->newColor("blue"); 
  6.         $line->setColor= $color; 
  7.         $img->add($line); 
  8.          ... 
  9.         return $img 
  10.  
  11.     class PngFactory extends Factory{ 
  12.       function newImage() { 
  13.         return new PngImage(); 
  14.       function newColor($name){ 
  15.         return new PngColor(...);  
  16.       ... 
  17.      
  18.     $factory= new PngFactory(); 
  19.     $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.

  1.     class Drawer  
  2.       def make_image( factory )  
  3.         img   =  factory.new_image 
  4.         line  =  factory.new_line( x1,y1, x2,y2 ) 
  5.         color = factory.new_color( "blue" ) 
  6.         line.color= color   # questo è un metodo equivalente a setColor() in Java o C++ 
  7.         img.add( line ) 
  8.          ... 
  9.         return img 
  10.       end 
  11.     end 
  12.  
  13.     factory= PngFactory.new 
  14.     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:

  1.     # codice di Drawer, fornisce la logica di creazione. 
  2.     # non usiamo _nessun_ accorgimento 
  3.     # per rendere la classe modulare, 
  4.     # essa è identica alla prima versione in PHP 
  5.      
  6.     class Drawer        
  7.       def make_image 
  8.         img   =  Image.new 
  9.         line  =  Line.new( x1,y1, x2,y2 ) 
  10.          ... 
  11.         return img 
  12.       end 
  13.     end 
  14.  
  15.      
  16.     # codice del modulo, contiene le classi. 
  17.     # Anche qui non dobbiamo fare niente di speciale 
  18.     # nessun metodo pensato per l'uso specifico 
  19.     # e nessun codice di boilerplate 
  20.      
  21.     module PNG 
  22.       class Color 
  23.         ... 
  24.       end 
  25.       class Line 
  26.         ... 
  27.       end 
  28.       ... 
  29.     end 
  30.      
  31.      
  32.     # codice per il setup, basandoci su file di configurazione o altro 
  33.     Configuration.setup(Drawer, PNG) 
  34.      
  35.     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ì:
  1.  
  2. # invece di funzioni potrebbero essere classi 
  3. def new_image(): 
  4.  .. 
  5.  
  6. def new_color(): 
  7.  .. 
ed in un altro file :
  1. import pngimage 
  2.  
  3. def build_image(factory_module): 
  4.  img= factory_module.new_image() 
  5.  line= factory_module.new_line(x1,y1, x2,y2) 
  6.  ... 
  7.  
  8.  return img 
  9.  
  10.  
  11. 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:
  1. ; questi sono metodi specializzati per essere chiamati solo se l'argomento è di tipo 
  2. ; png-image,  
  3. (defmethod make-line ((image png-image)) 
  4.   (make-instance 'png-line)) 
  5.  
  6. (defmethod make-color ((image png-image)) 
  7.   (make-instance 'png-color)) 
  8.  
  9.  
  10. (defun draw (image-type)  
  11.  
  12.    ; let* introduce delle variabili locali 
  13.    (let* ((my-image (make-instance image-type))   
  14.           (my-line  (make-line my-image))  
  15.           (my-color (make-color my-image))) 
  16.      (setf (color my-line) my-color)   
  17.      (add my-line my-image) 
  18.      my-image)) 
  19.       
  20. ..(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:

  1.  
  2. ; usiamo direttamente le variabili speciali nei nostri metodi 
  3. ; abbiamo così rimosso la necessità di usare un parametro 
  4. (defun make-image () 
  5.  (make-instance *image-type*)) 
  6.  
  7. (defun make-line () 
  8.  (make-instance *line-type*)) 
  9.  
  10. (defun make-color () 
  11.  (make-instance *color-type*)) 
  12.   
  13. ; anche draw risulta semplificato dall'assenza di parametri 
  14. (defun draw ()  
  15.    (let ((my-image  (make-image)) 
  16.           (my-line      (make-line)) 
  17.           (my-color    (make-color))) 
  18.      (setf (color my-line) my-color) 
  19.      (add my-line my-image) 
  20.      my-image)) 
  21.  
  22. ; affinché tutto funzioni draw deve essere chiamato in un contesto in cui le  
  23. ; variabili speciali abbiano un valore appropriato. Senza manomettere lo scope globale,  
  24. ; le dichiariamo come locali con let 
  25. (let ((*image-type* 'png-image) 
  26.       (*line-type*  'png-line) 
  27.       (*color-type* 'png-color)) 
  28.   (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.

È possibile consultare l'elenco degli articoli scritti da Gabriele Renzi.

Altri articoli sul tema Design / Design Patterns.

Risorse

  1. Il testo di Gamma, Helm, Johnson e Vlissides (la "Gang of Four") è decisamente una lettura obbligata per chi vuole essere un programmatore, al di là della critica che gli stiamo rivolgendo.
    http://www.amazon.com/exec/obidos/tg/detail/-/0201633612/002-3577799-4245635?v=glance
  2. Una presentazione di Peter Norvig in cui mostra l'invisibilità di alcuni DP in CL, Dylan e Smalltalk.
    http://www.norvig.com/design-patterns/
  3. 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
  4. 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
  5. 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
Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

Questo articolo o l'argomento ti ha interessato? Parliamone.