2011-10-18 25 views
5

In C++ utilizzo spesso oggetti in stile RAII per rendere il codice più affidabile e allocarli sullo stack per rendere il codice più performante (e per evitare bad_alloc).Stack assegnato agli oggetti RAII rispetto al principio DI

Ma la creazione di un oggetto di classe concreta su stack viola il principio di inversione di dipendenza (DI) e impedisce di deridere questo oggetto.

Si consideri il seguente codice:

struct IInputStream 
{ 
    virtual vector<BYTE> read(size_t n) = 0; 
}; 

class Connection : public IInputStream 
{ 
public: 
    Connection(string address); 
    virtual vector<BYTE> read(size_t n) override; 
}; 

struct IBar 
{ 
    virtual void process(IInputStream& stream) = 0; 
}; 

void Some::foo(string address, IBar& bar) 
{ 
    onBeforeConnectionCreated(); 
    { 
     Connection conn(address); 
     onConnectionCreated(); 
     bar.process(conn); 
    } 
    onConnectionClosed(); 
} 

posso testare IBar::process, ma voglio anche mettere alla prova Some::foo, senza creare oggetto Connection reale.

Sicuramente posso utilizzare una fabbrica, ma complicherà notevolmente il codice e introdurre l'allocazione dell'heap.
Inoltre, non mi piace aggiungere il metodo Connection::open, preferisco costruire oggetti completamente inizializzati e completamente funzionali.

vorrei fare Connection tipo un parametro di modello per Some (o per foo se estrarlo come una funzione gratuita), ma non sono sicuro che sia giusto modo (i modelli sembrano una magia nera per molte persone, in modo da preferisco usare il polimorfismo dinamico)

+2

I modelli non dovrebbero essere una magia nera per programmatori C++ più o meno competenti, non vedo alcun motivo per evitarli.Inoltre, non penso che l'allocazione dell'heap sia * quella * costosa (questo, ovviamente, dipende dal software che scrivi), quindi non vedo alcun motivo per evitarlo (quando usato con puntatori intelligenti). –

+4

@Alex B: c'è una ragione per evitarli, anche se sono d'accordo che non è perché sono "magia nera". È perché se tutto viene iniettato tramite i parametri del modello, quindi tutto ciò che scrivi è un modello, la tua libreria è solo di intestazione e questo può diventare piuttosto ingombrante in termini di compilazione o distribuzione. Anche se, suppongo che con attenzione si possa testare unitamente la libreria di sola intestazione, quindi costruire da essa una TU che contenga solo le istanze richieste dall'applicazione. –

+1

RAII e DI funzionano bene insieme, quindi il titolo è fuorviante, il problema è Stack Allocation vs DI. –

risposta

5

Quello che stai facendo in questo momento è "forzare" la classe RAII e la classe del fornitore di servizi (che, se vuoi testabilità, dovrebbe invece essere un'interfaccia). Affrontare questo da:

  1. astrarre Connection in IConnection
  2. hanno un ScopedConnection classe separata che fornisce RAII in cima a quella

Ad esempio:

void Some::foo(string address, IBar& bar) 
{ 
    onBeforeConnectionCreated(); 
    { 
     ScopedConnection conn(this->pFactory->getConnection()); 
     onConnectionCreated(); 
     bar.process(conn); 
    } 
    onConnectionClosed(); 
} 
+2

E accetta che 'ScopedConnection' non ha bisogno di essere deriso, è" sicuro "usare la versione reale anche nei test che dovrebbero isolare' Some :: foo'. Oppure, se ciò è inaccettabile, digrigna i denti e iniettalo come parametro template, o usa 'scoped_ptr' per fornire RAII, che come una classe standard (o di terze parti se sei ancora su C++ 03) è accettabile dipendenza. –

+0

Questo è quello che ho scritto sulla fabbrica. Per seguire la tua risposta, dovrei creare una factory solo per Connection, o factory per molte classi non correlate (come suggerisci tu). Porta questa fabbrica a 'Some' attraverso molti livelli (o rendila globale). – Abyx

+0

@Abyx: la fabbrica sarebbe un candidato ideale per DI, che sarebbe preferibile passarla manualmente o avere un globale. Ma ne hai bisogno per aumentare l'astrazione. – Jon

1

Con "posso usare una fabbrica, ma complicherà significativamente il codice e introdurrà l'allocazione dell'heap "Intendevo i seguenti passi:

Creare classe astratta e derivare Connection da esso

struct AConnection : IInputStream 
{ 
    virtual ~AConnection() {} 
}; 

Add fabbrica metodo per Some

class Some 
{ 
..... 
protected: 
    VIRTUAL_UNDER_TEST AConnection* createConnection(string address); 
}; 

Sostituire connecton pila-allocata da puntatore intelligente

unique_ptr<AConnection> conn(createConnection(address)); 
1

di scegliere tra il vero e proprio implementazione e il deriso, devi iniettare il vero ty pe che vuoi costruire in qualche modo. Il modo in cui consiglierei è di iniettare il tipo come parametro del modello opzionale. Ti consente di utilizzare in modo discreto lo Some::foo come in passato, ma ti consente di scambiare la connessione creata in caso di test.

template<typename ConnectionT=Connection> // models InputStream 
void Some::foo(string address, IBar& bar) 
{ 
    onBeforeConnectionCreated(); 
    { 
     ConnectionT conn(address); 
     onConnectionCreated(); 
     bar.process(conn); 
    } 
    onConnectionClosed(); 
} 

non vorrei creare il sovraccarico di una fabbrica e il polimorfismo runtime se si conosce il tipo effettivo al momento della compilazione.