2010-07-07 3 views
5

Я работаю над библиотекой, которая определяет клиентский интерфейс для некоторой службы. Под капотом я должен проверить данные, предоставленные пользователями, а затем передать их процессу «двигатель», используя класс Connection из другой библиотеки (обратите внимание: класс Connection не известен пользователям нашей библиотеки). Один из моих коллег предложил использовать Pimpl:Является ли это хорошим местом для использования шаблона 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; 
} 

Однако, я считаю, это очень тяжело протестировать - даже если я свяжу свои тесты в какой-то издевался реализации Connection, не имеют легкий доступ к нему, чтобы установить и утверждать ожидания. Могу ли я что-то отсутствует, или значительно чище и проверяемым решение использует интерфейс + завод:

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

Существуют ли какие-либо причины, чтобы пойти с Pimpl в этой ситуации?

+0

Возможно, я ошибаюсь, но если у вас есть член типа 'Connection' (а не' Connection * '), вы должны включить его определение в свой заголовок, и поэтому' Connection' известно пользователям вашей библиотеки. – ereOn

+0

См. Http: // stackoverflow.com/questions/825018/pimpl-idiom-vs-pure-virtual-class-interface –

+0

@ereOn: в заголовке клиента Я использую только декларацию объявления класса ClientImpl (возможно, поскольку элемент является указателем), поэтому заголовок ClientImpl может быть скрытый от моих клиентов библиотеки, поэтому я могу использовать Connection как член ClientImpl. – chalup

ответ

4

AFAIK обычная причина использования идиомы Pimpl заключается в сокращении зависимостей времени компиляции/ссылки до реализации класса (путем полного удаления сведений о реализации из общего файла заголовка). Другая причина может заключаться в том, чтобы позволить классу динамически изменять свое поведение (ака State pattern).

Второе, похоже, не так, и первое может быть достигнуто с наследованием + фабрикой. Однако, как вы заметили, последнее решение намного проще для модульного тестирования, поэтому я бы предпочел это.

+0

Еще одна причина использования идиомы Pimpl заключается в том, чтобы отделить детали реализации от общедоступного интерфейса класса: только публичные элементы видны в заголовке класса. –

+1

Единственная причина использования идиомы Pimpl - это когда вы не можете использовать абстрактный базовый класс. Например, когда вы реализуете класс, который должен быть создан как значение в стеке или как член другого класса. В этой ситуации у вас все еще есть «компиляционный брандмауэр» благодаря pimpl. –

+0

Если я экспортирую только абстрактный интерфейс + фабрику, разве это не уменьшает зависимость времени компиляции/ссылки? – chalup

0

Да, это хорошее место для использования рисунка Pimpl, и да, это будет сложно проверить, как есть.

Проблема состоит в том, что эти две концепции противостоят друг другу:

  • Pimpl является прятать зависимости от клиента: это уменьшает время компиляции/ссылок и лучше с точки стабильности ABI зрения.
  • модульное тестирование, как правило, о хирургическом вмешательстве в зависимости (использование макетов, например)

Однако, это не означает, что вы должны принести в жертву одну за другой. Это просто означает, что вы должны адаптировать свой код.

Теперь, если бы Connection был выполнен с одинаковой идиомой?

class Connection 
{ 
private: 
    ConnectionImpl* mImpl; 
}; 

и избавили через Factory:

// Production code: 

Client client = factory.GetClient(); 

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

Таким образом, вы можете получить доступ к вшивому песчаным подробностям теста осуществления связи во время тестирования клиента, не подвергая реализации клиента или нарушения ABI ,

+0

Проблема в том, что люди в пищевой цепи не хотят каких-либо фабричных методов/классов или каких-либо конструкторов, которые принимают зависимости - должен быть только конструктор Client :: Client() ... – chalup

+0

В этом случае вы можете использовать фабрика внутренне (делая ее singleton) для создания правильного 'ClientImpl'. Таким образом, интерфейс не изменяется, но конструктор клиента делает это. –