Несколько Pimpl-классов, использующих друг друга - PullRequest
0 голосов
/ 29 июня 2018

У меня есть три класса, которые я предоставляю пользователю через pimpl. Model - это контейнер данных, который можно прочитать и записать в файл. Manipulator - это объект, который может загрузить Model, выполнить изменения и вернуть его как новый Model. Consumer загружает Model и позволяет пользователю работать с ним (читать его свойства, распечатывать и т. Д.).

class Model {
    Model(std::string &filename);
    void toFile(std::string &filename);
private:
    class ModelImpl;
    std::unique_ptr<ModelImpl> mImpl;
};

class Manipulator {
    Manipulator(Model &model);
    Model alterModel(...);
private:
    class ManipulatorImpl;
    std::unique_ptr<ManipulatorImpl> mImpl;
};

class Consumer {
    Consumer(Model &model);
    void loadModel(Model &model);
    void doSomething();
private:
    class ConsumerImpl;
    std::unique_ptr<ConsumerImpl> mImpl;
};

Причина использования pimpl в первую очередь заключается в том, чтобы скрыть внутренние типы данных, используемые Model. Единственными видимыми для пользователя типами должны быть Model, Manipulator и Consumer и стандартные типы c ++.

Проблема, с которой я столкнулся здесь, заключается в реализациях ConsumerImpl и ManipulatorImpl: в этих классах я должен получить доступ к базовым структурам данных ModelImpl, но pimpl скрывает их:

Consumer::ConsumerImpl::loadModel(Model model) {
    auto someModelValue = model.mImpl->someInternalValue;
}

Очевидно, это не работает. Как это решить? Правильно ли здесь решение pimpl?

Редактировать: Мой коллега придумал это:

class Consumer {
    Consumer(Model &model);
    void loadModel(Model &model);
    void doSomething();
private:
    class ConsumerImpl;
    std::unique_ptr<ConsumerImpl> mImpl;
};

class Model {
    Model(std::string &filename);
    void toFile(std::string &filename);
private:
    void *getObjectPtr();
    class ModelImpl;
    std::unique_ptr<ModelImpl> mImpl;
    friend Consumer;
};

void *Model::Model::getObjectPtr() {
    return mImpl->getObjectPtr();
}



class Model::ModelImpl {
public:
//    [...]
    void *getObjectPtr();

private:
    SecretInternalType mData;
};

void *Model::ModelImpl::getObjectPtr() {
    return static_cast<void*>(&mData);
}


// Access the internal data of Model from Consumer:
void Consumer::ConsumerImpl::doSomething() {
    SecretInternalType* data = static_cast<SecretInternalType*>(mModel->getObjectPtr());
}

По сути, в модели есть метод, который возвращает пустой указатель на (скрытые) внутренние данные. Потребитель может получить этот указатель, привести его обратно к правильному типу и получить доступ к данным. Чтобы сделать это доступным только из класса Consumer, метод является закрытым, но friends с Consumer.

Я реализовал этот подход, и он работает для меня. Тем не менее, мне любопытно, что вы об этом думаете и есть ли какие-либо проблемы.

1 Ответ

0 голосов
/ 29 июня 2018

Проблема, с которой вы сталкиваетесь, на самом деле не связана с идиомой Pimpl - представьте, что вы стираете связанные с pimpl части своих фрагментов, проблема остается той же самой, поскольку она вызвана необходимостью доступа к частному представлению вашей модели (или ModelImpl) экземпляр. Вот как я бы попытался подойти к ситуации:

  • Определите список операций, которые имеют смысл для экземпляра Consumer для вызова на экземпляре Model. Они должны быть частью открытого интерфейса модели. Сделайте то же самое для отношений между моделью и манипулятором. Если это слишком загромождает ваш интерфейс модели, разделите его на два отдельных абстрактных класса, позвольте реализации модели наследоваться от обоих, и Consumer / Maninpulator работает на интерфейсе базового класса, который им предназначен.
  • Пересмотрите, какие части модели стоит скрыть. Если Модель владеет каким-то контейнером, к которому должен получить доступ потребитель, выставьте его (Effective STL, Item 2), доступ для чтения с помощью соответствующего метода должен быть в порядке.
  • Если объектам-потребителям по-прежнему требуется больше информации, их собственная роль может быть больше, чем у обычного потребителя, поэтому, возможно, некоторые части их реализации должны быть реализованы в другом классе или в модели?
  • Ввести (n) (абстрактный) класс для обмена данными между моделью и потребителем. Передайте это от Потребителя к Модели, позвольте Модели выбрать, какую информацию передать промежуточному слою. Но это уже вносит определенную сложность, которая может быть совершенно ненужной.

Какие бы изменения вы ни применяли к своему дизайну, вы все равно можете выбрать, использовать ли идиому Pimpl с методами переадресации или нет. И последнее предложение: не объявляйте один из классов friend - это может быть довольно самоуверенным, но ваш сценарий не предполагает, что такая сильная связь необходима.

...