Работа над прочным шаблоном команды с помощью shared_ptr

Я пытаюсь реализовать очень чистый шаблон команды в библиотеке.

Сейчас у меня есть следующая структура (несколько частей все еще дорабатываются):

  1. у пользователей (клиент-код) есть некоторый объект, назовите его «Менеджер»
  2. Manager содержит коллекцию shared_ptr<Foo>
  3. Manager предоставляет доступ к коллекции, возвращая shared_ptr<Foo>
  4. У меня есть абстрактный класс Command и иерархия команд для действий, выполняемых на Foo
  5. Код клиента не должен вызывать Command::execute(), только Manager должен, Manager::execute(shared_ptr<Command>), чтобы он мог обрабатывать отмену/возврат

Я хотел бы следовать следующим правилам:

  1. у пользователей (клиент-код) есть некоторый объект, назовите его «Менеджер»
  2. Manager содержит коллекцию shared_ptr<Foo>
  3. Manager предоставляет доступ к коллекции, возвращая shared_ptr<const Foo>
  4. У меня есть абстрактный класс Command и иерархия команд для действий, выполняемых на Foo
  5. Клиентский код не может (без обходных путей) вызывать Command::execute(), только Manager может, Manager::execute(shared_ptr<Command>), чтобы он мог обрабатывать отмену/повтор и получать неконстантные интеллектуальные указатели
  6. Manager должен иметь возможность разрешить Command объектам доступ и изменение shared_ptr<Foo>, даже если пользователь инициализирует Command objecst с помощью shared_ptr<const Foo>.

Я просто пытаюсь найти лучший способ справиться с раздачей shared_ptr<const Foo>, позволяя работать номерам 5 и 6.

Есть ли какой-нибудь пример/шаблон проектирования, который делает это, из которого я мог бы извлечь уроки? Хорошая ли это идея по сравнению с тем, что у меня уже есть/над чем я работаю?


person Matt    schedule 22.04.2011    source источник
comment
Вместо того, чтобы выдавать shared_ptr<const Foo>, не могли бы вы вместо этого выдать ключ или индекс в контейнере, откуда он пришел? Это избавит от необходимости иметь дело с const_cast и его потенциальными ловушками.   -  person Emile Cormier    schedule 22.04.2011
comment
@ Эмиль Кормье Я думаю, что на самом деле было бы неплохо просто раздать ключи или индексы. Единственная проблема (возможно) заключается в том, что я думаю, что мне может понадобиться, чтобы клиентский код имел доступ к константным членам для чтения значений. Вот почему иметь константные указатели — это хорошо. Я хотел бы избежать const_cast, потенциально выполняя поиск по указателю.   -  person Matt    schedule 23.04.2011


Ответы (3)


Поскольку в противном случае это не имело бы для меня никакого смысла, я собираюсь предположить, что

  • ваша библиотека предоставляет класс Manager (или, по крайней мере, базовый класс), и
  • клиенты должны использовать этот класс для вызова Command.

В этом случае, возможно, что-то вроде этого может работать:

void Manager::execute(Command& cmd, shared_ptr<Foo const> const& target)
{
    shared_ptr<Foo> mutableTarget = this->LookupMutableFoo(mutableTarget); // throws if not found
    cmd.execute(mutableTarget); // client cannot invoke without mutable pointer
}

// or, if the "target" needs to be stored in the Command you could use something like this:
void Manager::execute(Command& cmd)
{
    shared_ptr<Foo> mutableTarget = this->LookupMutableFoo(cmd.GetTarget()); // throws if not found
    cmd.execute(mutableTarget); // client cannot invoke without mutable pointer
}

Однако я не уверен, что использование const является лучшим решением здесь. Возможно, вам следует обернуть свои объекты Foo, например. ClientFoo объектов. Менеджер только раздает указатели на ClientFoo. Затем менеджер может (например, через friend) получить Foo из ClientFoo и использовать его для вызова Command.

