Su questo sito lo sforzo di tutti gli autori è quello di scrivere articoli che non siano semplici spezzoni di codice da copiare/incollare nei propri progetti, ma che siano d’aiuto nel migliorare la qualità del proprio codice, perché del codice scritto bene se non funziona si correggere, ma del codice scritto male se non funziona è davvero un bel guaio.
Abbiamo parlato in passato di teoria della programmazione ad oggetti ( qui ) metre oggi introdurrò l’argomento “design pattern” e di come questi possano aiutare a migliorare la qualità generale dei nostri programmi.
Vedremo in particolare un primo esempio di design pattern, molto semplice ma che, a mio avviso, è esplicativo dell’approccio che sta dietro tutti gli altri.
La storia
In principio c’era l’OOP…ma davvero “in principio”! Il primo linguaggio orientato agli oggetti risale infatti al 1967, non ascoltate quei professori che citano l’OOP come se fosse l’ultima frontiera in termini di paradigmi di programmazione…
In principio c’era l’OOP, dicevo, e i suoi cardini principali:
- Incapsulamento
- Ereditarietà
- Polimorfismo
Sfruttando sapientemente queste caratteristiche e con una buona dose di rigore si riuscivano e si riescono a scrivere programmi ben strutturati, chiari nella loro architettura e, soprattutto, facili da mantenere.
La programmazione ad oggetti, in breve OOP, non nasce né con lo scopo di creare programmi più veloci né per svolgere compiti che non possano essere svolti con la programmazione strutturata. Sopratutto su questo secondo punto fin dall’inizio erano tutti d’accordo: si sapeva bene infatti che tutti i linguaggi di programmazione in questo senso erano “Touring compatibili” che è un bel parolone (vedi macchina di touring corso di C qui) per dire in pratica che tutti i linguaggi di programmazione come li conosciamo sono in grado di calcolare le medesime funzioni in più o meno tempo, ma in ogni caso hanno tutti lo stesso potenziale.
Ma allora serve realmente a qualcosa la OOP oppure è solo fumo negli occhi?
Noi del club “OOP: un amore a prima vista” 😉 diciamo che “sì la OOP è l’invenzione migliore dopo il linguaggio C” (e trascuriamo volutamente il C++)
Lo scopo della OOP è quello di aiutare l’uomo, con le sue ridotte capacità, a scrivere codice sorgente facile da capire, modificare, da testare e da mantenere. E ribadisco che è un ausilio per l’uomo e non per la macchina, perché se è vero che il computer “gradirebbe” meglio un piccolissimo file in assembly piuttosto che 10mila righe di codice in obj-c è anche vero che il il computer impiega qualche secondo in più per la fase di compilazione, ma un uomo ci metterebbe anni a scrivere un programma intero in assembly
Ma la OOP da sola fornisce solo gli strumenti di base, non è la bacchetta magica per realizzare dei programmi ben strutturati. Non soprende quindi che nel corso degli anni diversi GURU abbiamo poi proposto le loro tecniche di scrittura e organizzazione del software, fornendo consigli e linee guida in tal senso.
Non vorrei spoilerare il seguito dell’articolo e quindi cito come esempio “Single point of responsability”, che non è qualcosa che sta dentro la OOP e neache un design pattern, è un’idea che bisogna avere in testa quando si progetta l’architettura di un software. L’idea è che in un software deve essere facilmente identificabile il responsabile di qualcosa. È un consiglio, è una buona pratica da seguire.
Io l’ho imparata a mie spese diversi (!!) anni fa, quando purtroppo non esisteva devAPP a spiegarmi le cose e non avevo mai badato al concetto di responsabilità. Avevo sviluppato un piccolo gestionale per l’emissione di fatture/bolle/preventivi (erano circa una dozzina di tipi di documenti diversi) e io avevo inserito il codice per la formattazione dei report di stampa in 12 punti diversi…ovviamente quando il cliente mi chiamava per aggiungere un riga qui o un logo lì era una tragedia. Quella è stata l’esperienza che mi ha insegnato il valore di “single point of responsability”.
Con una buona conoscenza della OOP e cercando di seguire questa e diverse altre linee guida è più facile scrivere programmi ben strutturati.
In questo contesto nascono e vivono i Design Pattern.
La loro data di nascita coincide con la data di pubblicazione del libro che è, a tutt’oggi, ritenuta la bibbia dei design patter (amazon) pubblicato nel 1997 da 4 ragazzi che si fanno chiamare “Gang of Four”. Uno dei quali (http://www.bignerdranch.com/instructors/hillegass.shtml) ha lavorato alcuni anni in Apple come docente su Cocoa (nel perio di passaggio next-step/apple ed è per questo e per la loro oggettiva qualità che i libri della “Gang of four” sono ritenuti, soprattutto nel mondo mac, dei must.
La definizione formale di design patter è “una soluzione progettuale generale a un problema ricorrente”, che in pratica significa fornire una possibile soluzione al problema di come creare l’architettura delle classi quando ci si trova davanti ad un problema già noto.
Magari starai dicendo “Ma la mia app per iphone è super cool, scialla di brutto, prende il testo scritto da me con le k, i xké e i tvb e magikamente lo fa diventare come vuole il vekkio prof di itaGLIano…come fanno quattro cowboy del 1997 a risolvere i miei problemi con il codice? E poi se c’ho problemi io scrivo a devAPP!”
Ecco, se parli così magari qualche problema ce l’hai per davvero…ma il concetto è che i design patterns sono una soluzione ad un problema generico, ma ricorrente, che si presenta durante la scrittura di un software, indipendentemente dalla specifica implementazione.
Problemi ai quali i desgn pattern possono dare una risposta sono ad esempio:
“Come faccio a creare una classe il cui comportamento non sia blindato all’interno della classe stessa, ma che possa variare a seconda del contesto in cui opera?”
oppure:
“Ho diversi oggetti nel mio programma interessati allo stato di un altro oggetto, come posso fare per mantenerli aggiornati ed evitare l’overhead di continue richieste?”
Ho scelto questi due esempi di proposito, perché la risposta fornita dalla GoFour è in ordine “Delegate design pattern” e “Observer design patter“. Anche se i nomi non vi dicono nulla li avrete sicuramente già incontrati nello sviluppo delle applicazioni iOS, il primo ad esempio nella generazione delle tabelle (UITableViewDelegate) e il secondo nella KVO (Key Value Observer).
Troviamo una soluzione con un design pattern.
Non c’è modo migliore per far comprendere l’utilità di un design pattern se non quello di formulare un esempio, in questo caso mancando totalmente di fantasia propongo un esempio molto simile a quello presentato nel libro “Head firs design patter“.
Immaginatevi programmatori neo assunti in un’azienda di produzione videogiochi, la D&P Game Inc. il vostro primo compito è quello di realizzare una classe che rappresenti un oggetto del loro nuovissimo videogioco, in particolare si tratta di un’anatra.
Il responsabile del progetto nel brief iniziale vi liquida dicendovi “devi programmare una classe che rappresenta un’anatra”.
Voi provetti programmatori chiedete insistentemente almeno delle specifiche un filo più dettagliate e ottenete questa risposta:
“Un’anatra! che cosa c’è da dire di più? Le anatre camminano, nuotano, starnazzano…e poi si, certo che pensiamo in futuro di avere altre versioni di anatre più evolute, ma ancora l’amministrazione non ha approvato i fondi e quindi non abbiamo nulla di definitivo, tu conosci la OOP, crea qualcosa che domani possiamo stravolgere completamente senza dover riscrivere il codice”.
Bene, pensate, ve la siete cavati con poco, e via a scrivere il codice.
@interface Duck : NSObject
-(void)swim;
-(void)walk;
-(void)quack;
-(void)display;
@end
Questa è la classe principale, quando ci saranno delle specifiche anatre si sfrutterà l’ereditarietà e si creeranno delle subclass di Duck eseguendo l’override degli specifici metodi oppure aggiungendovene altri…intendiamo quindi sfruttare l’ereditarietà per dare versatilità al programma.
Immediatamente ecco che arriva la prima modifica:vi viene detto che sono previsti due tipi di anatra nel videogioco, un germano e un’anatra comune.
Forti della OOP create immediatamente la subclass “MallardDuck” con l’override del solo metodo display, infatti un germano nuota, cammina e starnazza proprio come tutte le altre anatre.
#import "Duck.h"
@interface MallardDuck : Duck
@end
@implementation MallardDuck
-(void)display {
//Display specifico per il germano reale.
}
@end
Fin qui tutto bene, avete applicato correttamente i principi della OOP sfruttando l’ereditarietà, inoltre il programa è coerente con l’approccio DRY (Dont Repeat Yourself) perché se aveste optato per creare due classi separate vi sareste trovati a dover duplicare il codice degli altri metodi: swim, walk e quack.
Felici della vostra soluzione partecipate alla successiva riunione con il team, dove sembra prendere piede un’idea che sapete vi complicherà il lavoro: l’autore del videogioco ha deciso che l’anatra in realtà è un’anatra con dei superpoteri che in certe occasioni cammina e starnazza normalmente, certe altre, invece, corre a velocità supersonica ed emette un richiamo agli ultrasuoni.
Questo è un modo bizzarro per dire che il comportamento dell’anatra può cambiare a runtime, cioè durante l’esecuzione del programma.
La OOP vi ha preparato anche a questo, in fondo tutti sanno che un oggetto software è definito dal suo comportamento, il quale può variare in funzione dello stato interno dell’oggetto, giù quindi a scrivere l’implementazione della class modificata per variare il suo comportamento a runtime
@implementation Duck
//Questa variabile memorizza lo stato interno dell'anatra
int duckState;
-(void)walk {
if (duckState == 0) {
//cammina normalmente
} else {
//cammina velocità supersonica.
}
}
-(void)quack {
if (duckState == 0) {
//starnazza normalmente
} else {
//starnazza agli ultrasuoni.
}
}
@end
Bene, avete cambiato la vostra classe e avete risposto alle modifiche che vi erano state chieste durante la riunione. Ma siete ancora orgogliosi e fieri del vostro codice? Non iniziate ad avere *paura* che durante la prossima riunione possa venire fuori qualche altra modifica che vi obbligherà a cambiare tutto un’altra volta?
Per esempio se io avessi scritto quel codice inizierei a sperare che:
- non mi chiedano di far starnazzare l’anatra con gli ultrasuoni mentre cammina normalmente (bisognerebbe usare due variabili separate)
- non mi chiedano di aggiungere un nuovo stato dell’anatra (dovrei rivedere tutti gli if e aggiungere una nuova condizione)
- non mi chiedano di aggiungere un nuovo tipo di camminata, che mi costringerebbe ad usare due stati solo per distinguere il tipo di camminata…
- non siano presenti tante subclass, perché in ogni subclass devo ripetere la logica degli stati e degli if…
Insomma, è bastato aggiungere un’inezia al software per renderlo già molto più rigido e difficile da modificare e questo non è un bene.
La risposta data dalla GaOfFour nel 1997 è racchiusa in un design pattern che si chiama “Strategy”.
Questa è la definizione formale:
Strategy: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy let the algorithm vary independently from clients that use it.
Il senso è più o meno: Prendi una famiglia di algoritmi, e crea delle classi che li incapsulino e fai in modo che abbiano tutti la stessa interfaccia. Strategy ti permette di usare uno o l’altro algoritmo senza che la classe che lo utilizza ne venga in qualche modo interessata.
Detto così sembra che non c’entri molto con anatre e superpoteri, ma invece c’entra eccome, basta saper guardare bene.
Il consiglio che ci arriva da questo design pattern è quello di evidenziare le parti del codice (gli algoritmi) che sono più soggetti a variazione e spostarli su classi separate, facendo in modo che abbiano la stessa interfaccia e che quindi possano essere usati alternativamente.
In questo caso quello che varia nel nostro codice è proprio il verso dell’anatra e il suo modo di camminare, proviamo quindi a seguire questo consiglio per questo secondo aspetto, per il primo poi seguiremo la stessa logica.
Creiamo quindi la classe WalkBehavior che servirà ad incapsulare il modo di camminare dell’anatra
@interface WalkBehavior : NSObject
-(void)walk;
@end
@implementation WalkBehavior
-(void)walk {
NSLog(@"standard walking");
}
@end
e ne creiamo immediatamente una subclass per la velocità supersonica
@interface WalkBehaviorSuperSpeed : WalkBehavior
-(void)walk;
@end
@implementation WalkBehaviorSuperSpeed
-(void)walk {
NSLog(@"supersonic walking speed");
}
@end
Adesso possiamo dire che l’anatra ha un (“has a” in OOP ha un significato specifico) modo di camminare, vediamo infatti nell’header della classe la presenza di una property di tipo WalkBehavior.
@interface Duck : NSObject
@property (nonatomic, retain) WalkBehavior *walkBehavior
-(void)swim;
-(void)walk;
-(void)quack;
-(void)display;
@end
L’implementazione del metodo walk cambia e diventa
-(void)walk {
[walkBehavior walk];
}
In questo modo possiamo cambiare a runtime il modo di camminare dell’anatra semplicemente assegnandole un nuovo WalkBehavior, come in questo esempio:
Duck *duck = [[Duck alloc] init];
WalkBehavior *walkBH = [[WalkBehavior alloc] init];
[duck setWalkBehavior:walkBH];'
[duck walk]; //stampa "standard walking"
WalkBehaviorSuperSpeed *walkBHSuperS = [[WalkBehaviorSuperSpeed alloc] init];
[duck setWalkBehavior:walkBHSuperS];
[duck walk]; //stampa "supersonic waking speed"
Siamo riusciti in pratica a fare quanto suggerito dal design pattern Strategy, abbiamo separato le parti di codice che sono soggette a cambiamento, ne abbiamo fatto delle classi esterne e abbiamo dato a tutte la stessa interfaccia, in questo modo la classe che le utilizza può usarne una qualsiasi indifferentemente.
Spero che sia chiaro, nonostante l’esempio fantasioso, quale sia la logica alla base di queste modifiche e vorrei concludere soltanto citando qualcuno dei vantaggi ottenuti utilizzando questo approccio:
- Non abbiamo più bisogno di mantenere informazioni sullo stato interno dell’oggetto
- Posso cambiare separatamente tutti i comportamenti, non è un problema quindi mischiare comportamenti da superpoteri e comportamenti normali.
- La creazione di nuovi modi di camminare non influenza minimamente la classe Duck, che potrà utilizzare senza alcuna modifica anche ne nuove subclass di WalkBehavior.
Una nota per i più curiosi:I design pattern fanno un uso continuo del concetto di interfaccia e classe astratta, costrutti presenti in java e in altri linguaggi ma non presenti in obj-c.
In particoalre Strategy prevede che la classe WalkBehavior sia una classe astratta, mentre nell’esempio è una classe concreta. Avrei potuto utilizzare il concetto di protocol obj-c che è molto simile ad una classe astratta, ma ho preferito evitare per non aggiungere complessita o creare confusione con il delegate design pattern.
Alla prossima!
7 Responses to “Introduzione ai Design Patterns”
12 Novembre 2012
freellinoottimo lavoro… come sempre!
12 Novembre 2012
Ignaziocovviamente il “come sempre” era riferito alla gang of four, giusto?
12 Novembre 2012
Acunamatata“Alzi la mano chi pensa che stia usando devAPP per dare sfogo a mie personalissime frustrazioni”
ghghhghghhghghgh complimenti, magistrale come sempre…
12 Novembre 2012
Ignaziocfa un pò zelig:
http://sphotos-a.xx.fbcdn.net/hphotos-ash4/197297_146165498780290_3123699_n.jpg
17 Novembre 2012
GaetanoChiarissimo e utilissimo, speravo da tanto che DevApp parlasse dei design pattern in maniera chiara e soprattutto con degli esempi chiarificatori!
19 Novembre 2012
Francesco AbboOttimo spunto per far riflettere programmatori meno esperti su una progettazione del software secondo logiche modulari e ottimizzate… Difficile trovare articoli sul tema se non i manuali specializzati. Complimenti!
11 Gennaio 2013
AlessandroComplimenti per l’ottima introduzione , è stata chiarissima e illuminante.