I campi dell'intestazione del messaggio

Gran parte dei campi dell'intestazione sono inizializzati dal metodo che lo manda:
  • JMSDestination
  • JMSDeliveryMode
  • JMSExpiration
  • JMSPriority
  • JMSMessageID
  • JMSTimestamp
Altri tre sono settati dal cliente:
  • JMSCorrelationID
  • JMSReplyTo
  • JMSType
E infine il provider setta
  • JMSRedelivered.
L'amministratore del sistema può configurare JMS per dare un valore specifico per JMSDeliveryMode, JMSExpiration e JMSPriority.
  • JMSDestination: destinazione a cui é stato mandato il messaggio.
  • JMSDeliveryMode: modo di spedizione secondo il quale é stato mandato il messaggio.
  • JMSMessageID: identifica univocamente ogni messaggio spedito da un provider
  • JMSTimestamp: é il momento in cui il messaggio é stato passato a un provider per essere spedito.
  • MSCorrelationID: può essere usato dal client per creare una relazione tra messaggi.
  • JMSReplyTo: la destinazione dove si dovrebbe mandare la risposta a un determinato messaggio.
  • JMSRedelivered: settato se il messaggio é stato mandato più volte, e quindi forse già recepito dal client.
  • JMSType: tipo del messaggio.
  • JMSExpiration: Settato a zero nel caso il messaggio sia sempre valido.
  • JMSPriority: priorità dal messaggio definita nell'intervallo [0..9], con 0 ad indicare il valore più basso.

Usare i template

Nel sesto capitolo di C++ Templates: The Complete Guide si fanno alcune utili considerazioni sull'uso pratico delle template.

Nel primo paragrafo si spiega come i dettagli implementativi propri dei template causino un dilemma e, possibilimente, anche degli errori a livello di linkaggio. La soluzione standard é quella del cosiddetto "inclusion model" che consiste in pratica nel mettere dichiarazione della classe template e la sua definizione nello stesso file che viene incluso dal codice che lo usa.

Nel secondo paragrafo si illustra il metodo dell'"instanziazione esplicita" che consiste in pratica nell'indicare esplicitamente quale template si voglia utilizzare, in modo da dare un suggerimento al linker sul lavoro da fare con la classe template. Il problema é che le regole sottese sono davvero poco elastiche: richiedere più di una volta l'esplicita istanziazione di un template porta a errori in fase di linkaggio e definizioni duplicate delle entità instanziate.

E' possibile sviluppare codice aperto alla scelta del modello inclusivo e della instaziazione esplicita due file di include per ogni template, includendo uno o l'altro header a seconda del modello che intendiamo usare. L'opzione viene dettagliata nel sottoparagrafo 6.2.2, a me sembra francamente sorgente di incomprensioni anche in un progetto di media dimensione, e quindi lascerei perdere.

Nel terzo paragrafo si mostra il separation model, in pratica si fa riferimento alla keyword export applicata alla dichiarazione di un template, in modo da renderla utilizzabile al codice seguente anche se la definizione del codice non é visibile. E' da notare che le funzioni inline all'interno di un template non vengono esportate, dato che non é possibile combinare export e inline.

Nonostante questo problema, il modello separativo sembra notevolmente più attraente di quello inclusivo, sfortunatamente ha altre limitazioni. Non molti compilatori supportano la parola chiave export, e quindi il codice scritto in questo modo rischia di non essere portabile. Secondariamente il modello separativo sposta il lavoro dalla fase di compilazione a quella del linkaggio, e nella realtà si scopre spesso che i vantaggi in termini di tempi impiegati per generare l'eseguibile non cambiano sostanzialmente. Inoltre ci possono essere dei problemi a livello semantico, che verranno trattati più avanti nel testo.

Il sesto paragrafo affronta il problema del debug per template. Verificare il funzionamento di un template per ogni possibile tipo sembra un compito impossibile, inoltre la diagnostica per gli errori generati all'interno di un template é a prima vista terrorizzante.