person Paul Groke    schedule 22.04.2011
comment
Примерно так мне бы хотелось. Я пытаюсь решить, будут ли все команды иметь одну цель shared_ptr<Foo>, поэтому я могу передать ее для выполнения или нет. Хорошо, что по сути ни один клиент не может вызвать command.execute(...), потому что он не может получить неконстантный указатель. - person Matt; 22.04.2011
comment
Кроме того, почему вы рекомендуете класс-оболочку вместо const? Я вижу, как я могу получить один и тот же конечный результат в любом случае (я предоставляю только методы, которые являются константами в Foo). - person Matt; 22.04.2011
comment
Главным образом потому, что метод LookupMutableFoo кажется несколько грязным (или, по крайней мере, странным) :) Кроме того, использование отдельного класса гораздо более гибко, чем использование const. Но, как я уже писал, я не уверен - мне нужно знать гораздо больше о том, что вы пытаетесь сделать, чтобы быть уверенным. - person Paul Groke; 22.04.2011
comment
Если у меня есть std::set<std::shared_ptr<Foo> > и я хочу найти std::shared_ptr<const Foo>, будет ли это работать из коробки? Я знаю, что Set эффективен при поиске элементов (он хранится в виде красно-черного дерева или что-то в этом роде?), но также и то, что эти типы могут быть несовместимы. Мне просто интересно, могу ли я использовать set::find. - person Matt; 23.04.2011
comment
Нет, не сработает. Вы можете const_pointer_cast shared_ptr, а затем просто убедиться, что он содержится в set. Или вы можете использовать навязчивый набор повышения: boost.org/doc/libs/1_46_1/doc/html/intrusive/ - person Paul Groke; 23.04.2011
comment
@pgroke Понятно. К счастью, я понял, что имеет смысл хранить std::map<Key, std::shared_ptr<Foo> > там, где я могу получить Key из const Foo. Теперь поиск в таблице имеет смысл, и клиентский код может получить доступ к элементам const Foo (а не просто удерживать какой-либо ключ) - person Matt; 24.04.2011

Я думаю, что это passkey pattern должно быть правильным для вас:

class CommandExecuteKey{
private:
  friend class Manager;
  CommandExecuteKey(){}
};

class Command{
public:
  // usual stuff
  void execute(CommandExecuteKey);
};

class Manager{
public
  void execute(Command& cmd){
    // do whatever you need to do
    cmd.execute(CommandExecuteKey());
  }
};

Теперь Command ничего не знает о Manager, только о ключе, который нужен для функции execute. Пользователь не сможет вызвать метод execute напрямую, так как только Manager может создавать CommandExecuteKey объекты благодаря приватному конструктору и friendship.

int main(){
  Command cmd;
  Manager mgr;
  //cmd.execute(CommandExecuteKey()); // error: constructor not accessible
  mgr.execute(cmd); // works fine, Manager can create a key
}

Теперь, что касается вашего 6-го пункта:
Когда вы получите команду, найдите все свои shared_ptr<Foo> для правильного объекта (используя сохраненный shared_ptr команды в качестве ключа поиска), а затем передайте этот изменяемый объект из ваших внутренних shared_ptrs обратно. к команде.

person Xeo    schedule 22.04.2011
comment
Кажется, что Command все еще транзитивно знает о менеджере (через CommandExecuteKey). Но тем не менее это интересный образец. Я никогда не слышал об этом раньше. - person Emile Cormier; 22.04.2011
comment
@Emile: Ну, ты тоже знаешь, кому ты вручаешь ключи от входной двери, нет? ;) Одним из преимуществ является то, что вы можете легко поменять базовый дружественный класс, даже не заметив Command. То же самое и с вашей входной дверью, она никогда не узнает, кому вы передаете ключи. - person Xeo; 22.04.2011
comment
Хе-хе. :-) О, теперь я вижу. Это похоже на дружбу, ограниченную только определенными частями класса. Это как если бы я хотел раздать ключи от входной двери, не называя свою группу крови и SSN. +1 - person Emile Cormier; 22.04.2011
comment
@Emile: Правильно, это позволяет точно контролировать, кто и что может делать с вашим классом. :) Связанный вопрос переходит в дальнейшие объяснения. - person Xeo; 22.04.2011
comment
Мне нравится этот метод, но я чувствовал, что выбранный ответ лучше соответствует тому, что я хочу. Тем не менее, это действительно крутой трюк, спасибо! - person Matt; 24.04.2011

