Pages

Tuesday, January 17, 2012

Test Driven Development, C++ e Qt: refactoring, ottimizzazioni, d-pointer

Nel precedente post abbiamo dato una messa a punto ai file del progetto. Potrebbe essere il caso di fare qualcosina anche per il codice sorgente...
In questa puntata vedremo alcuni passi di refactoring che ci aiuteranno ad implementare funzionalità future, e renderanno il codice più robusto. Vedremo ad esempio come applicare il meccanismo di Signal e Slot di Qt, particolarmente indicato per la programmazione "ad eventi", e in cosa consiste il pattern d-pointer che in alcuni ambienti (ad esempio nella programmazione per KDE) è divenuto uno standard praticamente obbligatorio

Andando per gradi, una prima facile osservazione è che quella di stampare l'help message è una funzionalità piuttosto marginale dell'applicazione finale, lo sappiamo già... quindi perchè non estrarre un metodo apposito? In questo modo, quando aggiungeremo il controllo del numero di argomenti, tutto ciò che dovremo fare sarà invocare il metodo printHelpMessage nel caso in cui avremo zero argomenti, e negli altri casi proseguire col flusso normale.


void DownloadManagerCore::start(const QStringList &arguments)
{
    printHelpMessage();
}

void DownloadManagerCore::printHelpMessage()
{
    *output << "Usage: downloadmanager url1 [url2... urlN]" << endl;
    *output << "Downloads the URLs passed in the command-line to the local directory.";
    *output << " If the target file already exists, a .0, .1, .2, etc. is appended to differentiate." << endl;
}



Naturalmente verifichiamo tramite i nostri unit test che la modifica non abbia rotto il codice.
Un'altra facile osservazione è che il "vero" progetto, DownloadManager, in questo momento non fa proprio nulla, neanche istanziare la classe DownloadManagerCore.
In questo caso, basta prendere spunto dal main dell'esempio di Qt che abbiamo visto nella prima puntata.
Per farlo però notiamo che ci serve connettere QCoreApplication con un segnale proveniente dalla nostra classe principale.
In Qt segnali e slot sono un modo per comunicare degli eventi, molto flessibile e intuitivo.
In questo caso, ad esempio, la classe DownloadManagerCore emetterà il segnale finished al termine delle sue operazioni, e lo connetteremo allo slot quit() di QCoreApplication. Che tradotto, vuol dire che faremo terminare l'applicazione quando lo deciderà la nostra classe.
Cominciamo quindi ad aggiungere il segnale allo header di DownloadManagerCore.

signals:
    void finished();

Adesso modifichiamo di conseguenza main.cpp di DownloadManager.

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    QTextStream output(stdout);

    DownloadManagerCore core(&output);

    QObject::connect(&core, SIGNAL(finished()), &app, SLOT(quit()));

    QStringList arguments = app.arguments();

    arguments.removeFirst();

    core.start(arguments);

    return app.exec();

}

Dato che nella scorsa puntata abbiamo introdotto nel costruttore di DownloadManagerCore un textStream, dobbiamo qui istanziarlo per poterlo usare. In effetti il compito principale è proprio di istanziare le classi giuste, e passare a queste il comando. Nient'altro.
È interessante notare che quindi probabilmente non avremo più alcun bisogno di modificare il main, dato che deleghiamo tutte le funzonalità alla libreria. Al più ci limiteremo a creare nuovi oggetti e passarli a DownloadManagerCore. Se però proviamo a compilare e lanciare DownloadManager, ci accorgiamo che l'applicazione stampa correttamente il messaggio di errore, ma non esce! Questo semplicemente perchè non abbiamo ancora emesso il segnale finished(), e QCoreApplication lo sta aspettando per terminare.
Possiamo testare che questo segnale venga emesso? La risposta è si, basta usare QSignalSpy.
Aggiungiamo quindi un nuovo test a DownloadManagerCoreTest:


void DownloadManagerCoreTest::itShouldEmitFinishedSignalWhenCalledWithNoArguments()
{
    QString output;
    QTextStream outputStream(&output);
    DownloadManagerCore *lib = new DownloadManagerCore(&outputStream, this);
    QSignalSpy signalSpy(lib, SIGNAL(finished()));
    QVERIFY2(signalSpy.isValid(), "We want to verify we're listening to an existing signal...");
    lib->start(QStringList());
    QCOMPARE(signalSpy.size(), 1);
}


In questo test verifichiamo che, sempre con zero argomenti, viene emesso il segnale finished dopo aver effettuato la chiamata start. Naturalmente il test non passa, ma per farlo diventare verde è sufficiente aggiungere, alla fine del metodo start, la riga

    emit finished();

Anche adesso però l'applicazione rimane appesa: questo perchè nel main il metodo start viene invocato ancora prima che l'event loop (che processa i messaggi, compresi i segnali) venga avviato dalla chiamata app.exec(). E quindi?
Basta rifattorizzare ancora un po' il nostro codice, in questo modo:

  • aggiungere uno slot privato a DownloadManagerCore, chiamandolo startDownloads, senza parametri in ingresso
  • spostare tutto quello che si trova nel metodo start nello slot appena creato
  • memorizzare la lista degli argomenti ricevuta da start in un field privato
  • creare un timer, con tempo di attesa 0: quando l'event loop verrà avviato, il timer scatterà, e connettendo il timer allo slot startDownloads, verrà chiamato il codice ad esso associato (cioè help message e uscita dell'applicazione.
void DownloadManagerCore::start(const QStringList &arguments)
{
    this->arguments = arguments;
    QTimer::singleShot(0, this, SLOT(startDownloads()));
}

void DownloadManagerCore::printHelpMessage()
{
    *output << "Usage: downloadmanager url1 [url2... urlN]" << endl;
    *output << "Downloads the URLs passed in the command-line to the local directory.";
    *output << " If the target file already exists, a .0, .1, .2, etc. is appended to differentiate." << endl;
}

void DownloadManagerCore::startDownloads()
{
    printHelpMessage();
    emit finished();
}


Finalmente funziona tutto come dovrebbe.
Ma guardiamo DownloadManagerCore: abbiamo dovuto aggiungere due metodi privati, e un field privato che prima non era necessario. Tutte queste informazioni inquinano lo header: è buona pratica mantenere infatti le intestazioni il più pulite possibile, e tra gli elementi indesiderati ci sono proprio i metodi e field privati.
Questo per diverse ragioni, una è che ogni aggiunta ad uno header costringe a ricompilare tutti gli utilizzatori, inoltre i nuovi metodi e field privati rompono la compatibilità binaria nel caso si stia sviluppando una libreria.
Inoltre gli header del progetto sono un po' come l'indice di un libro: ci dicono sommariamente cosa fa la classe, con chi parla, di cosa ha bisogno. I dettagli privati non devono stare nell'indice!
Esiste una tecnica, chiamata "cheshire cat", o "pimpl", che permette di spostare questi "oggetti indesiderati" nei file contenenti l'implementazione della classe (i .cpp). In Qt questa tecnica viene meglio specializzata e chiamata d-pointer, maggiori informazioni in questo link. Si tratta di dichiarare una classe privata, la cui definizione però viene spostata nel file .cpp, mentre nella classe pubblica rimarrà un solo field privato, un puntatore alla classe privata, che conterrà tutti i metodi e le variabili che ci servono.
Qt offre anche delle macro molto utili, basate su due convenzioni:

  • La classe privata deve chiamarsi NomeDellaClassePubblicaPrivate.
  • Il puntatore alla classe privata deve chiamarsi d_ptr.