Complicazioni di base sui template

Il quinto capitolo di C++ Templates: The Complete Guide é dedicato ad aspetti dei template che pur essendo di base sono anche un po' spinosi.

Nel primo paragrafo si esplora l'uso di typename e del costrutto .template, situazioni che capiterà di rado di affrontare nella programmazione quotidiana, ma meglio non farsi cogliere impreparati.

Il secondo paragrafo mette sull'avviso nei confronti di una stranezza dei template. Nel caso di classi derivate l'uso di this-> é necessario per riferirsi a dati o metodi della superclasse. Vedremo meglio la faccenda più avanti, nel capitolo nove del libro, ci si assicura.

Il terzo paragrafo ci introduce alla parametrizzazione di membri di template.

Ad esempio, la nostra classe Stack potrebbe essere resa più elastica definendo l'operatore assegnamento in modo parametrico:

template <typename T>
class Stack {
// ...

// assign stack of elements of type T2
template <typename T2>
Stack<T>& operator= (Stack<T2> const&);
};

Ecco a seguire l'implementazione del metodo:

template <typename T>
template <typename T2>
Stack<T>& Stack<T>::operator= (Stack<T2> const& op2)
{
if ((void*)this == (void*)&op2) {
return *this;
}

Stack<T2> tmp(op2); // create a copy of the assigned stack

elems.clear();
while (!tmp.empty()) { // copy all elements
elems.push_front(tmp.top());
tmp.pop();
}
return *this;
}

In questo modo é possibile assegnare uno stack di interi a uno di float.

Nel quarto paragrafo ci si spiega come creare un template con parametri templatizzati. Utile se vogliamo creare, ad esempio, uno stack di cui specifichiamo sia il tipo dei dati trattati che il tipo del container utilizzato per memorizzarli, una cosa così:

Stack<int,std::vector<int> > vStack;

Notevole il quinto paragrafo, dove ci viene spiegato che, in un certo senso, é possibile chiamare una sorta di costruttore di default per i tipi di base. Ovvero, questo codice:

template <typename T>
void foo()
{
T x = T();
}

compila correttamente anche nel caso che T sia un tipo built-in, come ad esempio un int. Stupefacente, sensato, e molto utile. Il valore che viene assegnato é, molto propriamente, zero.

Il sesto paragrafo illustra un problema che, prima o poi capita a tutti: il fatto che l'uso di stringhe letterali risulta un po' ostico in connessione con i template.

Il problema é che una stringa come "apple" ha tipo char const[6], mentre "tomato" ha tipo char const[7] e non, come ci si potrebbe aspettare, lo stesso tipo.

Il paragrafo entra nei dettagli di questo problema fornendo alcune possibili soluzioni.

Nontipi come parametri di template

Continuo la lettura di C++ Templates: The Complete Guide, affrontando il terzo capitolo, che tratta i parametri di template che non sono tipi.

Abbiamo visto nei capitoli precedenti come specificare i tipi che parametrizzano il nostro template, sia esso per funzioni o per classi. Qui vediamo come parametrizzare la nostra template usando non un tipo ma un valore.

Curiosamente, per mostrare l'utilizzo di questa feature relativamente avanzata, si fa ricorso ad un esempio che non coinvolge l'uso di un container STL, cosa che é stata fatta per la prima definizione di una classe template.

In pratica il nostro esempio é l'implementazione di uno stack che si appoggia su un array C-style, dove però la dimensione dell'array viene parametrizzata con un parametro, un intero, deciso dall'utente.

Ecco qui a seguire la definizione della classe:

#include <stdexcept>
#include <iostream>

using namespace std;

template <typename T, int MAXSIZE>
class Stack {
private:
T elements[MAXSIZE];
int numElems; // current number of elements
public:
Stack() : numElems(0) {}
void push(const T&);
void pop();
T top() const;
bool empty() const {
return numElems == 0;
}
bool full() const {
return numElems == MAXSIZE;
}
};

