Пишу приложение, работающее по простенькому пакетному протоколу.
Общий цикл работы приложения такой:
Принять пакет
Распарсить пакет
Если пакет удовлетворяет условию A_i, то вызвать функцию F_i где i = 1...N.
Проблема в том, что функции F_i принимают разные наборы параметров, разные как по количеству так и по типам данных. В качестве параметров могут выступать как поля полученного пакета (передаются как rvalue, то есть не должны меняться в F_i), так и переменные, живущие на протяжении всего цикла работы (свойства соединения, счётчик пакетов, некое состояние протокола и т. п., эти вещи могут передаваться как lvalue и меняться внутри F_i).
Код сейчас выглядит примерно так:
struct BasePacket
{
virtual void F(Connection &, Database &) const = 0; // большой список параметров
};
struct PingPacket: public BasePacket
{
virtual void F(Connection &conn, Database & /*unused parameter*/) const // лишние параметры
{
...
conn.send_data(int_field+1);
++conn._packet_counter; // нужно допускать PingPacket к внутренностям Connection
...
}
int int_field;
};
struct PongPacket: public BasePacket
{
virtual void F(Connection & /*unused parameter*/, Database &db) const // лишние параметры
{
...
db.save(float_field);
conn._timer.stop(); // нужно допускать PingPacket к внутренностям Connection
...
}
float float_field;
};
Connection::main_cycle(Database &db)
{
while (1)
{
BasePacket *packet = receive_packet(); /// Здесь фабрика: получаем пакет, парсим, в зависимости от того,
/// что получили, создаём того или иного потомка BasePacket.
packet->F(*this, db); /// Передаём всё окружение, необходимое исполнения логики протокола.
}
}
Ситуация дурацкая. По идее наследники BasePacket это не более чем хранилища распарсенных данных из сетевых пакетов. Но получается, что по их виртуальным методы распихивается логика протокола (мне же не хочется писать огромный switch по типам пакетов). Протокол stateful, так что всем этим методам нужен доступ к общим данным (Connection, Database, Counter, Cache, Query, etc). В принципе все эти данные можно уложить в Connection, но тогда получится, что методы пакетов работают с данными соединения, что нарушает инкапсуляцию. По хорошему бы эти методы должны быть членами Connection, а к данным из соответствующего пакета доступаться через геттеры (которые всё равно есть). Но как тогда реализовать полиморфный вызов этих методов? Таки очень не хочется писать огромный switch по типам пакетов.
Подозреваю, что то, что мне нужно, называется мультиметодами. Чтобы работало вот так:
Connection::handle_packet(PingPacket *packet)
{
send_data(packet.get_int_field()+1);
++packet_counter; // работаем со своими приватными переменными
}
Connection::handle_packet(PongPacket *packet)
{
db.save(packet.get_float_field());
_timer.stop(); // работаем со своими приватными переменными
}
Connection::main_cycle(Database &db)
{
...
BasePacket *packet = receive_packet(); /// Здесь фабрика: ...
handle_packet(packet); //здесь полиморфный вызов по формальному параметру, а не по this.
...
}
Можно ли здесь обойтись без опасных хаков и жуткого синтаксиса? Может ещё как-нибудь архитектуру перевернуть?