per collaborazioni, commenti, critiche, e altro contattateci alla e-mail: clubinfo@libero.it risponderemo al più presto! |
Introduzione. |
Cosa è un pattern? |
Livelli di astrazione |
Componenti di un pattern |
Qualità di un pattern |
Alcuni esempi |
Conclusioni |
Anti pattern. |
Bibliografia. |
Alcuni siti interessanti: |
Con la parola “software” non si intende il solo codice eseguibile localmente sulla macchina, bensì si tratta di un insieme di documenti, procedure, processi, regole, schemi, relativi allo sviluppo di una applicazione. Il codice eseguibile o i listati sorgenti sono solo una minima parte del blocco di documenti.
L’ingegneria del software insegna che la vita di un prodotto non dovrebbe finire mai, in quanto dopo le tre fasi per lo sviluppo:
1) analisi
2) progettazione
3) codifica
esiste una quarta fase chiamata manutenzione, che prevede necessariamente di dover tornare indietro ad una delle tre fasi precedenti, per porre delle modifiche. Modificare un sistema software già esistente è molto importante, è viene fatto per tre motivi:
1) eliminazione di difetti
2) adattamento del software ai cambiamenti sociali (vedi euro)
3) aggiunta di funzionalità
Il motivo per cui si preferisce intervenire su qualcosa di esistente, piuttosto che ricominciare l’attività da zero, è legato essenzialmente all’aspetto economico e temporale. Se infatti da un lato ricominciare da zero può portare a soluzioni più eleganti per certi problemi, dall’altro lato si incorre a costi troppo elevati per lavori già sostenuti e tempi molto lunghi senza per altro la sicurezza di ottenere un prodotto migliore.
Al livello più basso, fortunatamente, i linguaggi orientati agli oggetti vengono incontro a queste necessità offrendo importanti caratteristiche per la ricusabilità e la modificabilità del codice:
1) information hiding
2) incapsulamento
3) ereditarietà
4) polimorfismo
5) overloading
Tuttavia questo miglioramento si ha solo nell’ultimo e per certi versi meno significativo (tutte le scelte progettuali sono già state fatte) aspetto dello sviluppo. E’ stato largamente dimostrato che intervenire su dei problemi nati in fase di analisi o di progettazione è molto più difficile (leggi costoso) che non in fase di codifica.
Per quanto riguarda queste due fasi, quindi, è molto difficile indicare come muoversi e quali siano le soluzioni migliori per affrontare determinati problemi, in quanto non essendo strutturate mediante linguaggi formali (a differenza della fase di codifica) non si può costruire così facilmente una cultura comune. Non è facile trovare le soluzioni migliori a problemi specifici, ci vuole molta esperienza, ed è un peccato non poter comunicare agli altri tale conoscenza, così da creare una vera e propria scienza ingegnieristica. Per far ciò risulta necessario stabilire un vocabolario per esprimere tali concetti e un linguaggio per collegarli.
Per questo ci vengono incontro i pattern.
Un pattern è un nucleo di idee, che contiene al suo interno l’essenza di una soluzione sicura e testata ad un problema ricorrente in un determinato contesto.
Non si tratta solo di un principio astratto o di una metodologia, ma è un insieme di elementi legati da delle relazioni ben precise, che porta ad una soluzione reale e concreta.
Sostanzialmente un pattern non indica solo cosa fare, ma specifica anche qual è la strada migliore per farlo: contiene l’aspetto pratico.
Tale concetto, può essere espresso a più livelli di astrazione:
pattern architetturali (o concettuali); esprimono una organizzazione strutturale o uno schema per definire l’architettura ad alto livello di un sistema software. Forniscono un insieme di sottosistemi, le loro responsabilità e le relazioni tra di essi. Questi concetti sono descritti mediante termini e concetti che fanno parte del dominio dell’applicazione.
design pattern; forniscono degli schemi per rifinire i sottosistemi e componenti di un sistema software. Descrivono la struttura di interfacciamento tra le componenti, e le loro relazioni. Queste sono descritte mediante costrutti di analisi del software come oggetti, classi, ereditarietà, relazioni usa o component of.
idiomi ( o pattern di codifica); si tratta di pattern di basso livello, ovvero applicabile ad uno specifico linguaggio di programmazione. Essi descrivono come implementare gli aspetti di un problema usando un set specifico e ben definito di comandi e operazioni offerte dal linguaggio.
L’esposizione di un pattern è effettuata mediante una descrizione in linguaggio naturale, ma segue una struttura logica ben precisa:
Nome; per potersi riferire ad un pattern, senza dover ogni volta descriverlo in dettaglio ogni pattern possiede un nome che deve essere corto e estremamente significativo. Molti pattern vengono nominati in pubblicazioni internazionali, senza essere minimamente spiegati.
Problema; è la descrizione del problema che viene affrontato e risolto, ovvero l’obbiettivo ultimo: lo scopo che si vuole raggiungere.
Contesto; è un elenco di vincoli e condizioni per le quali il pattern può essere applicato per raggiungere la soluzione. Il contesto è molto importante per definire l’applicabilità del patten stesso.
Componenti; è una descrizione dei moduli e degli elementi che entrano in gioco, e delle loro interazioni. Fornisce la motivazione per cui l’applicazione di tale pattern può risultare una buona soluzione.
Soluzione; a seconda del tipo di pattern si può trattare di una porzione di codice, o di un insieme di entità e di regole statiche e dinamiche che ne definiscono il comportamento; oppure potrebbe essere un elenco di operazioni da effettuare. In questa descrizione può essere di notevole aiuto affidarsi ad immagini e diagrammi, come compendio alla spiegazione testuale.
Esempi; si tratta della descrizione di alcuni casi di applicazione del pattern
Contesto risultante; riporta le possibili conseguenze (sia positive che negative) dovute all’applicazione del pattern, ciò il contesto finale che si ottiene dopo l’applicazione. E’ una parte molto importante, soprattutto nel caso in cui si debbano applicare altri pattern.
Base logica; è una spiegazione non formale dei motivi per cui il pattern raggiunge la soluzione migliore al problema
Pattern affini; è un elenco di alternative possibili al pattern, in modo da fornire una possibile soluzione differente.
Trovandoci nel campo dell’ingegneria del software ha abbastanza senso parlare di qualità del pattern. Le caratteristiche di un pattern non sono molto diverse da quelle di un buon sistema software:
Incapsulamento e astrazione; ogni pattern deve incapsulare un problema ben definito e per esso deve fornire dei confini chiari e distinti che aiutino ad isolarlo dal contesto e a frammentarlo in più parti distine.
Modificabilità e variabilità; ogni pattern deve essere aperto a possibili estensioni e parametrizzazioni, in modo che possa lavorare insieme ad altri per risolvere una quantità maggiore di problemi. Un pattern dovrebbe inoltre essere possibile implementarlo in più modi.
Generatività e scomponibilità; ogni pattern, una volta applicato genera un nuovo contesto che sostituisce quello precedente e che deve la base per l’applicazione di altri pattern in maniera naturale e agevole. Questo permettere di risolvere ampi progetti attraverso l’applicazione ripetuta di diversi pattern.
Equilibrio; ogni pattern deve mantenere un equilibrio interno tra le sue componenti e le relazioni tra di esse.
Continuando a leggere l’articolo si troveranno alcuni esempi di pattern molto comuni. La presentazione di qualche esempio concreto mi sembra indispensabile affinché il concetto possa risultare più chiaro.
Il primo esempio che riporto, è un pattern di basso livello, ovvero un idioma del C++.
Lo scopo di tale pattern è di risolvere il problema di creare una classe che possa essere instanziata una sola volta.
Per risolvere questo problema basta definire una sola classe:
class Singleton {
public:
static Singleton* Instance(); // gives back a real object!
static proof(void); // proof that the object was made
protected:
Singleton(); // constructor
private:
static Singleton* _singleton;
};
Singleton* Singleton::_singleton = 0;
Singleton* Singleton::Instance() {
if (_singleton == 0)
{
_singleton = new Singleton;
} // end if
return _singleton;
} // end Instance()
Il costruttore della classe è protetto, e quindi non può essere invocata nessuna istanza. Tuttavia l’unica istanza della classe è definita come puntatore (statico/privato) alla classe stessa. Quando viene chiesta una istanza mediante la funzione membro Istance(), se nessuna istanza è stata ancora definta, allora istanzia la memoria necessaria per l’oggetto, altrimenti ritorna il puntatore dell’oggetto già creato.
Vediamo quali sono i vantaggi nell’usare questo pattern. Il controllo della singola istanza è assoluto, perché tale compito è svolto direttamente dalla classe stessa. La responsabilità del corretto funzionamento non è nelle mani del programmatore che andrà ad usarlo.
Il pattern, inoltre è semplice da estendere per permettere di creare degli oggetti Singleton più complessi. E’ molto facile inoltre estendere il pattern per far si che si possano istanziare un numero definito di oggetti (maggiore di uno).
Chi ha già programmato sotto Windows utilizzando le librerie MFC, oppure sotto dos con le Turbo Vision, si renderà conto che ha già usato questo pattern, pur non sapendo di che si trattava.
Questo pattern ritorna utile nel caso in cui si abbiano dei dati, dai quali dipendono più eventi. Ad esempio in un foglio di calcolo, il cambiamento di una cella può generare molte azioni, come il cambiamento di altre celle a causa di un legame, il cambiamento di un grafico, ecc… Gestire queste dipendenze può essere difficoltoso in quanto si possono facilmente generare dei cicli. Inoltre il sistema deve essere flessibile da permettere di aggiungere o eliminare delle dipendenze dinamicamente (se ad esempio si chiude il grafico, non è più necessario aggiornarlo, oppure si può aggiungerne un altro).
Una soluzione che non prevede l’applicazione del pattern è quella di lasciare agli oggetti il compito di andare a controllare periodicamente i dati, per verificare se ci sono cambiamenti. Risulta molto più efficiente fare in modo che sia l’oggetto osservato a notificare il proprio cambiamento agli oggetti osservatori.
I partecipanti in questo pattern sono:
1) il soggetto (ovvero i dati)
2) gli osservatori
Tutti gli oggetti osservatori devono possedere una interfaccia comune, chiamata Observer, che definisce la funzione membro update() che il soggetto chiamerà quando ci sono delle modifiche.
Il soggetto, invece deve possedere tre funzioni fondamentali:
addObserver(), che viene richiamata per aggiungere un osservatore nella lista interna
removeObserver(),che viene richiamata per togliere un osservatore nella lista interna
notify(), di tipo privato, usata per notificare a tutti gli observer nella lista le modifiche.
Un oggetto, come è noto può contenere altri oggetti come membri. In genere è meglio che l’oggetto base (chiamiamolo A) contenga un riferimento ad un altro oggetto (B), piuttosto che l’oggetto stesso.
Supponiamo che tale l’istanza dell’oggetto B avvenga nel metodo costruttore di A. Un problema che nasce da tale soluzione è che qualora si abbia la necessità di derivare un oggetto C da A, e si vuole cambiare il modo in cui istanziare l’oggetto B, questo non è possibile. Vediamo un esempio in Java:
public
class A {
protected C riferim;
public A() {
riferim = new C();
}
};
La soluzione consiste nel creare l’oggetto B esplicitamente, cioè lo si crea mediante un altro metodo. Nella classe derivata C è sufficiente ridefinire tale metodo per ottenere il comportamento voluto. Vediamo la soluzione:
public class
A {
protected B riferim;
public A() {
B = creaRiferim();
}
protected B creaRiferim () {
return new B();
}
}
Supponiamo adesso che R sia una classe derivata da B, per far usare R, piuttosto che B alla classe C basterà derivare e ridefinire creaRiferim():
protected B
creaRiferim() {
return new R();
}
I componenti di questo pattern sono:
1) prodotto, che definisce l’interfaccia dell’oggetto generato; nel nostro esempio si tratta di B.
2) creatore, è il metodo che restituisce un oggetto di tipo prodotto; nel nostro caso creaRiferim();
Molto spesso nei programmi, gli oggetti vengono suddivisi in gerarchie. Tali gerarchie sono tali che un oggetto che vi appartiene può contenere al suo interno riferimenti ad altri oggetti della stessa gerarchia. Tali oggetti sono una sorte di contenitore per altri oggetti della gerarchia.
Il problema che si pone è quello di trovare una struttura per tali oggetti adatta per trattare allo stesso modo oggetti semplici e oggetti contenitori. Sarebbe un problema, infatti, trattare diversamente e due tipi di oggetti.
La soluzione sta nel creare una classe astratta (interfaccia) che rappresenta entrambi I tipi di oggetto.
Chi si interfaccia con un oggetto della gerarchia non sa (ne ha bisogno di sapere) di che oggetto si tratta, ma può usare i metodi comuni messi a disposizione. Deve essere l’oggetto composto che si deve preoccupare di propagare il comando ricevuto a tutti i suoi componenti.
I componenti per tale pattern sono:
1) Component: definisce la classe astratta che è l’interfaccia agli oggetti della gerarchia, sia oggetti semplici che composti.
2) Leaf: rappresenta l’oggetto semplice, il quale non contiene altri oggetti della gerarchia.
3) Composite: questo componente contiene al suo interno altri oggetti della gerarchia
Al concetto di pattern si contrappone l’anti-pattern. Si tratta sempre di un mezzo per diffondere conoscenza relativa ad un problema. Se un pattern rappresenta una “buona pratica”, un anti-pattern può essere visto come “una lezione appresa”. Gli anti-pattern si dividono in due tipi:
quelli che descrivono una cattiva soluzione ad un problema causato,
quelli che descrivono come uscire da una cattiva situazione a procedere verso una buona soluzione.
Non bisogna sottovalutare l’importanza di questo strumento perché è fondamentale perché la presenza di buoni pattern in un sistema software spesso non basta; bisogna anche poter dimostrare che per un sistema che non ha avuto successo, questo è dovuto al fatto che tali pattern non sono stati applicati.
Il testo più significativo in campo di pattern è
E. Gamma,
R. Helm, R. Johnson, J. Vlissides, Design Patterns: Elements of Reusable
Object-Oriented Software, 1995, Addison_Wesley.
il quale ha dato inizio alla popolarità dei pattern nell’ambiente informatico.
http://www.enteract.com/~bradapp/,
sito curato da Brad Appleton in cui trovare un’ottima spiegazione sui pattern e
un gran numero di siti connessi.
www.mokabyte.com,
una rivista di informatica on-line in italiano che tratta tra i vari argomenti
anche i pattern:
http://www.enteract.com/~bradapp/docs/patterns-intro.html , sito in cui trovare ulteriori notizie sui pattern.
Questo articolo è stato scaricato dal Club di informatica |