Шаблон наблюдателя + Шаблон посетителя для системы сообщений

Недавно я начал внедрять систему диспетчеризации сообщений, использующую «шаблон наблюдателя»: здесь ничего особенного. Разрабатывая его, я подумал, что было бы неплохо посылать объекты «Послания» от «субъекта», которые могут существенно отличаться друг от друга и могут быть прочитаны многими «наблюдателями».

Эти разные сообщения принимали форму разных классов сообщений (например, подумайте о «Сообщении пользователя о выходе», «Переключение режима экрана» и «Уровень громкости изменился», все они нуждаются в различной информации), и вскоре я обнаружил, что «наблюдатели» «Мне не нужно было знать о каждом отдельном сообщении, которое я хотел бы создать (это было бы … неустойчиво, если не сказать больше). Вместо этого я хотел бы, чтобы каждый наблюдатель мог реагировать на определенные виды сообщений.

Поэтому, чтобы сделать что-то, я подумал, что двойная отправка может быть моим вариантом здесь. После небольшого количества я получил этот кусок (c ++ 11 только из-за цикла for):

#include <iostream>
#include <vector>
#include <string>

/**
* A few forward declarations.
*/

class Message_base;
class Message_type_a;
class Message_type_b;

/**
* Base observer...
*/

class Observer_base
{
public:

/**
* All these implementations are empty so we don't have to specify them
* in every single derived class.
*/

virtual void        get_message(const Message_base&) {}
virtual void        get_message(const Message_type_a&) {}
virtual void        get_message(const Message_type_b&) {}
};

/**
* Specification of all message types.
*/

class Message_base
{
public:

/**
* This is the method that will implement the double dispatching in all
* derived classes.
*/

virtual void        be_recieved(Observer_base &r) const=0;  //Now that's a nasty method name.
};

class Message_type_a:public Message_base
{
private:
int integer_value;

public:
Message_type_a(int v):integer_value(v) {}
int             get_integer_value() const {return integer_value;}
void            be_recieved(Observer_base &r) const {r.get_message(*this);}

};

class Message_type_b:public Message_base
{
private:
std::string string_value;

public:
Message_type_b(const std::string v):string_value(v) {}
std::string         get_string_value() const {return string_value;}
void            be_recieved(Observer_base &r) const {r.get_message(*this);}
};

/**
* This is the base clase for the Subject... Notice that there are no virtual
* methods so we could as well instantiate this class instead of derive it.
*/

class Subject_base
{
private:

std::vector<Observer_base *>    observers;

public:

void            emit_message(const Message_base& m) {for(auto o : observers) m.be_recieved(*o);}    //Again, nasty to read since it's... backwards.
void            register_observer(Observer_base * o) {observers.push_back(o);}
};

/**
* Now we will create a subject class for the sake of it. We could just call the
* public "emit_message" from main passing Message objects.
*/

class Subject_derived:public Subject_base
{
public:

void            emit_message_a(int v) {emit_message(Message_type_a(v));}
void            emit_message_b(const std::string v) {emit_message(Message_type_b(v));}
};

/**
* This gets fun... We make two observers. One will only get type_a messages
* and the other will get type_b.
*/

class Observer_type_a:public Observer_base
{
private:

int             index;  //We will use it to identify the observer.

public:

Observer_type_a(int i):index(i) {}
void            get_message(const Message_type_a& m) {std::cout<<"Observer_type_a ["<<index<<"] : got type_a message : "<<m.get_integer_value()<<std::endl;}
};

class Observer_type_b:public Observer_base
{
private:

std::string         name; //Merely to identify the observer.

public:

Observer_type_b(const std::string& n):name(n) {}
void            get_message(const Message_type_b& m) {std::cout<<"Observer_type_b ["<<name<<"] : got type_b message : "<<m.get_string_value()<<std::endl;}
};

/**
* Stitch all pieces together.
*/