template <typename T, int MAXSIZE>
void Stack<T,MAXSIZE>::push (T const& elem)
{
if (numElems == MAXSIZE) {
throw out_of_range("Stack<>::push(): stack is full");
}
elements[numElems++] = elem;
}

template<typename T, int MAXSIZE>
void Stack<T,MAXSIZE>::pop ()
{
if (numElems <= 0) {
throw out_of_range("Stack<>::pop(): empty stack");
}
--numElems; // decrement number of elements
}

template <typename T, int MAXSIZE>
T Stack<T,MAXSIZE>::top () const
{
if (numElems <= 0) {
throw out_of_range("Stack<>::top(): empty stack");
}
return elements[numElems-1]; // return last element
}

Notiamo che il template ora dipende dal tipo T e dall'intero MAXSIZE, questo viene usato per determinare la dimensione dell'array sottostante e per effettuare controlli sull'accesso ai dati dello stack.

Il codice per verificare il funzionamento della classe é il seguente:

#include <iostream>
using namespace std;
//...

int main() {
try {
Stack<int,20> int20Stack; // stack of up to 20 ints
int20Stack.push(7);
cout << int20Stack.top() << endl;
int20Stack.pop();

Stack<string,40> stringStack; // stack of up to 40 strings
stringStack.push("hello");
cout << stringStack.top() << endl;
stringStack.pop();
stringStack.pop();
}
catch (exception const& ex) {
cerr << "Exception: " << ex.what() << endl;
return EXIT_FAILURE; // exit program with ERROR status
}
}

Notiamo come nell'instanziazione della nostra template specifichiamo come secondo parametro un intero, che sarà la dimensione massima del nostro stack.

Il resto del capitolo é dedicato all'approfondimento del tema, con dettagli sull'uso di valori di default e restrizioni sul tipo di valori che si possono passare.

Quanti giorni tra due date: difftime

Vogliamo calcolare la differenza in giorni tra due dati istanti.

