2010-07-07 14 views
5

Sto lavorando a una libreria che definisce un'interfaccia client per qualche servizio. Sotto il cofano devo convalidare i dati forniti dagli utenti e poi passarlo al processo di "motore" utilizzando classe Connection da un'altra libreria (nota: la classe Connection non è noto agli utenti della nostra biblioteca). Uno dei miei colleghi ha proposto di utilizzare Pimpl:È un buon posto per usare il modello PIMPL?

class Client { 
public: 
    Client(); 
    void sendStuff(const Stuff &stuff) {_pimpl->sendStuff(stuff);} 
    Stuff getStuff(const StuffId &id) {return _pimpl->getStuff(id);} 
private: 
    ClientImpl *_pimpl; 
} 

class ClientImpl { // not exported 
public: 
    void sendStuff(const Stuff &stuff); 
    Stuff getStuff(const StuffId &id); 
private: 
    Connection _connection; 
} 

Tuttavia, trovo molto difficile prova - anche se collego il mio test per qualche implementazione deriso di connessione, non ho un facile accesso ad essa per impostare e convalidare le aspettative. Mi sto perdendo qualcosa, o molto più pulito e la soluzione verificabile utilizza l'interfaccia + fabbrica:

class ClientInterface { 
public: 
    void sendStuff(const Stuff &stuff) = 0; 
    Stuff getStuff(const StuffId &id) = 0; 
} 

class ClientImplementation : public ClientInterface { // not exported 
public: 
    ClientImplementation(Connection *connection); 
    // +implementation of ClientInterface 
} 

class ClientFactory { 
    static ClientInterface *create(); 
} 

Esistono motivi per andare con Pimpl in questa situazione?

+0

Potrei sbagliarmi, ma se hai un membro di tipo 'Connection' (e non' Connection * '), devi includere la sua definizione nell'header e quindi' Connection' è noto agli utenti della tua libreria. – ereOn

+0

Vedere http: // stackoverflow.it/questions/825018/pimpl-idiom-vs-pure-virtual-class-interface –

+0

@ereOn: nell'intestazione del client uso solo la dichiarazione anticipata della classe ClientImpl (è possibile, poiché il membro è un puntatore) così l'intestazione di ClientImpl può essere nascosto dai miei client di libreria, quindi posso usare Connection come membro di ClientImpl. – chalup

risposta

4

AFAIK il solito motivo per utilizzare l'idioma Pimpl è ridurre le dipendenze del tempo di compilazione/collegamento all'implementazione della classe (rimuovendo del tutto i dettagli di implementazione dal file di intestazione pubblico). Un altro motivo potrebbe essere quello di consentire alla classe di cambiare il suo comportamento dinamico (aka il State pattern).

Il secondo non sembra essere il caso qui, e il primo può essere raggiunto con l'eredità + una fabbrica pure. Tuttavia, come hai notato, quest'ultima soluzione è molto più semplice da testare, quindi preferirei questo.

+0

Un altro motivo per utilizzare l'idioma Pimpl consiste nel separare i dettagli di implementazione dall'interfaccia pubblica di una classe: solo i membri pubblici sono visibili nell'intestazione della classe. –

+1

L'unico motivo per usare l'idioma Pimpl è quando non è possibile utilizzare una classe base astratta. Ad esempio quando si implementa una classe che deve essere istanziata come valore sullo stack o come membro di un'altra classe. In quella situazione puoi ancora avere "firewall di compilazione" grazie a Pimpl. –

+0

Se esporto solo l'interfaccia astratta + factory, non riduce le dipendenze del tempo di compilazione/collegamento? – chalup

0

Sì, questo è un buon posto per utilizzare il modello di Pimpl, e sì, sarà difficile per testare come è.

Il problema è che i due concetti si oppongono l'un l'altro:

  • Pimpl è di nascondere le dipendenze dal client: questo riduce la compilazione/collegamento in tempo ed è meglio dal punto di vista della stabilità ABI.
  • Unit Testing è di solito circa un intervento chirurgico nelle dipendenze (uso di mock up, per esempio)

Tuttavia, ciò non significa che si dovrebbe sacrificare uno per l'altro. Significa semplicemente che dovresti adattare il tuo codice.

Ora, che cosa se Connection è stato realizzato con lo stesso linguaggio?

class Connection 
{ 
private: 
    ConnectionImpl* mImpl; 
}; 

e consegnati attraverso una fabbrica:

// Production code: 

Client client = factory.GetClient(); 

// Test code: 
MyTestConnectionImpl impl; 
Client client = factory.GetClient(impl); 

In questo modo, è possibile accedere al nitty particolari granulosi del test implementano di connessione durante il test del client senza esporre l'attuazione al cliente o rompere l'ABI .

+0

Il problema è che le persone nella catena alimentare non vogliono alcun tipo di metodo/classe factory né costruttori che prendono dipendenze - dovrebbe esserci solo il costruttore Client :: Client() ... – chalup

+0

In questo caso è possibile utilizzare la fabbrica internamente (rendendola singleton) per generare il giusto 'ClientImpl'. In questo modo l'interfaccia non cambia, ma lo fa il costruttore del client. –

Problemi correlati