Мне нужен способ предоставить разные интерфейсы из одного объекта.
Например. Пользователь 1 должен иметь возможность звонить Foo::bar()
, а пользователь 2 должен иметь возможность звонить Foo::baz()
, но пользователь 1 не может звонить Foo::baz()
и, соответственно, пользователь 2 не может звонить Foo::bar()
.
Мне удалось это сделать, но я не думаю, что это оптимально.
class A
{
public:
virtual void bar() = 0;
virtual ~A() = 0;
};
class B
{
public:
virtual void baz() = 0;
virtual ~B() = 0;
};
class Foo : public A, public B
{
public:
Foo() = default;
void baz() override;
void bar() override;
};
class Factory
{
public:
Factory()
{
foo = std::make_shared<Foo>();
}
std::shared_ptr<A> getUserOne()
{
return foo;
}
std::shared_ptr<B> getUserTwo()
{
return foo;
}
private:
std::shared_ptr<Foo> foo;
};
Есть ли лучший способ добиться этого. Возможно, с объектами-обертками. Мне действительно не нужно выделять этот объект foo с помощью new
(std::make_shared
). Я даже предпочитаю этого не делать, но я не могу использовать необработанные указатели, а интеллектуальные указатели дают ненужные накладные расходы и системные вызовы.
Обновлено: я постараюсь привести пример.
Есть машина. Первый пользователь — водитель. Он может управлять рулем, ускоряться или использовать тормоза. Второй пользователь — пассажир, и он может, например, управлять радио.
Я не хочу, чтобы пассажир мог пользоваться тормозами, а водитель — радио.
Кроме того, они оба находятся в машине, поэтому действия первого пользователя повлияют на второго пользователя и наоборот.
Прочтите о ху проблема. Ваши вопросы читаются как «я сделал xy, но я думаю, что это не оптимально, что еще мне делать?», но это не на 100% ясно, какую проблему вы пытаетесь решить. Что такое «пользователи»? Зачем вам другой доступ?
Позвольте мне привести пример. Представьте, что пользователи — это два разных потока. Один из них должен иметь возможность заполнять некоторые полезные данные в классе, а другой должен иметь возможность их читать, очищать, возможно, блокировать другого пользователя. Другими словами - один из пользователей должен иметь возможность использовать методы объекта, а другой должен иметь возможность управлять объектом.
По сути, вам нужны общие данные между двумя объектами. Наследование не очень хороший выбор для этого, потому что вам не только не нужны is A
отношения, но вы явно хотите их избежать. Поэтому состав - это ваш ответ, тем более, что у вас есть фабрика:
class Data
{
public:
void bar();
void baz();
};
Тогда вместо наследования вы должны использовать композицию:
class A
{
public:
A(Base *base) : mBase(base) {}
void bar() { mBase->bar(); }
private:
Base *mBase = nullptr;
};
//class B would be the same only doing baz()
Наконец Factory
:
class Factory
{
public:
A *getUserOne() { return &mA; }
B *getUserTwo() { return &mB; }
private:
Base mBase;
A mA(&mBase);
B mB(&mBase);
};
Пара моментов об этом решении. Пока он не размещается в куче, вам нужно будет поддерживать жизнь Factory
, пока есть его пользователи. По этой причине использование std::shared_ptr
, как в OP, может быть идеей умный. :-) Но это, конечно, связано со стоимостью атомарного подсчета ссылок.
Во-вторых, A
никак не связано с B
. Это задумано и, в отличие от исходного решения, не допускает dynamic_cast
между A
и B
.
Наконец, где будет реализация, зависит от вас. Вы можете иметь все это в Data
, а A
и B
просто вызывать его (как показано выше), но вы также можете превратить Data
просто в struct
, содержащий только ваши данные, и реализовать свои методы в A
и B
соответственно. Последнее является более «ориентированным на данные» программированием, которое в наши дни пользуется большой популярностью, в отличие от более традиционного «объектно-ориентированного», которое я решил продемонстрировать.
Вы можете объявить свои данные отдельно
struct Data
{
/* member variables */
};
Иметь класс интерфейса, способный манипулировать указанными данными, все члены будут защищены
class Interface
{
protected:
Interface(Data &data) : m_data{data} {}
void bar() { /* implementation */ }
void baz() { /* implementation */ }
Data &m_data;
};
Имеют производные классы, которые делают общедоступными конкретные члены
class A : private Interface
{
public:
A(Data &data) : Interface{data} {}
using Interface::bar;
};
class B : private Interface
{
public:
B(Data &data) : Interface{data} {}
using Interface::baz;
};
Таким образом, у вас также могут быть пользователи, способные иметь перекрывающийся доступ к некоторым функциям без необходимости реализовывать их несколько раз.
class Admin : private Interface
{
public:
Admin(Data &data) : Interface{data} {}
using Interface::bar;
using Interface::baz;
};
Конечно, в зависимости от того, как вы используете данные, вам может понадобиться указатель или общий указатель, возможно, добавить некоторую синхронизацию между доступами из нескольких потоков.
Пример кода с использованием этой модели:
void test()
{
Data d{};
auto a = A{d};
a.bar();
// a.baz is protected so illegal to call here
auto b = B{d};
b.baz();
// b.bar is protected so illegal to call here
auto admin = Admin{d};
admin.bar();
admin.baz();
}
Мне это кажется эффективным в том смысле, что у вас есть только один набор данных и одна реализация для манипулирования данными, независимо от того, сколько у вас типов пользователей.
Кто такие пользователь1 и пользователь2? Как они вообще попадают внутрь одной и той же программы? Как вы решаете, что user1 является В самом деле user1, а не user2, притворяясь?