int main(int argc, char ** argv)
{
Observer_type_a o_a1(1);
Observer_type_a o_a2(2);
Observer_type_b o_b1("Sauron");
Observer_type_b o_b2("Roverandom");

Subject_derived s_a;

s_a.register_observer(&o_a1);
s_a.register_observer(&o_b1);

s_a.emit_message_a(23);
s_a.emit_message_b("this is my content");

s_a.register_observer(&o_a2);
s_a.register_observer(&o_b2);

s_a.emit_message_a(99);
s_a.emit_message_b("this is my second content");

//gloriously exit.
return 0;
}

Для ясности я изложу свои цели здесь:

  • Иметь возможность отправлять много разных сообщений от субъекта.
  • Специализируйте наблюдателей, чтобы они игнорировали каждое сообщение, не предназначенное для них (было бы еще лучше, если бы их вообще не отправляли, но я знаю, что могу сделать это, регистрируя различные группы наблюдателей).
  • Избегайте RTTI и производных классов.

Здесь возникает мой вопрос: я пропустил более простую реализацию для достижения своих целей?

Важно отметить, что система, в которой будет использоваться эта система, не будет иметь столько наблюдателей и, возможно, менее десяти субъектов, присутствующих одновременно.

2

Решение

Некоторые шаблоны метапрограммирования:

// a bundle of types:
template<class...>struct types{using type=types;};

// a type that does nothing but carry a type around
// without being that type:
template<class T>struct tag{using type=T;};

// a template that undoes the `tag` operation above:
template<class Tag>using type_t=typename Tag::type;

// a shorter way to say `std::integral_constant<size_t, x>`:
template<std::size_t i>struct index:std::integral_constant<std::size_t, i>{};

Получить индекс типа в types<...>:

// this code takes a type T, and a types<...> and returns
// the index of the type in there.
// index_of
namespace details {
template<class T, class Types>
struct index_of{};
}
template<class T, class Types>
using index_of_t=type_t<details::index_of<T,Types>>;
namespace details {
// if the first entry in the list of types is T,
// our value is 0
template<class T, class...Ts>struct index_of<T, types<T,Ts...>>:
tag< index<0> >
{};
// otherwise, it is 1 plus our value on the tail of the list:
template<class T, class T0, class...Ts>
struct index_of<T, types<T0, Ts...>>:
tag< index< index_of_t<T,types<Ts...>{}+1 > >
{};
}

Вот один «канал» вещатель (он отправляет один вид сообщения):

// a token is a shared pointer to anything
// below, it tends to be a shared pointer to a std::function
// all we care about is the lifetime, however:
using token = std::shared_ptr<void>;
template<class M>
struct broadcaster {
// f is the type of something that can eat our message:
using f = std::function< void(M) >;
// we keep a vector of weak pointers to people who can eat
// our message.  This lets them manage lifetime independently:
std::vector<std::weak_ptr<f>> listeners;

// reg is register.  You pass in a function to eat the message
// it returns a token.  So long as the token, or a copy of it,
// survives, broadcaster will continue to send stuff at the
// function you pass in:
token reg( f target ) {
// if thread safe, (write)lock here
auto sp = std::make_shared<f>(std::move(target));
listeners.push_back( sp );
return sp;
// unlock here
}
// removes dead listeners:
void trim() {
// if thread safe, (try write)lock here
// and/or have trim take a lock as an argument
listeners.erase(
std::remove_if( begin(listeners), end(listeners), [](auto&& p){
return p.expired();
} ),
listeners.end()
);
// unlock here
}
// Sends a message M m to every listener who is not dead:
void send( M m ) {
trim(); // remove dead listeners
// (read) lock here
auto tmp_copy = listeners; // copy the listeners, just in case
// unlock here

for (auto w:tmp_copy) {
auto p = w.lock();
if (p) (*p)(m);
}
}
};

Вот многоканальный subject который может поддерживать любое количество различных типов сообщений (определяется во время компиляции). Если вы не соответствуете типу сообщения, send и / или reg не удастся скомпилировать. Вы несете ответственность за решение, является ли сообщение const& или значение или что-то еще. Пытаясь reg Rvalue сообщение не будет работать. Предполагается, что M передается reg а также send в явном виде.