Per istante intendiamo il numero di secondi passati dalla mezzanotte del primo gennaio 1970 UTC (l'inizio del tempo UNIX), un valore integrale che viene gestito da variabili di tipo time_t.

Come spesso accade, risolviamo il nostro problema riducendolo a uno più semplice, per poi adattare il risultato ai nostri bisogni.

Calcoliamo quindi i secondi di differenza tra due tempi usando la funzione difftime, definita in ctime con questo prototipo:

double difftime(time_t t2, time_t t1);

dove t2 é il tempo più lontano dall'inizio del tempo. Se si mette in t2 il primo tra i due tempi si otterrà un risultato negativo.

Dividiamo poi il risultato per il numero di secondi in un giorno, 86400, ottenendo il risultato atteso:

double days = difftime(end, start) / 86400;

Notiamo che il numero di giorni é espesso come numero decimale, dato che si fa una differenza al secondo, quindi si ottiene una risposta che potrebbe essere del tipo "circa otto giorni e mezzo".

Per passare da una data in formato leggibile a un umano a una nel formato accettato da difftime usiamo la funzione mktime(), anch'essa definita in ctime:

time_t mktime(tm* pTime);

che non fa altro che convertire il contenuto di un oggetto di tipo tm nel numero di secondi passati dall'inizio del tempo UNIX. In caso di errore la funzione ritorna -1.

La creazione di un oggetto di tipo tm, una struttura definita in ctime, sarebbe banale, se non ci fosse da tener presente alcune sottigliezze.

Bisogna infatti specificare l'attimo definendo tutte le sue componenti in un modo che non é sempre naturale.
Secondi, minuti e ore non danno problemi - anche se vanno indicati come li ho scritti, non partendo dalle ore, come sembrerebbe spontaneo.
Segue poi il giorno del mese, nessun problema.
Poi il mese, partendo da Gennaio che vale 0 fino a dicembre che é 11.
E infine l'anno a partire dal 1900 (ovvero, per il 2000 occorre mettere 100).

Dunque, per creare un oggetto tm corrispondente alla mezzanotte del 2 ottobre 2000 dobbiamo scrivere:

tm tmStart = {0, 0, 0, 2, 9, 100};

Per testare la funzionalità scriviamo questa funzioncina:

#include
#include

using namespace std;
// ...
void testDiff() {
tm tmStart = {0, 0, 0, 2, 9, 100};
time_t start = mktime(&tmStart);
if(start == -1) {
cout << "Bad start date." << endl;
return;
}

tm tmEnd = {0, 0, 0, 3, 7, 109};
time_t end = mktime(&tmEnd);
if(end == -1) {
cout << "Bad end date." << endl;
return;
}

double days = difftime(end, start) / 86400;
cout << "difference = " << days << " days" << endl;
return;
}

Template per classi

Nel capitolo tre di C++ Templates: The Complete Guide si affrontano i template per classi. L'esempio che si fa é, anche qui, un classico, uno stack generico.

Un po' discutibile la scelta di usare come struttura dati sottostante un vector STL, dato che in questo modo si finisce per usare una classe template per spiegare come usare i template per classi. Insomma, si cade nella ricorsione.

Il vantaggio é che si semplifica molto il codice, rendendo forse più facile la comprensione al principiante - a patto che si fidi e non si faccia troppe domande sulla classe vector.

Anche questo capitolo mi sembra ben fatto, dando una buona introduzione alle funzionalità di base e a quelle meno comuni ma che é buona cosa sapere che ci sono e come usarle, come la specializzazione del template per uno specifico tipo e l'uso di argomenti di default nella definizione di un template.

Qui riporto solo il codice per un esempio di base, con la definizione della classe generica Stack:

#include <vector>
#include <stdexcept>
#include <iostream>

using namespace std;

template<class T>
class Stack {
private:
vector<T> elements;

public:
void push(T const& element) {
// append copy of passed element
elements.push_back(element);
}
void pop();
T top() const;
bool empty() const {
return elements.empty();
}
};

template<class T>
void Stack<T>::pop()
{
if (elements.empty()) {
throw out_of_range("Stack<>::pop(): empty stack");
}
elements.pop_back();
}

template<class T>
T Stack<T>::top() const
{
if (elements.empty()) {
throw out_of_range("Stack<>::top(): empty stack");
}
return elements.back();
}

Notiamo come pop() e top() possano tirare eccezioni nel caso si cerchi di chiamarle su uno stack vuoto. Il codice che usa questa classe, perciò, dovrà tenere conto di questa possibilità:

try {
Stack<string> stringStack;

stringStack.push("hello");
cout << stringStack.top() << endl;
stringStack.pop();
stringStack.pop();
}
catch (const exception& ex) {
cerr << "Exception: " << ex.what() << endl;
}

Template per funzioni

Cominciata la lettura di C++ Templates: The Complete Guide, di cui si parlava nel post precedente.

Letto il capitolo introduttivo, in cui gli autori giustificano le loro convenzioni di scrittura del codice che dovremmo accettare in tutto il libro (e che a me sembrano un poco innaturali), e il capitolo due, che tratta i template per le funzioni.

Non male, il capitolo, spiegano le cose piuttosto bene, e scendendo in dettagli che in genere sono tralasciabili, ma comunque é bello sapere dove si può andare a vedere quando venga la necessità di rivederseli.

Qui riporto un piccolo esempio di base per una funzione templatizzata, la solita max(), giusto come riferimento.

Tipicamente definiamo un template per una funzione in questo modo:

template <typename T>
inline const T& myMax (const T& a, const T& b)
{
return a < b ? b : a;
}

In questo caso la funzione é inline, data la sua semplicità, e per mantenere le prestazioni di quella che é la controparte C, che é implementata come macro.

Il vantaggio del template C++ é che é type-safe e a prova di conflitti con le precedenze degli operatori (rimando al libro che sto leggendo per i dettagli).

Per vedere testare il codice ho scritto queste righe di codice:

int three = 3;
cout << "myMax<int> is " << myMax(three, 8) << endl;

string one("one");
string two("two");
cout << "myMax<string> is " << myMax(one, two) << endl;

In cui la mia funzione parametrizzata viene testata per interi e stringhe.

Che dire. Funziona.

C++ Templates: The Complete Guide

Sento il bisogno di rinfrescare le mie conoscenze di base sulle template, mi sono così preso questo libro che mi pare prometta bene.

Trattasi di:
C++ Templates: The Complete Guide
di David Vandevoorde, Nicolai M. Josuttis
Addison-Wesley Professional 2003

Un utile link per chi fosse interessato al libro é questo. Si tratta di alcune pagine web che lo Josuttis gli ha dedicato, includendo anche riferimenti al codice sorgente per gli esempi affrontati nel libro.

Un altro link da considerare é quest'altro, che punta alla home del libro nel sito dell'altro autore, Vandevoorde

A quanto ho visto da una rapida scorsa, non si tratta di un manuale sulla Standard Template Library ma piuttosto una guida all'uso del concetto di template in C++.

Dato che é proprio questo che mi interessa al momento, direi che questo tomo é quello che fa per me. Almeno credo.

Seguiranno dettagli nei prossimi post.

POSIX Mutex

Nel post precedente abbiamo visto come gestire diversi thread in una applicazione scritta in C++. Ora vediamo come gestire i problemi di concorrenza, sempre nell'ambito delle specifiche POSIX.

Il concetto di semaforo viene implementato con il Mutex (che sta per mutual exclusion, mutua esclusione).

Dovrebbe servire a spiegare il concetto questo esempio.

Scriviamo una piccola applicazione in cui una classe somma dei dati contenuti al suo interno dividendo il lavoro su quattro thread. Il punto da tenere sotto controllo é dove viene letto e incrementato il totale corrente, che risulta essere la risorsa condivisa tra i diversi thread.

Infatti si regolamentasse il suo accesso con un mutex, potrebbe succedere che un thread legga il suo valore dopo di un altro ma scriva il suo risultato aggiornato prima. Di conseguenza il suo lavoro andrebbe perso.

Vediamo il codice:
#include <errno.h>
#include <iostream>
#include <sstream>

using namespace std;

class HugeWork {
public:
HugeWork(int res, int a, int b, int c, int d) : m_result(res) {
m_values[0] = a;
m_values[1] = b;
m_values[2] = c;
m_values[3] = d;

pthread_mutex_init(&m_mutex, 0);
}

~HugeWork() {
pthread_mutex_destroy(&m_mutex);
}

double getResult();
private:
static void* increase(void*);

int m_result;
int m_values[4];

pthread_t m_thread_id[4];
pthread_mutex_t m_mutex;
};

double HugeWork::getResult() {
ostringstream os;
for (int i = 0; i < 4; ++i) {
if (pthread_create(&m_thread_id[i], NULL, &increase, this) < 0) {
cout << "Error creating thread " << i << ": " << errno << endl;
return 0;
}

os << "Thread " << m_thread_id[i] << " created (" << i << ")" << endl;
cout << os.str();
os.str("");
}

// wait for all the threads to complete
for(int i = 0; i < 4; ++i) {
os << "Joining thread " << m_thread_id[i] << endl;
cout << os.str();
os.str("");

pthread_join(m_thread_id[i], 0);
}

return m_result;
};

// notice: should be static for pthread requirements
void* HugeWork::increase(void* ptr) {
HugeWork* hw = static_cast<HugeWork*> (ptr);


sleep(1);

int result = 0;
for (int i = 0; i < 4; ++i) {
result += hw->m_values[i];
}

pthread_mutex_lock(&hw->m_mutex);
cout << "increasing " << result << endl;
hw->m_result += result;
pthread_mutex_unlock(&hw->m_mutex);

return 0;
}

int main(int argc, char** argv) {
HugeWork hw(1, 1, 2, 3, 4);

cout << "Final result: " << hw.getResult() << endl;
}

POSIX Threads

Una semplice applicazione C++ per illustrare le basi della programmazione multithreading secondo le specifiche POSIX.

Molti dettagli sono dipendenti dall'implementazione, in questo caso lo sviluppo é stato fatto su Cygwin.

Disegnamo una classe, CountDown, che permette di fare il conto alla rovescia.

Dal main vediamo come viene usata, si istanzia un oggetto della classe e si chiama su di esso il metodo run():

CountDown cd1("two x two", 2, 2);
// ...
cd1.run();

Notiamo che tra i membri della classe abbiamo messo un oggetto di tipo pthread_t che rappresenta l'identificatore del thread in cui viene eseguita:

pthread_t m_thread_id;

Il metodo run() crea un nuovo thread salvando nella variabile membro m_thread_id l'identificatore del thread creato e passando l'indirizzo della metodo privato go() che cura i dettagli implementativi della classe.

pthread_create(&m_thread_id, NULL, &go, this)

Il metodo go() é dichiarato static per ragioni di compatibilità con pthread, che é una libreria C e quindi nulla sa di classi e oggetti.

La prima cosa che facciamo in go() é rendere disponibile l'accesso alla classe su cui stiamo lavorando. Il "trucco" a cui abbiamo fatto ricorso é quello di passare this dalla run() via pthread_create come puntatore a void, e qui farlo tornare un puntatore a CountDown con un (piuttosto brutale) static_cast.

CountDown* cd = static_cast(ptr);

E quindi viene eseguito in count down usando i settaggi per l'oggetto corrente. Notiamo l'uso della funzione sleep() per scandire il tempo:

sleep(cd->m_sleep);

Per evitare che un oggetto venga distrutto quando il suo thread é ancora in corso di esecuzione, facciamo una join (ovvero, attendiamo la terminazione del thread) nel distruttore:

pthread_join(m_thread_id, 0);

E questo é, più o meno, tutto. A seguire il codice completo:

#include <string>
#include <iostream>
#include <unistd.h>
#include <errno.h>
#include <sstream>

using namespace std;

class CountDown {
public:
CountDown(const char* id, int start, int sleep =1) :
m_id(id), m_start(start), m_sleep(sleep), m_thread_id(0) {}

~CountDown();

void run();

private:
static void* go(void*);

string m_id;
int m_start;
int m_sleep;
pthread_t m_thread_id;
};

// ensure the object is destroyed only when the thread is terminated
CountDown::~CountDown() {
pthread_join(m_thread_id, 0);
cout << "Dtor completed on " << m_id << endl;
}

// notice: should be static for pthread requirements
void* CountDown::go(void* ptr) {
CountDown* cd = static_cast<CountDown*>(ptr);

ostringstream os;
os << "running " << cd->m_id << endl;
cout << os.str();

for(int i = cd->m_start; i > 0; --i) {
os.str("");
os << cd->m_id << " " << i << endl;
cout << os.str();
sleep(cd->m_sleep);
}

cout << "count down done for " << cd->m_id << endl;
return 0;
}

void CountDown::run() {
if(pthread_create(&m_thread_id, NULL, &go, this) < 0) {
cout << "Error creating thread " << errno << endl;
return;
}

ostringstream os;
os << "Thread id " << m_thread_id << " created (" << m_id << ")" << endl;
cout << os.str();
}

/*
*
*/
int main(int argc, char** argv) {
CountDown cd1("two x two", 2, 2);
CountDown cd2("three", 3);
CountDown cd3("four", 4);

cd1.run();
cd2.run();
cd3.run();

ostringstream os;
os << "All threads are running" << endl;
cout << os.str();
}

Anatre in formazione

Completiamo il discorso sul pattern Strategy secondo quanto si trova nel primo capitolo di Head First - Design Patterns, descrivendo la gerarchia di anatre e poi le gerarchie di comportamenti associati.

La classe base delle anatre é Duck, una classe astratta che ha due dati membro che sono poi le due strategie dell'anatra: il modo di volare e il modo di emettere un verso.

Notiamo che, per essere utilizzabile, un anatra reale dovrà definire le due strategie, in modo che possano essere coerentemente invocati i metodi fly() e quack() su di essa. Il metodo swim é comune a tutte le implementazioni, e quindi viene definito in Duck.

package hf.strategy;

public abstract class Duck {
protected FlyBehavior flyBehavior;
protected QuackBehavior quackBehavior;

public Duck() {
}

public void setFlyBehavior (FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}

public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior = quackBehavior;
}

public void fly() {
flyBehavior.fly();
}

public void quack() {
quackBehavior.quack();
}

public void swim() {
System.out.println("All ducks float, even decoys!");
}
}


