2009-08-13 13 views
7

Nel tipo di programmazione incorporata, il determinismo e la trasparenza del codice di esecuzione sono molto apprezzati. Ciò che intendo per trasparenza è, ad esempio, essere in grado di guardare sezioni arbitrarie della memoria e sapere quale variabile è memorizzata lì. Quindi, dato che sono sicuro che i programmatori incorporati si aspettano, il nuovo deve essere evitato, se possibile, e se non può essere evitato, quindi limitato all'inizializzazione.Come evitare le variabili globali nella programmazione integrata

Capisco la necessità di questo, ma non sono d'accordo con il modo in cui i miei colleghi hanno fatto questo, né conosco un'alternativa migliore.

Quello che abbiamo sono diversi array globali di strutture e alcune classi globali. C'è una matrice di strutture per mutex, una per i semafori e una per le code di messaggi (queste sono inizializzate in main). Per ogni thread che viene eseguito, la classe che lo possiede è una variabile globale.

Il problema più grande che ho con questo è nei test unitari. Come posso inserire un oggetto fittizio quando la classe che voglio testare le variabili globali #include s non lo faccio?

Ecco la situazione in pseudo-codice:

foo.h

#include "Task.h" 
class Foo : Task { 
public: 
    Foo(int n); 
    ~Foo(); 
    doStuff(); 
private: 
    // copy and assignment operators here 
} 

bar.h

#include <pthread.h> 
#include "Task.h" 

enum threadIndex { THREAD1 THREAD2 NUM_THREADS }; 
struct tThreadConfig { 
    char  *name, 
    Task  *taskptr, 
    pthread_t threadId, 
    ... 
}; 
void startTasks(); 

bar.cpp

#include "Foo.h" 

Foo foo1(42); 
Foo foo2(1337); 
Task task(7331); 

tThreadConfig threadConfig[NUM_THREADS] = { 
    { "Foo 1", &foo1, 0, ... }, 
    { "Foo 2", &foo2, 0, ... }, 
    { "Task", &task, 0, ... } 
}; 

void FSW_taskStart() { 
    for (int i = 0; i < NUMBER_OF_TASKS; i++) { 
     threadConfig[i].taskptr->createThread(); 
    } 
} 

Cosa succede se voglio più o meno attività? Un diverso insieme di argomenti nel costruttore di foo1? Penso che dovrei avere un bar.h e un bar.cpp separati, il che sembra molto più lavoro del necessario.

+0