// fancy wrapper around a tuple of broadcasters:
template<class...Ts>
struct subject {
std::tuple<broadcaster<Ts>...> stations;
// helper function that gets a broadcaster compatible
// with a message type M:
template<class M>
broadcaster<M>& station() {
return std::get< index_of_t<M, types<Ts...>>{} >( stations );
}
// register a message of type M.  You should call with M explicit usually:
template<class M>
token reg( std::function<void(M)> listener ) {
return station<M>().reg(std::move(listener));
}
// send a message of type M.  You should explicitly pass M usually:
template<class M>
void send( M m ) {
station<M>().send(std::forward<M>(m));
}
};

живой пример.

Когда ты reg, он возвращает tokenака std::shared_ptr<void>, Пока этот токен (или копия) сохраняется, сообщения будут передаваться. Если это уходит, сообщения к reged callback закончатся. Как правило, это означает, что слушатели должны поддерживать std::vector<token>и рег лямбды, которые используют this волей-неволей.

В C ++ 14 / 1z вышеупомянутое становится немного лучше (мы можем покончить с types<...> а также index_of для одного).

Если вы добавите прослушиватель во время цикла вещания, он не будет отправлен. Если вы удалите слушателя во время цикла вещания, он не будет отправлен после точки, в которую вы его удалили.

Потокобезопасные комментарии настроены для блокировки чтения / записи на вещателе.

Память, выделенная для мертвых слушателей для данного вещателя, восстанавливается, когда trim или же send называется. Тем не менее std::function будет уничтожен очень давно, так что до следующего send, Я делаю это тогда, потому что мы собираемся перебирать список сообщений, так или иначе, может сначала убрать любой беспорядок.

Это решение не имеет RTTI или динамического приведения, а сообщения отправляются только слушателям, которые их понимают.


В все становится проще. Удалите весь шаблон метапрограммирования, удалите subject (держать broadcaster) и просто сделайте это для обработки более одного канала:

template<class...Ms>
struct broadcasters : broadcaster<Ms>... {
using broadcaster<Ms>::reg...;
using broadcaster<Ms>::send...;

template<class M>
broadcaster<M>& station() { return *this; }
};

этот broadcasters в настоящее время почти полное улучшение subject выше.

Из-за улучшений в std::function поскольку , reg Функция обычно делает правильные вещи, если параметры сигнала не слишком похожи. Если у вас возникнут проблемы с reg или же sendВы вынуждены звонить .station<type>().reg(blah),

Но 99/100 раз вы можете просто сделать .reg( lambda ) а также .send( msg ) и разрешение перегрузки делает правильную вещь.

Живой пример.

А вот и вся система, дополненная модульной системой обеспечения безопасности нитей:

struct not_thread_safe {
struct not_lock {~not_lock(){}};
auto lock() const { return not_lock{}; }
};
struct mutex_thread_safe {
auto lock() const { return std::unique_lock<std::mutex>(m); }
private:
mutable std::mutex m;
};
struct rw_thread_safe {
auto lock() { return std::unique_lock<std::shared_timed_mutex>(m); }
auto lock() const { return std::shared_lock<std::shared_timed_mutex>(m); }
private:
mutable std::shared_timed_mutex m;
};
template<class D, class>
struct derived_ts {
auto lock() { return static_cast<D*>(this)->lock(); }
auto lock() const { return static_cast<D const*>(this)->lock(); }
};
using token = std::shared_ptr<void>;
template<class M, class TS=not_thread_safe>

