Я создаю компонентно-ориентированную систему для небольшой игры, которую разрабатываю. Основная структура выглядит следующим образом: каждый объект в игре состоит из «GameEntity»; контейнер, содержащий вектор указателей на элементы в классе «Компонент».
Компоненты и объекты взаимодействуют друг с другом, вызывая метод send в родительском классе GameEntity компонента. Метод send является шаблоном, который имеет два параметра, Command (который является перечислением, которое включает в себя такие инструкции, как STEP_TIME и тому подобное), и параметр данных общего типа ‘T’. Функция send проходит по вектору Component * и вызывает сообщение о получении каждого компонента, которое благодаря использованию шаблона удобно вызывает перегруженный метод приема, который соответствует типу данных T.
Однако здесь возникает проблема (или, скорее, неудобство), заключающаяся в том, что класс Component является чисто виртуальной функцией и всегда будет расширяться. Из-за практического ограничения запрета виртуализации шаблонных функций мне пришлось бы объявлять виртуальную функцию приема в заголовке для каждого типа данных, который мог бы использоваться компонентом. Это не очень гибко и не расширяемо, и, более того, по крайней мере, мне кажется, это нарушение принципа ОО программирования, заключающегося в том, чтобы не дублировать код без необходимости.
Поэтому мой вопрос заключается в том, как я могу изменить заглушки кода, представленные ниже, чтобы сделать мою объектно-ориентированную структуру объекта максимально гибкой, без использования метода, который нарушает лучшие практики кодирования
Вот соответствующие заглушки каждого класса и пример того, как можно использовать расширенный класс компонентов, чтобы обеспечить некоторый контекст для моей проблемы:
Класс игры Entity:
class Component;
class GameEntity
{
public:
GameEntity(string entityName, int entityID, int layer);
~GameEntity(void){};
//Adds a pointer to a component to the components vector.
void addComponent (Component* component);
void removeComponent(Component*);
//A template to allow values of any type to be passed to components
template<typename T>
void send(Component::Command command,T value){
//Iterates through the vector, calling the receive method for each component
for(std::vector<Component*>::iterator it =components.begin(); it!=components.end();it++){
(*it)->receive(command,value);
}
}
private:
vector <Component*> components;
};
Класс компонента:
#include Компонент класса «GameEntity.h»
{
public:
static enum Command{STEP_TIME, TOGGLE_ANTI_ALIAS, REPLACE_SPRITE};
Component(GameEntity* parent)
{this->compParent=parent;};
virtual ~Component (void){};
GameEntity* parent(){
return compParent;
}
void setParent(GameEntity* parent){
this->compParent=parent;
}
virtual void receive(Command command,int value)=0;
virtual void receive(Command command,string value)=0;
virtual void receive(Command command,double value)=0;
virtual void receive(Command command,Sprite value)=0;
//ETC. For each and every data typeprivate:
GameEntity* compParent;
};
Возможное расширение класса Component:
#include "Sprite.h"#include "Component.h"class GraphicsComponent: Component{
public:
GraphicsComponent(Sprite sprite, string name, GameEntity* parent);
virtual void receive(Command command, Sprite value){
switch(command){
case REPLACE_SPRITE: this->sprite=value; break
}
}
private:
Spite sprite;}
Должен ли я использовать нулевой указатель и привести его к соответствующему типу? Это может быть осуществимо, так как в большинстве случаев тип будет известен из команды, но опять-таки не очень гибок.
Это идеальный случай для стирания типа!
Когда базовое программирование на основе шаблонов и объектно-ориентированное программирование сталкиваются, вы сталкиваетесь с простой, но трудной для решения проблемой: как безопасно хранить переменную, в которой мне все равно тип но вместо этого заботиться о как я могу это использовать? Общее программирование ведет к взрыву информации о типах, в то время как объектно-ориентированное программирование зависит от очень специфических типов. Что делать программисту?
В этом случае самое простое решение — это своего рода контейнер, который имеет фиксированный размер, может хранить любую переменную и БЕЗОПАСНО извлекать ее / запрашивать ее тип. К счастью, Boost имеет такой тип: повышение :: любой.
Теперь вам нужна только одна виртуальная функция:
virtual void receive(Command command,boost::any val)=0;
Каждый компонент «знает», что он был отправлен, и, таким образом, может извлечь значение следующим образом:
virtual void receive(Command command, boost::any val)
{
// I take an int!
int foo = any_cast<int>(val);
}
Это либо успешно преобразует int, либо выдает исключение. Не любите исключения? Сначала сделайте тест:
virtual void receive(Command command, boost::any val)
{
// Am I an int?
if( val.type() == typeid(int) )
{
int foo = any_cast<int>(val);
}
}
«Но о!» Вы можете сказать, стремясь не любить это решение: «Я хочу отправить более одного параметра!»
virtual void receive(Command command, boost::any val)
{
if( val.type() == typeid(std::tuple<double, char, std::string>) )
{
auto foo = any_cast< std::tuple<double, char, std::string> >(val);
}
}
«Хорошо», вы могли бы подумать: «Как мне разрешить передачу произвольных типов, например, если я хочу, чтобы float один раз, а int в другой?» И к этому, сэр, вы будете избиты, потому что это плохая идея. Вместо этого свяжите две точки входа в один и тот же внутренний объект:
// Inside Object A
virtual void receive(Command command, boost::any val)
{
if( val.type() == typeid(std::tuple<double, char, std::string>) )
{
auto foo = any_cast< std::tuple<double, char, std::string> >(val);
this->internalObject->CallWithDoubleCharString(foo);
}
}
// Inside Object B
virtual void receive(Command command, boost::any val)
{
if( val.type() == typeid(std::tuple<float, customtype, std::string>) )
{
auto foo = any_cast< std::tuple<float, customtype, std::string> >(val);
this->internalObject->CallWithFloatAndStuff(foo);
}
}
И там у вас есть это. Удаляя надоедливую «интересную» часть типа с помощью boost :: any, мы теперь можем безопасно и надежно передавать аргументы.
Для получения дополнительной информации об удалении типов и о преимуществах стирания частей типа на объектах, которые вам не нужны, чтобы они лучше сочетались с общим программированием, см. Эта статья
Другая идея, если вы любите манипуляции со строками, это:
// Inside Object A
virtual void receive(Command command, unsigned int argc, std::string argv)
{
// Use [boost::program_options][2] or similar to break argv into argc arguments
// Left as exercise for the reader
}
Это имеет любопытную элегантность; программы разбирают свои параметры таким же образом, чтобы вы могли представить концепцию обмена данными как «подпрограммы», которые затем открывают целый набор метафор и такие, которые могут привести к интересным оптимизациям, таким как выделение фрагментов данных обмен сообщениями и т. д.
Однако стоимость высока: строковые операции могут быть довольно дорогими по сравнению с простым приведением. Также обратите внимание, что boost :: any не имеет нулевой стоимости; Каждый any_cast требует RTTI-поиска, по сравнению с нулевым поиском, необходимым только для передачи фиксированного количества параметров. Гибкость и косвенность требуют затрат; однако в этом случае это более чем стоит.
Если вы хотите вообще избежать таких затрат, есть одна возможность, которая обеспечивает необходимую гибкость, а также отсутствие зависимостей и, возможно, даже более приемлемый синтаксис. Но хотя это стандартная функция, она может быть небезопасной:
// Inside Object A
virtual void receive(Command command, unsigned int argc, ...)
{
va_list args;
va_start ( args, argc );
your_type var = va_arg ( args, your_type );
// etc
va_end( args );
}
Функция переменных аргументов, используемая, например, в printf, позволяет передавать произвольно много аргументов; очевидно, вам нужно будет сообщить функции вызываемого абонента, сколько аргументов передано, так что это предоставляется через argc. Имейте в виду, однако, что функция вызываемого не может определить, были ли переданы правильные параметры; он с радостью возьмет все, что вы дадите, и интерпретирует, как если бы это было правильно. Поэтому, если вы случайно передадите неверную информацию, вам не будет оказана поддержка во время компиляции, которая поможет вам выяснить, что идет не так. Мусор на входе, Мусор на выходе.
Кроме того, есть множество вещей, которые нужно помнить относительно va_list, например, все числа с плавающей запятой преобразуются с удвоением, структуры передаются указателями (я думаю), но если ваш код правильный и точный, проблем не будет, и у вас будет эффективность отсутствие зависимостей и простота использования. Я бы порекомендовал, для большинства случаев, обернуть va_list и тому подобное в макрос:
#define GET_DATAMESSAGE_ONE(ret, type) \
do { va_list args; va_start(args,argc); ret = va_args(args,type); } \
while(0)
И затем версия для двух аргументов, затем один для трех. К сожалению, шаблон или встроенное решение не могут быть использованы здесь, но большинство пакетов данных будут иметь не более 1-5 параметров, и большинство из них будут примитивами (почти наверняка, хотя ваш вариант использования может отличаться), поэтому разработка Несколько уродливых макросов, которые могут помочь вашим пользователям, будут иметь дело в основном с аспектом безопасности.
Я не рекомендую эту тактику, но она вполне может быть самой быстрой и простой тактикой на некоторых платформах, например, таких, которые не позволяют даже компилировать временные зависимости или встроенные системы, где виртуальные вызовы могут быть запрещены.
Других решений пока нет …