La definizione di un'anatra reale richiederà la specificazione delle strategie adottate. Vediamo ad esempio la classe MallardDuck:


package hf.strategy;

public class MallardDuck extends Duck {
public MallardDuck() {
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}
}

Strategie

Restano ora da definire le strategie. Al top della gerarchia di una strategia ci sarà un'interfaccia, che dichiara il metodo che viene richiesto. Qui vediamo la strategia di volo:

package hf.strategy;

public interface FlyBehavior {
public void fly();
}

E qui la strategia di starnazzamento:

package hf.strategy;

public interface QuackBehavior {
public void quack();
}


Una classe reale utilizzerà una implementazione coerente con i suoi scopi delle strategie richieste. MallardDuck richiedeva queste:

package hf.strategy;

public class Quack implements QuackBehavior {
public void quack() {
System.out.println("Quack");
}
}

// ---
package hf.strategy;

public class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("I'm flying!!");
}
}

La strategia dell'anatra

Nel primo capitolo di Head First - Design Patterns si tratta il pattern Strategy a partire dalla progettazione di una gerarchia di classi che descrivono il comportamento di diversi tipi di anatre.

Il problema é quello di dare alla gerarchia una robustezza tale da permetterle di reggere agevolemente a cambiamenti futuri delle specifiche di progetto.