Le macro utili che Qt ci offre sono:

  • Q_DECLARE_PRIVATE: genera i metodi accessor a d_ptr (servono due metodi, uno per accedere da metodi normali, l'altro per i metodi const). Si deve mettere nello header della classe pubblica, nella sezione private, subito dopo la dichiarazione di d_ptr.
  • Q_D: usa gli accessor generati dalla macro precedente per memorizzare in una variabile locale, chiamata d, il puntatore alla classe privata. È da utilizzare nei metodi in cui vogliamo accedere ai dati privati della classe.
Ci sono anche altre macro, utili ad esempio per accedere alla classe pubblica dalla classe privata, ma ce ne occuperemo più avanti, quando serviranno.


Continuando con i refactoring, ci accorgiamo di un'altra cosa, e cioè che lo slot privato startDownloads potrebbe risultare inutile*: basterebbe spostare la lista degli argomenti in costruzione, far diventare start uno slot pubblico, e lasciare al main.cpp di DownloadManager il compito di usare il timer per chiamare questo slot
Dobbiamo quindi:

  • dichiarare una classe DownloadManagerCorePrivate nel file di intestazione, senza però specificarne le caratteristiche.
  • aggiungere un field puntatore a DownloadManagerCorePrivate di nome d_ptr, e la macro Q_DECLARE_PRIVATE(DownloadManagerCore), che genererà gli accessor a d_ptr
  • Dichiarare nel file .cpp la classe DownloadManagerCorePrivate, spostandiovi ciò che prima era privato in DownloadManagerCore
  • spostare la logica del timer nel main.cpp
  • cancellare startDownloads, e trasformare come descritto sopra il metodo start.
  • creare un distruttore per DownloadManagerCore, dove cancellare il d-pointer, che non essendo un QObject, rimarrebbe lì a generare memory leak se non venisse cancellato manualmente
Ecco il risultato finale:

  • main.cpp
int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    QTextStream output(stdout);
    QStringList arguments = app.arguments();
    arguments.removeFirst();

    DownloadManagerCore core(&output, arguments);
    QObject::connect(&core, SIGNAL(finished()), &app, SLOT(quit()));

    QTimer::singleShot(0, &core, SLOT(start()));
    return app.exec();
}

  • downloadmanagercore.h
class QTextStream;
class DownloadManagerCorePrivate;
class DownloadManagerCore : public QObject {
    Q_OBJECT
public:
    DownloadManagerCore(QTextStream *output, const QStringList &arguments, QObject *parent = 0);
    ~DownloadManagerCore();


public slots:
    void start();

private:
    DownloadManagerCorePrivate * const d_ptr;
    Q_DECLARE_PRIVATE(DownloadManagerCore)

signals:
    void finished();
};
  • downloadmanagercore.cpp
class DownloadManagerCorePrivate {
public:
    DownloadManagerCorePrivate(QTextStream *output, const QStringList &arguments);
    QTextStream *output;
    QStringList arguments;
    void printHelpMessage();
};

DownloadManagerCore::DownloadManagerCore(QTextStream *output, const QStringList &arguments, QObject *parent)
    : QObject(parent), d_ptr(new DownloadManagerCorePrivate(output, arguments))
{
}


DownloadManagerCore::~DownloadManagerCore()
{
    delete d_ptr;
}


void DownloadManagerCore::start()
{
    Q_D(DownloadManagerCore);
    d->printHelpMessage();
    emit finished();
}

DownloadManagerCorePrivate::DownloadManagerCorePrivate(QTextStream *output, const QStringList &arguments)
    : output(output), arguments(arguments)
{
}

void DownloadManagerCorePrivate::printHelpMessage()
{
    *output << "Usage: downloadmanager url1 [url2... urlN]" << endl;
    *output << "Downloads the URLs passed in the command-line to the local directory.";
    *output << " If the target file already exists, a .0, .1, .2, etc. is appended to differentiate." << endl;
}




Naturalmente dovremo sistemare anche i test, ma è facile: basta spostare l'argomento QStringList dal metodo start al costruttore, ed il gioco è fatto... barra verde su tutto!
Nella prossima puntata cominceremo ad implementare qualche funzionalità in più, sempre lasciandoci guidare dai test, e come al solito, qui potete trovare i sorgenti per questa puntata.


* il termine "slot privato" in effetti è quasi un controsenso: un metodo privato è invisibile a tutti, tranne che alla propria classe; uno slot invece, qualunque sia la visibilità impostata, è accessibile pubblicamente tramite connect. Il risultato quindi è un metodo che è privato invocato normalmente, e pubblico invocato come slot.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.