suppongo che voi dire '& foo1' e non '% foo1' (l'operatore modulo)? – DaveR

+0

Grazie. Questo è quello che ottengo per riscrivere invece di copiare/incollare –

risposta

4

Se si desidera testare tale codice in primo luogo, si consiglia di leggere Working Effectively With Legacy Code Vedere anche this.

In pratica l'uso del linker per inserire oggetti finti/falsi e le funzioni dovrebbe essere l'ultima risorsa, ma è ancora perfettamente valido.

Tuttavia è possibile utilizzare anche l'inversione del controllo, senza un framework questo può spingere alcune responsabilità al codice client. Ma aiuta davvero i test. Per esempio per testare FSW_taskStart()

tThreadConfig threadConfig[NUM_THREADS] = { 
    { "Foo 1", %foo1, 0, ... }, 
    { "Foo 2", %foo2, 0, ... }, 
    { "Task", %task, 0, ... } 
}; 

void FSW_taskStart(tThreadConfig configs[], size_t len) { 
    for (int i = 0; i < len; i++) { 
     configs[i].taskptr->createThread(); 
    } 
} 

void FSW_taskStart() { 
    FSW_taskStart(tThreadConfig, NUM_THREADS); 
} 

void testFSW_taskStart() { 
    MockTask foo1, foo2, foo3; 
    tThreadConfig mocks[3] = { 
      { "Foo 1", &foo1, 0, ... }, 
      { "Foo 2", &foo2, 0, ... }, 
      { "Task", &foo3, 0, ... } 
     }; 
    FSW_taskStart(mocks, 3); 
    assert(foo1.started); 
    assert(foo2.started); 
    assert(foo3.started); 
} 

Ora è possibile possibile possibile passare la versione finta di sei Discussioni su 'FSW_taskStart' per garantire che la funzione non di fatto avvia i fili come richiesto. Sfortunatamente devi fare affidamento sul fatto che l'originale FSW_taskStart passa gli argomenti corretti, ma ora stai testando molto più del tuo codice.

+0

Beh, sono abbastanza fortunato da non essere ancora un codice legacy, ed è per questo che voglio ottenere le cose al momento. Ho già pensato a IOC, ma vedere un esempio mi ha aiutato a credere che sia possibile. In questo modo, il codice di produzione può utilizzare le variabili globali, in modo da poter prevedere dove saranno in memoria, ma il codice di prova non può semplicemente includere bar.cpp. Non sono ancora sicuro però. Nel modo in cui hai proposto, non avrò bisogno di modificare alcun file .h, semplicemente includo diversi file .cpp nel progetto del test unitario? –

+1

+1. DIP e IOC sono un buon modello di architettura, in particolare per .. test con oggetto mock. Se non è possibile modificare la firma delle funzioni, è possibile farlo con qualche riferimento indiretto. Invece di passare il tuo oggetto contesto puoi chiamare una funzione che lo restituisce. Questa funzione può utilizzare IOC e restituire oggetti di contesto reali o oggetto fittizio a seconda dell'inizializzazione ... – neuro

+1

@drhorrible, il nome del libro potrebbe essere un po 'fuorviante ;-) La sua definizione di codice legacy è codice senza test di unità. È principalmente la raccolta di tecniche per il collaudo di unità in diversi scenari difficili. – iain

-1

è possibile allocare memoria utilizzando malloc e quindi ottenere il nuovo operatore per rendere l'oggetto in quella posizione

void* mem = malloc(3*sizeof(SomeClass)); 
SomeClass *a = new(mem) SomeClass(); 
mem += sizeof(SomeClass); 
SomeClass *b = new(mem) SomeClass(); 
mem += sizeof(SomeClass); 
SomeClass *c = new(mem) SomeClass(); 

in modo da poter malloc tutta la memoria poi allocare come si desidera. Nota: assicurati di chiamare manualmente la decostruzione come non si chiama quando si chiama delete

+0

-1, malloc è altrettanto vietato come nuovo in questi ambienti. Questo aggiunge dolore senza guadagno. – MSalters

+0

Quindi il bombardamento di un blocco di memoria all'inizio è vietato? Sembra essere una regola stupida. – Lodle

3

L'aiuto per l'iniezione di dipendenza nella propria situazione?Questo potrebbe eliminare tutte le variabili globali e consentire una facile sostituzione delle dipendenze nei test delle unità.

Ogni funzione principale del thread passa una mappa contenente dipendenze (driver, cassette postali, ecc.) E le archivia nelle classi che le useranno (invece di accedere ad alcune variabili globali).

Per ogni ambiente (target, simulatore, unit test ...) si crea una funzione di "configurazione" che crea tutti gli oggetti necessari, i driver e tutti i thread, dando ai thread l'elenco delle dipendenze. Ad esempio, la configurazione di destinazione potrebbe creare un driver USB e iniettarlo in qualche thread di comunicazione, mentre la configurazione di test dell'unità di comunicazione potrebbe creare un driver USB stub controllato dai test.

Se è assolutamente necessaria questa "trasparenza" per le variabili importanti, creare classi per esse, che le manterranno a un indirizzo noto e iniettare queste classi dove necessario.

È un po 'più di lavoro rispetto agli elenchi statici di oggetti, ma la flessibilità è fantastica, soprattutto quando si verificano problemi di integrazione complicati e si desidera scambiare componenti per il test.

Approssimativamente:

// Config specific to one target. 
void configure_for_target_blah(System_config& cfg) 
{ // create drivers 
    cfg.drivers.push_back("USB", new USB_driver(...)) 
    // create threads 
    Thread_cfg t; 
    t.main = comms_main; // main function for that thread 
    t.drivers += "USB"; // List of driver names to pass as dependencies 
    cfg.threads += t; 
} 

// Main function for the comms thread. 
void comms_main(Thread_config& cfg) 
{ 
    USB_driver* usb = cfg.get_driver("USB"); 
    // check for null, then store it and use it... 
} 

// Same main for all configs. 
int main() 
{ 
    System_config& cfg; 
    configure_for_target_blah(cfg); 
    //for each cfg.drivers 
    // initialise driver 
    //for each cfg.threads 
    // create_thread with the given main, and pass a Thread_config with dependencies 
}