Я не понимаю ваш вопрос на 100%, но вот...

Единственное, что я могу придумать для # 5, это сделать Command::execute частным/защищенным и сделать Manager friend из Command. Недостатком этого подхода является то, что теперь вы ввели зависимость от Command до Manager.

Что касается #6, если объекты shared_ptr<const Foo> пользователя были получены из коллекции shared_ptr<Foo> Менеджера, то Manager должен иметь возможность безопасно преобразовать const_pointer_cast shared_ptr<const Foo*> обратно в shared_ptr<Foo*>. Если Manager попытается преобразовать константу shared_ptr<const Foo*>, где pointee является фактическим постоянным объектом, вы получите неопределенное поведение.


Я подумал о другом решении для № 5:

Определите класс ExecutableCommand, производный от Command. ExecutableCommand имеет добавленный метод вызова команды, который может использоваться только Manager. Клиенты могут получить доступ только к ExecutableCommand объектам через указатели/ссылки на Command. Когда менеджер хочет вызвать Command, он понижает его до ExecutableCommand, чтобы получить доступ к интерфейсу вызова.

Рабочий пример (включая const_pointer_cast для #6):

#include <iostream>
#include <string>
#include <vector>
#include <boost/shared_ptr.hpp>

using namespace std;
using namespace boost;

//------------------------------------------------------------------------------
struct Foo
{
    Foo(int x) : x(x) {}
    void print() {++x; cout << "x = " << x << "\n";} // non-const
    int x;
};

//------------------------------------------------------------------------------
struct Command
{
    // Interface accessible to users
    std::string name;

private:
    virtual void execute() = 0;
};

//------------------------------------------------------------------------------
struct ExecutableCommand : public Command
{
    // Only accessible to Manager
    virtual void execute() {} // You may want to make this pure virtual
};

//------------------------------------------------------------------------------
struct PrintCommand : public ExecutableCommand
{
    PrintCommand(shared_ptr<const Foo> foo)
        : foo_( const_pointer_cast<Foo>(foo) ) {}

    void execute() {foo_->print();}

private:
    shared_ptr<Foo> foo_;
};

//------------------------------------------------------------------------------
struct Manager
{
    void execute(Command& command)
    {
        ExecutableCommand& ecmd = dynamic_cast<ExecutableCommand&>(command);
        ecmd.execute();
    }

    void addFoo(shared_ptr<Foo> foo) {fooVec.push_back(foo);}

    shared_ptr<const Foo> getFoo(size_t index) {return fooVec.at(index);}

private:
    std::vector< shared_ptr<Foo> > fooVec;
};

//------------------------------------------------------------------------------
int main()
{
    Manager mgr;
    mgr.addFoo( shared_ptr<Foo>(new Foo(41)) );

    Command* print = new PrintCommand(mgr.getFoo(0));
    // print.execute() // Not allowed
    mgr.execute(*print);

    delete print;
}
person Emile Cormier    schedule 22.04.2011
comment
Не будет ли проблемой то, что Command является базовым классом, поэтому превращение Manager в friend не будет наследоваться (дружба не наследуется)? - person Matt; 22.04.2011
comment
Если Manager вызывает команды только через базовый класс Command, то это не проблема. - person Emile Cormier; 22.04.2011