2010-07-07 14 views
5

Pracuję nad biblioteką, która definiuje interfejs klienta dla niektórych usług. Pod maską muszę zweryfikować dane dostarczone przez użytkowników, a następnie przekazać je do procesu "engine", używając klasy Connection z innej biblioteki (uwaga: klasa Connection nie jest znana użytkownikom naszej biblioteki). Jeden z moich kolegów zaproponował korzystania PIMPL:Czy to dobre miejsce na wzór 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; 
} 

Jednak uważam, że to bardzo trudne do badania - nawet jeśli mogę połączyć moje testy jedni się naśmiewali realizacji połączenia, nie mieć do niego łatwy dostęp, aby ustawić i sprawdź oczekiwania. Ja czegoś brakuje, lub znacznie czystsze i sprawdzalne rozwiązaniem jest użycie interfejsu + fabryczne:

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(); 
} 

czy są jakieś powody, aby przejść z PIMPL w tej sytuacji?

+0

Mogę się mylić, ale jeśli masz członka typu 'Connection' (a nie' Connection * '), musisz dołączyć jego definicję do swojego nagłówka, więc" Connection "jest znane użytkownikom twojej biblioteki. – ereOn

+0

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

+0

@ereOn: w nagłówku klienta używam tylko deklaracji forward klasy ClientImpl (jest to możliwe, ponieważ element jest wskaźnikiem), więc nagłówek ClientImpl może być ukryte przed moimi klientami bibliotecznymi, więc mogę korzystać z połączenia jako członek ClientImpl. – chalup

Odpowiedz

4

AFAIK Zwykłym powodem używania idiomu Pimpl jest zredukowanie zależności czasu kompilacji/łącza do implementacji klasy (przez całkowite usunięcie szczegółów implementacji z publicznego pliku nagłówkowego). Innym powodem może być dynamiczne zmienianie zachowania klasy (inaczej: State pattern).

Nie wydaje się, że drugi przypadek ma tu miejsce, a pierwszy można osiągnąć również dzięki dziedziczeniu + fabryka. Jak jednak zauważyłeś, ostatnie rozwiązanie jest o wiele łatwiejsze do testowania jednostkowego, więc wolałbym to.

+0

Innym powodem używania idiomu Pimpl jest oddzielenie szczegółów implementacji od publicznego interfejsu klasy: tylko członkowie publiczni są widoczni w nagłówku klasy. –

+1

Jedynym powodem używania idiomu Pimpl jest sytuacja, w której nie można używać abstrakcyjnej klasy bazowej. Na przykład po zaimplementowaniu klasy, która ma być instancjonowana jako wartość na stosie lub jako członek innej klasy. W tej sytuacji nadal możesz mieć "zaporę kompilacji" dzięki pimpl. –

+0

Jeśli eksportuję tylko abstrakcyjny interfejs + fabryka, czy to nie zmniejsza również zależności czasu kompilacji/łącza? – chalup

0

Tak, to jest dobre miejsce do korzystania z wzoru Pimpl, i tak będzie trudno przetestować jak jest.

Problemem jest to, że dwie koncepcje naprzeciwko siebie:

  • Pimpl jest o ukrywanie zależności od klienta: zmniejsza kompilacji/czas łącza i jest lepszy z punktu widzenia stabilności ABI.
  • Unit Testing jest zwykle o interwencji chirurgicznej w zależności od zastosowania (mock up, na przykład)

Jednak to nie znaczy, że należy poświęcić jeden dla drugiego. Oznacza to jedynie, że powinieneś dostosować swój kod.

Co, jeśli Connection został zaimplementowany z tym samym idiomem?

class Connection 
{ 
private: 
    ConnectionImpl* mImpl; 
}; 

i dostarczone przez fabrykę:

// Production code: 

Client client = factory.GetClient(); 

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

W ten sposób można uzyskać dostęp do Maryla Rodowicz szczegóły testu realizować połączenia podczas testowania klienta bez narażania realizację do klienta lub zerwania ABI .

+0

Problem polega na tym, że ludzie w łańcuchu żywnościowym nie chcą żadnej fabrycznej metody/klasy ani konstruktorów, którzy biorą na siebie zależności - powinien być tylko konstruktor Client :: Client() ... – chalup

+0

W tym przypadku można użyć fabryka wewnętrznie (dzięki czemu singleton) generuje odpowiedni "ClientImpl". W ten sposób interfejs się nie zmienia, ale konstruktor klienta to robi. –

Powiązane problemy