Я работаю над библиотекой, которая определяет клиентский интерфейс для некоторого сервиса. Под капотом я должен проверить данные, обеспеченные пользователями, и затем передать его процессу "механизма" с помощью класса Соединения из другой библиотеки (примечание: класс Соединения не известен пользователям нашей библиотеки). Один из моих коллег предложил использовать 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;
}
Однако мне очень трудно протестировать - даже если я связываю свои тесты с некоторой дразнившей реализацией Соединения, у меня нет легкого доступа к нему, чтобы установить и проверить ожидания. Я пропускающий что-то, или намного более чистое и тестируемое решение использует интерфейс + фабрика:
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 в этой ситуации?
Насколько я знаю, обычная причина использования идиомы Pimpl - уменьшить зависимости времени компиляции / компоновки от реализации класса (путем полного удаления деталей реализации из общедоступного файла заголовка). Другая причина может заключаться в том, чтобы дать классу возможность динамически изменять свое поведение (он же шаблон состояния ).
Второе, похоже, здесь не так, и первое также может быть достигнуто с помощью наследования + фабрики. Однако, как вы отметили, последнее решение намного проще провести модульное тестирование, поэтому я бы предпочел это.
Да, это хорошее место для использования паттерна Pimpl, и да, это будет трудно протестировать в существующем виде.
Проблема в том, что эти две концепции противостоят друг другу:
Однако это не означает, что вы должны жертвовать одним ради другого. Это просто означает, что вы должны адаптировать свой код.
Итак, что если Connection
будет реализован с использованием той же идиомы?
class Connection
{
private:
ConnectionImpl* mImpl;
};
И поставляться через Factory:
// Production code:
Client client = factory.GetClient();
// Test code:
MyTestConnectionImpl impl;
Client client = factory.GetClient(impl);
Таким образом, вы сможете получить доступ к мельчайшим деталям вашей тестовой реализации соединения во время тестирования клиента, не раскрывая реализацию клиенту и не нарушая ABI.