Я работаю над коммуникационной библиотекой C ++, в которой я получаю сериализованные данные от ряда устройств (сетевых сокетов, сетей uart / usb, CAN и LIN). Мне также нужно создать сериализованные данные из моих объектов сообщений.
У меня есть базовый класс с именем MessageBase, из которого в настоящее время у меня есть два производных класса с именами Message и CtrlMessage. Проекту в конечном итоге понадобится еще несколько типов сообщений в будущем, и поэтому я ищу реализацию с использованием шаблона проектирования, который позволяет легко расширяться до новых типов сообщений в будущем.
Моя другая цель, как говорит Скотт Мейес, трудно использовать классы неправильно и легко использовать правильно.
Я начал изучать использование шаблона NVI и использование фабрики C ++ для создания сообщений, однако классу Factory понадобилось бы обрабатывать некоторую десериализацию заголовка, чтобы выяснить, какой тип сообщения находится в полезной нагрузке.
class MessageBase
{
private:
// other public & private methods omitted for brevity
MessageBase &ISerialize( dsStream<byte> &sdata) = 0;
public:
MessageBase &Serialize( dsStream<byte> &sdata)
{
ISerialize(sdata);
}
}
class Message : public MessageBase
{
private:
// other public & private methods omitted for brevity
MessageBase &ISerialize( dsStream<byte> &sdata);
public:
}
class MessageFactory
{
private:
public:
CreateMessageFromStream( dsStream<byte> &RxData)
{
// read N bytes from RxData to determine type and then
// jump into switch to build message
switch(MsgType)
{
case MSG_DATA:
{
Message *pMsg = new Message(RxData);
}
break;
case MSG_CTRL:
{
MessageCtrl *pMsg = new MessageCtrl(RxData);
}
break;
}
}
// I shorten quite a bit of this to, hopefully, give the basic idea.
Другой подход, который я изучал, — «Двойная рассылка», как обрисовано в общих чертах Скоттом Мейерсом № 33 в его книге «Более эффективный C ++». Но это, по-видимому, только смещает проблему: либо требуется, чтобы все производные классы одного уровня знали друг о друге, либо более продвинутое решение с stl map для эмуляции vtable. Этот код выглядит ужасно и трудно следовать.
Я взглянул на шаблон посетителя C ++ и шаблон Builder Creational, и все они требуют, чтобы вызывающая сторона заранее знала, какой тип производного типа Message вы хотите создать.
Я знаю, что мог бы просто поместить большой оператор switch в MessageFactory, как показано, и покончить с этим, но мне нужен способ добавить новые типы сообщений, полученные из MessageBase, и не должен касаться класса MessageFactory. Я не хочу, чтобы другие программисты знали или ходили искать все места, где необходимо обновить код для нового типа сообщения.
Кроме того, это встроенное приложение, и в этом случае некоторые вещи не обсуждаются. Я могу использовать методы программирования шаблонов, но у меня нет библиотеки STL и нет поддержки библиотеки Boost.
Какие-либо предложения?
Я не знаю, стоит ли это того. Но машинное оборудование, чтобы делать то, что вы хотите, может быть написано.
Начнем с MessageBase
, Оно имеет private
конструктор.
Вы тогда говорите это сделать MessageHelper<T>
быть friend
учебный класс.
MessageHelper
выглядит так:
enum MessageType {
TYPE1, // notice no assigment
TYPE2, // values should be consecutive, distinct, and start at `0`
TYPE3, // or things go poorly later on.
NUM_TYPES /* should be last */
};
template<MessageType> struct MessageTag {}; // empty, for overloading
template<MessageType...> struct MessageTags {};
template<MessageType Last, MessageType... List> struct MakeMessageTags:
MakeMessageTags<MessageType(Last-1), MessageType(Last-1), List...>
{};
template<MessageType... List> struct MakeMessageTags<MessageType(0), List...>:
MessageTags<List...>
{};
typedef MessageBase*(*MessageCreatorFunc)(dsStream<byte>&);
// write this somewhere, next to a given type. If you don't, code later will fail to compile
// (yay). You could make a macro to write these:
MessageCreatorFunc MessageCreator( MessageTag<TYPE1> ) {
return []( dsStream<byte>& st )->MessageBase* {
return new MessageType1(st);
};
}
// manual compile time switch:
template<MessageType... List>
MessageBase* CreateMessageFromStream_helper( MessageType idx, dsStream<byte>& st, MessageTags<List...> )
{
static MessageCreatorFunc creator[] = { MessageCreator(MessageTag<List>())... };
return creator[idx]( st );
}
MessageBase* CreateMessageFromStream( dsStream<byte>& st ) {
// stuff, extract MessageType type
MessageBase* msg = CreateMessageFromStream_helper( type, st, MakeMessageTags<MessageType::NUM_TYPES>() );
// continue
}
эффект приведенного выше кода заключается в том, что мы автоматически создаем таблицу переходов вручную для создания наших сообщений.
Если никто не пишет MessageCreator( MessageTag<TYPE> )
перегрузка или она не видна в контексте _helper
вышеуказанное не компилируется. Таким образом, это гарантирует, что если вы добавите новый тип сообщения, вы напишите код создания или нарушите сборку. Гораздо лучше, чем где-то скрытый оператор switch.
В какой-то момент должна быть связь между MessageType
и тип C ++, который должен быть создан: вышеупомянутый механизм просто гарантирует, что если эта связь не установлена, мы получим ошибку компилятора.
Вы можете немного повеселиться и получить лучшее сообщение, вместо того, чтобы перегружать MessageCreator, специализируясь на:
template<MessageType TYPE>
void MessageCreator( MessageTag<TYPE> ) {
static_assert( "You have failed to create a MessageCreator for a type" );
}
// specialization:
template<>
MessageCreatorFunc MessageCreator( MessageTag<TYPE1> ) {
return []( dsStream<byte>& st )->MessageBase* {
return new MessageType1(st);
};
}
который немного более тупой, но может генерировать лучшее сообщение об ошибке. (в то время как template<>
не требуется для всех случаев, так как переопределение также заменяет template
по крайней мере, по стандарту один такая специализация, которая может компилироваться, должна существовать, или программа плохо сформирована, диагноз не требуется (!?)).
Других решений пока нет …