struct broadcaster:
TS
{
using f = std::function< void(M) >;
mutable std::vector<std::weak_ptr<f>> listeners;
token reg( f target )
{
auto l = this->lock();
auto sp = std::make_shared<f>(std::move(target));
listeners.push_back( sp );
return sp;
}
// logically const, but not really:
void trim() const {
auto l = const_cast<broadcaster&>(*this).lock();
auto it = std::remove_if( listeners.begin(), listeners.end(), [](auto&& p){
return p.expired();
} );
listeners.erase( it, listeners.end() );
}
// logically const, but not really:
void send( M m ) const
{
trim(); // remove dead listeners
auto tmp_copy = [this]{
auto l = this->lock();
return listeners; // copy the listeners, just in case
}();

for (auto w:tmp_copy) {
auto p = w.lock();
if (p) (*p)(m);
}
}
};
template<class TS, class...Ms>
struct basic_broadcasters :
TS,
broadcaster<Ms, derived_ts<basic_broadcasters<TS, Ms...>, Ms> >...
{
using TS::lock;
using broadcaster<Ms, derived_ts<basic_broadcasters<TS, Ms...>, Ms> >::reg...;
using broadcaster<Ms, derived_ts<basic_broadcasters<TS, Ms...>, Ms> >::send...;

template<class M>
broadcaster<M, derived_ts<basic_broadcasters<TS, Ms...>, M>>& station() { return *this; }
template<class M>
broadcaster<M, derived_ts<basic_broadcasters<TS, Ms...>, M>> const& station() const { return *this; }
};
template<class...Ms>
using broadcasters = basic_broadcasters<rw_thread_safe, Ms...>;

Живой пример.

broadcasters<Messages...> теперь является широковещательным классом с блокировкой чтения и записи, который использует 1 общую разделяемую блокировку для синхронизации каждой очереди широковещания.

basic_broadcasters<not_thread_safe, Messages...> вместо этого создает один без блокировки (то есть не является потокобезопасным).

3

Другие решения

Я думаю, что вы должны придерживаться того, что будет проще. Если все ваши наблюдатели обрабатывают все сообщения, то у вас должен быть один тип наблюдателя. Если сообщения не связаны, каждый наблюдатель должен следить только за сообщениями, которые он обрабатывает.

Решение с использованием Boost :: Signal2 будет:

#include <string>
#include <cstdio>
#include <iostream>
#include <functional>
#include <boost/signals2/signal.hpp>

class Subject
{
public:
void emit_message_a(int v) {
sig_a(v);
}

void emit_message_b(const std::string v) {
sig_b(v);
}

template<typename F>
void register_listener_a(const F &listener)
{
sig_a.connect(listener);
}

template<typename F>
void register_listener_b(const F &listener)
{
sig_b.connect(listener);
}

private:
boost::signals2::signal<void (int)> sig_a;
boost::signals2::signal<void (std::string)> sig_b;
};

class Observer
{
public:
Observer():
name("John")
{}

void observe(int v) {
std::cout << name << " has observed phoenomenon int: " << v << std::endl;
}

void observe(std::string v) {
std::cout << name << " has observed phoenomenon string: " << v << std::endl;
}

private:
std::string name;
};

int main()
{
Subject s;
Observer o;

s.register_listener_a([&](int v){o.observe(v);});
s.register_listener_b([&](std::string v){o.observe(v);});s.register_listener_a([](int val) {
std::cout << "Received message a : " << val << std::endl;
});
s.register_listener_a([](int message_a) {
printf("I have received message a, too! It is %d.\n", message_a);
});

s.register_listener_b([](std::string msg) {
std::cout << "A B type message was received! Help!\n";
});

s.emit_message_a(42);

s.emit_message_b("a string");

s.emit_message_a(-1);

s.emit_message_b("another string");
}

Запустив его, я получаю:

John has observed phoenomenon int: 42
Received message a : 42
I have received message a, too! It is 42.
John has observed phoenomenon string: a string
A B type message was received! Help!
John has observed phoenomenon int: -1
Received message a : -1
I have received message a, too! It is -1.
John has observed phoenomenon string: another string
A B type message was received! Help!

Если вы собираетесь использовать его, обязательно прочитайте руководство.

1

По вопросам рекламы [email protected]