Если у вас нет очень высоких характеристик производительности, я бы использовал формат сообщения с самоописанием. При этом обычно используется общий формат (скажем, ключ = значение), но нет конкретной структуры, вместо этого известные атрибуты описывают тип сообщения, и затем любые другие атрибуты могут быть извлечены из этого сообщения с использованием логики, специфичной для этого типа сообщения.
Я считаю, что этот тип сообщений сохраняет лучшую обратную совместимость - поэтому, если у вас есть новые атрибуты, которые вы хотите добавить, вы можете добавить их, и старые клиенты просто их не увидят. Сообщения, использующие фиксированные структуры, как правило, работают хуже.
РЕДАКТИРОВАТЬ: Дополнительная информация о форматах сообщений с самоописанием. По сути, идея заключается в том, что вы определяете словарь полей - это универсальный набор полей, который содержит ваше общее сообщение. Теперь сообщение по умолчанию должно содержать некоторые обязательные поля, и тогда вам решать, какие другие поля будут добавлены в сообщение. Сериализация / десериализация довольно проста: в итоге вы создаете блоб, в котором есть все поля, которые вы хотите добавить, а на другом конце вы создаете контейнер, который имеет все атрибуты (представьте карту). Обязательные поля могут описывать тип, например, у вас может быть поле в словаре, которое является типом сообщения, и это устанавливается для всех сообщений. Вы запрашиваете это поле, чтобы определить, как обрабатывать это сообщение. Как только вы попадаете в логику обработки, вы просто извлекаете другие необходимые логике атрибуты из контейнера (карты) и обрабатывает их.
Этот подход обеспечивает наилучшую гибкость, позволяет выполнять такие вещи, как только поля передачи, которые действительно изменились. Теперь то, как вы сохраняете это состояние с обеих сторон, зависит от вас - но, учитывая, что у вас есть однозначное соответствие между сообщением и логикой обработки - вам не нужно ни наследование, ни составление. Интеллектуальность системы такого типа проистекает из того, как вы сериализуете поля (и десериализуетесь, чтобы вы знали, какой атрибут в словаре это поле). В качестве примера такого формата рассмотрим протокол FIX - теперь я бы не стал пропагандировать это для игр, но идея должна продемонстрировать, что собой представляет самоописываемое сообщение.
EDIT2: я не могу предоставить полную реализацию, но вот набросок.
Сначала позвольте мне определить тип значения - это типичный тип значений, которые могут существовать для поля:
typedef boost::variant<int32, int64, double, std::string> value_type;
Теперь я опишу поле
struct field
{
int field_key;
value_type field_value;
};
Теперь вот мой контейнер сообщений
struct Message
{
field type;
field size;
container<field> fields; // I use a generic "container", you can use whatever you want (map/vector etc. depending on how you want to handle repeating fields etc.)
};
Теперь предположим, что я хочу создать сообщение, представляющее собой обновление TIME_SYNC
, используйте фабрику, чтобы сгенерировать мне соответствующий скелет
boost::unique_ptr<Message> getTimeSyncMessage()
{
boost::unique_ptr<Message> msg(new Message);
msg->type = { dict::field_type, TIME_SYNC }; // set the type
// set other default attributes for this message type
return msg;
}
Теперь я хочу установить больше атрибутов, и здесь мне нужен словарь поддерживаемых полей, например ...
namespace dict
{
static const int field_type = 1; // message type field id
// fields that you want
static const int field_time = 2;
:
}
Так что теперь я могу сказать,
boost::unique_ptr<Message> msg = getTimeSyncMessage();
msg->setField(field_time, some_value);
msg->setField(field_other, some_other_value);
: // etc.
Теперь сериализация этого сообщения, когда вы готовы к отправке, просто проходит через контейнер и добавляется в BLOB-объект. Вы можете использовать кодировку ASCII или двоичную кодировку (я бы сначала начал с первого, а затем перешел на второе - в зависимости от требований). Таким образом, версия выше в кодировке ASCII может выглядеть примерно так:
1=1|2=10:00:00.000|3=foo
Здесь для аргументов я использую |
для разделения полей, вы можете использовать что-то еще, что, как вы можете гарантировать, не встречается в ваших значениях. С двоичным форматом - это не имеет значения, размер каждого поля может быть встроен в данные.
Десериализация будет проходить через большой двоичный объект, извлекать каждое поле соответствующим образом (например, путем разделения на |
), использовать фабричные методы для создания скелета (как только вы получите тип - поле 1
), затем заполните все атрибуты в контейнере. Позже, когда вы хотите получить определенный атрибут - вы можете сделать что-то вроде:
msg->getField(field_time); // this will return the variant - and you can use boost::get for the specific type.
Я знаю, что это всего лишь набросок, но, надеюсь, он передает идею, лежащую в основе самоописываемого формата.Когда у вас есть основная идея, можно выполнить множество оптимизаций, но это совсем другое дело ...