Il pattern Strategy definisce una famiglia di algoritmi, li incapsula, e li rende intercambiabili. Strategy fa un modo che l'algoritmo cambi indipendentemente dal client che lo usa.

Nel corso del capitolo vengono citati tre importanti principi, che riporto qui a seguire.

Il fatto che di costante nello sviluppo software sia solo il cambiamento conduce al seguente principio di progettazione: identifica gli aspetti dell'applicazione che cambiano e separali da quelli che non mutano.

Un altro importante principio: programma verso un interfaccia e non verso una implementazione. Questo permette una maggior astrazione del codice e quindi un miglior adattamento del codice al cambiamento.

Un terzo principio: preferisci la composizione all'ereditarietà. Permette molta più flessibilità, dato che permette di cambiare il comportamento di una classe a runtime.

Detto questo, vediamo le specifiche del nostro progetto.

Vogliamo gestire una gerarchia di anatre, in modo da poter gestire classi che rappresentano sia animali veri e propri che oggetti come un anatra di gomma o un anatra da richiamo.

L'idea é che alcuni algoritmi che potrebbero sembrare scontati nella nostra gerarchia (il volare dell'anatra, nel nostro esempio) in realtà non lo sono.

Nell'implementazione originale, ogni nostra anatra poteva volare, emettere un verso (quack!) e nuotare. Creando nuove classi derivate, abbiamo cozzato contro una variazione della progettazione originale: esistono anatre che non emettono versi e anatre che non volano.

Abbiamo quindi rifattorizzato la classe di base per riflettere questo cambiamento: l'anatra generica si appoggia su classi esterne che implementano la strategia della specifica anatra.

Avremo quindi una gerarchia di classi basate in Duck, la classe astratta da cui tutte le nostre anatre derivano, e una gerarchia per ogni strategia che Duck utilizza.

Nel nostro caso abbiamo una interfaccia FlyBehavior per la strategia di volo e QuackBehavior per la strategia di starnazzamento.

Nei post seguenti vedremo il codice per implementare questa soluzione.