Это хорошее место для использования шаблона PIMPL?

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

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 в такой ситуации?


person chalup    schedule 07.07.2010    source источник
comment
Возможно, я ошибаюсь, но если у вас есть член типа Connection (а не Connection*), вы должны включить его определение в свой заголовок, чтобы Connection был известен пользователям вашей библиотеки.   -  person ereOn    schedule 07.07.2010
comment
См. stackoverflow.com/questions/ 825018 /   -  person Tomek Szpakowicz    schedule 07.07.2010
comment
@ereOn: в заголовке клиента я использую только предварительное объявление класса ClientImpl (это возможно, поскольку член является указателем), поэтому заголовок ClientImpl можно скрыть от клиентов моей библиотеки, поэтому я могу использовать Connection как член ClientImpl.   -  person chalup    schedule 07.07.2010
comment
Неважно. Я слишком быстро читаю. Я думал, что Connection была частной структурой.   -  person ereOn    schedule 07.07.2010


Ответы (3)


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

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

person Péter Török    schedule 07.07.2010
comment
Еще одна причина использовать идиому Pimpl - отделить детали реализации от публичного интерфейса класса: в заголовке класса видны только публичные члены. - person Luc Touraille; 07.07.2010
comment
Единственная причина использовать идиому Pimpl - это когда вы не можете использовать абстрактный базовый класс. Например, когда вы реализуете класс, который должен быть создан как значение в стеке или как член другого класса. В этой ситуации у вас все еще может быть «брандмауэр компиляции» благодаря pimpl. - person Tomek Szpakowicz; 07.07.2010
comment
Если я экспортирую только абстрактный интерфейс + фабрику, разве это не уменьшает также зависимости времени компиляции / компоновки? - person chalup; 07.07.2010
comment
@Luc, это то, что я имел в виду, говоря о сокращении зависимостей времени компиляции / компоновки до реализации класса, по-видимому, недостаточно четко - обновлено. - person Péter Török; 07.07.2010
comment
@chalup, да, именно поэтому я предположил, что это лучшее решение в целом. - person Péter Török; 07.07.2010
comment
@tomekszpakowicz: я могу инициализировать член-указатель в конструкторе класса (либо указателем, переданным в аргументе, либо путем вызова фабричного метода в списке конструкторов). И вместо значения в стеке я могу использовать указатель с областью действия. - person chalup; 07.07.2010
comment
@chalup Конечно, можешь. Но C ++ дает нам уникальную возможность определять абстрактные типы данных, которые можно использовать так же, как встроенные типы, например: complex a(1, -2); complex b = 2 * a; complex c = a + b - complex(4, 1); Можете ли вы использовать с ними трюк с абстрактным базовым классом? - person Tomek Szpakowicz; 07.07.2010
comment
@tomek: нет, я не могу, хороший момент. Но с другой стороны, мне не нужен такой синтаксис в ситуации, описанной в моем вопросе. - person chalup; 07.07.2010
comment
@Peter: Я думаю, что это мой комментарий был недостаточно ясным :). Я хотел подчеркнуть, что отделение деталей реализации от общедоступного интерфейса полезно не только для сокращения времени компиляции / компоновки, но и для обеспечения более чистой документации в коде: когда пользователь / разработчик, использующий класс, просматривает заголовок , он видит только то, что должен видеть, то есть членов общественности. Если ему действительно нужна дополнительная информация о внутреннем устройстве класса, он может найти класс реализации, но оба аспекта (интерфейс / реализация) хорошо изолированы. - person Luc Touraille; 07.07.2010

GoTW15

GoTW28

От Херба Саттера. Хорошие указатели для начала.

person DumbCoder    schedule 07.07.2010

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

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

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

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

А что, если Connection был реализован с использованием той же идиомы?

class Connection
{
private:
  ConnectionImpl* mImpl;
};

И доставлены через завод:

// Production code:

Client client = factory.GetClient();

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

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

person Matthieu M.    schedule 07.07.2010
comment
Проблема в том, что люди в пищевой цепочке не хотят никаких фабричных методов / классов или каких-либо конструкторов, которые принимают зависимости - должен быть только конструктор Client :: Client () ... - person chalup; 08.07.2010
comment
В этом случае вы можете использовать фабрику внутри (сделав ее одноэлементной) для генерации правильного ClientImpl. Таким образом интерфейс не меняется, но изменяется конструктор клиента. - person Matthieu M.; 08.07.2010