Как написано, это «пахнет», но это могут быть только те примеры, которые вы привели. Хранение данных в контейнерах универсальных объектов, а затем их преобразование для получения доступа к данным не является запахом кода. Вы увидите, что он используется во многих ситуациях. Однако, когда вы используете его, вы должны знать, что вы делаете, как вы это делаете и почему. Когда я смотрю на пример, использование сравнения на основе строк говорит мне, какой объект является тем, что является предметом, который срабатывает в моем личном измерителе запаха. Это говорит о том, что вы не совсем уверены в том, что вы здесь делаете (и это хорошо, поскольку у вас хватило мудрости прийти сюда к программистам. SE и сказать: «Эй, мне не нравится то, что я делаю, помогите меня нет! ").
Фундаментальная проблема с шаблоном приведения данных из общих контейнеров, как это, заключается в том, что производитель данных и потребитель данных должны работать вместе, но это может быть неочевидно, что они делают это на первый взгляд. В каждом примере этой модели, вонючей или не вонючей, это фундаментальная проблема. Это очень возможно для следующего разработчика , чтобы быть совершенно не знают , что вы делаете этот шаблон , и разорвать его случайно, поэтому , если вы используете этот шаблон вы должны позаботиться , чтобы помочь следующему отказу разработчиков. Вы должны упростить ему непреднамеренное нарушение кода из-за некоторых деталей, о которых он, возможно, не знал.
Например, что если я захочу скопировать плеер? Если я просто смотрю на содержимое объекта проигрывателя, это выглядит довольно просто. Мне просто нужно скопировать attack
, defense
и tools
переменные. Проще простого! Что ж, я быстро выясню, что использование указателей делает его немного сложнее (в какой-то момент стоит взглянуть на умные указатели, но это уже другая тема). Это легко решается. Я просто создаю новые копии каждого инструмента и помещаю их в свой новый tools
список. В конце концов, Tool
это действительно простой класс с одним членом. Поэтому я создал кучу копий, включая копию Sword
, но я не знал, что это меч, поэтому я только скопировал name
. Позже, attack()
функция смотрит на имя, видит, что это «меч», забрасывает его, и случаются плохие вещи!
Мы можем сравнить этот случай с другим случаем в программировании сокетов, в котором используется тот же шаблон. Я могу настроить функцию сокета UNIX следующим образом:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(portno);
serv_addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr));
Почему это тот же шаблон? Потому bind
что не принимает sockaddr_in*
, он принимает более общий sockaddr*
. Если вы посмотрите на определения для этих классов, мы увидим, что у нас sockaddr
есть только один член семьи, который мы присвоили sin_family
*. Семья говорит, к какому подтипу следует применить sockaddr
. AF_INET
говорит вам , что адрес структура на самом деле sockaddr_in
. Если бы это было AF_INET6
, адрес был бы sockaddr_in6
, который имеет большие поля для поддержки больших адресов IPv6.
Это идентично вашему Tool
примеру, за исключением того, что оно использует целое число, чтобы указать, какое семейство, а не std::string
. Тем не менее, я собираюсь утверждать, что он не пахнет, и постараюсь сделать это по причинам, отличным от «это стандартный способ сделать сокеты, поэтому он не должен« пахнуть ». Очевидно, это тот же шаблон, который почему я утверждаю, что хранение данных в универсальных объектах и их преобразование не являются автоматически запахом кода, но есть некоторые различия в том, как они это делают, что делает их более безопасными.
При использовании этого шаблона наиболее важной информацией является передача информации о подклассе от производителя к потребителю. Это то, что вы делаете с name
полем, а сокеты UNIX делают с их sin_family
полем. Это поле - информация, которая нужна потребителю, чтобы понять, что на самом деле создал производитель. Во всех случаях этого шаблона это должно быть перечисление (или, по крайней мере, целое число, действующее как перечисление). Зачем? Подумайте, что ваш потребитель собирается делать с информацией. Им нужно будет выписать некоторые большиеif
заявление илиswitch
оператор, как вы сделали, где они определяют правильный подтип, приводят его и используют данные. По определению, может быть только небольшое количество этих типов. Вы можете хранить его в виде строки, как и раньше, но у этого есть множество недостатков:
- Медленно -
std::string
обычно требуется некоторая динамическая память, чтобы сохранить строку. Вы также должны делать полнотекстовое сравнение, чтобы соответствовать имени каждый раз, когда вы хотите выяснить, какой у вас подкласс.
- Слишком разносторонний - нужно сказать, что нужно ограничивать себя, когда вы делаете что-то чрезвычайно опасное. У меня были такие системы, которые искали подстроку, чтобы сказать, на какой тип объекта он смотрит. Это прекрасно работало до тех пор, пока имя объекта случайно не содержало эту подстроку, и не создало ужасно загадочную ошибку. Поскольку, как мы указывали выше, нам нужно лишь небольшое количество случаев, нет никаких причин использовать инструмент с огромным количеством мощностей, такой как строки. Это ведет к...
- Склонность к ошибкам. Давайте просто скажем, что вы захотите совершить убийственное неистовство, пытаясь выяснить, почему ничего не работает, когда один потребитель случайно установил имя волшебной ткани
MagicC1oth
. Серьезно, на такие ошибки могут уйти дни, пока они не поняли, что произошло.
Перечисление работает намного лучше. Это быстро, дешево и намного менее подвержено ошибкам:
class Tool {
public:
enum TypeE {
kSword,
kShield,
kMagicCloth
};
TypeE type;
std::string typeName() const {
switch(type) {
case kSword: return "Sword";
case kSheild: return "Sheild";
case kMagicCloth: return "Magic Cloth";
default:
throw std::runtime_error("Invalid enum!");
}
}
};
Этот пример также демонстрирует switch
оператор, включающий перечисления, с единственной наиболее важной частью этого шаблона: default
регистр, который выбрасывает. Вы никогда не должны попадать в такую ситуацию, если вы все делаете идеально. Однако, если кто-то добавит новый тип инструмента, и вы забудете обновить свой код для его поддержки, вы захотите, чтобы что-то перехватило ошибку. На самом деле, я так рекомендую их, что вы должны добавить их, даже если они вам не нужны.
Другим огромным преимуществом enum
является то, что он дает следующему разработчику полный список допустимых типов инструментов, сразу же. Нет необходимости изучать код, чтобы найти специализированный класс Флейты Боба, который он использует в своей эпической битве с боссом.
void damageWargear(Tool* tool)
{
switch(tool->type)
{
case Tool::kSword:
static_cast<Sword*>(tool)->damageSword();
break;
case Tool::kShield:
static_cast<Sword*>(tool)->damageShield();
break;
default:
break; // Ignore all other objects
}
}
Да, я вставил «пустую» инструкцию по умолчанию, просто чтобы ясно указать следующему разработчику, что я ожидаю, если какой-то новый неожиданный тип появится у меня на пути.
Если вы сделаете это, шаблон будет меньше пахнуть. Однако, чтобы быть без запаха, последнее, что вам нужно сделать, это рассмотреть другие варианты. Эти приведения являются одними из наиболее мощных и опасных инструментов, которые есть в репертуаре C ++. Вы не должны использовать их, если у вас нет веских причин.
Одна очень популярная альтернатива - это то, что я называю «объединенная структура» или «объединенный класс». Для вашего примера это на самом деле очень хорошо подходит. Чтобы создать один из них, вы создаете Tool
класс с перечислением, как раньше, но вместо того , чтобы создавать подклассы Tool
, мы просто помещаем в него все поля из каждого подтипа.
class Tool {
public:
enum TypeE {
kSword,
kShield,
kMagicCloth
};
TypeE type;
int attack;
int defense;
};
Теперь вам не нужны подклассы вообще. Вам просто нужно посмотреть на type
поле, чтобы увидеть, какие другие поля действительно действительны. Это намного безопаснее и проще для понимания. Однако у него есть недостатки. Есть моменты, когда вы не хотите использовать это:
- Когда объекты слишком разные - вы можете получить список полей, и может быть неясно, какие из них применимы к каждому типу объектов.
- При работе в критической ситуации с памятью - если вам нужно сделать 10 инструментов, вы можете лениться с памятью. Когда вам нужно сделать 500 миллионов инструментов, вы начнете заботиться о битах и байтах. Профсоюзные структуры всегда больше, чем они должны быть.
Это решение не используется сокетами UNIX из-за различий, усугубляемых открытостью API. Цель сокетов UNIX состояла в том, чтобы создать что-то, с чем мог бы работать любой вариант UNIX. Каждый вариант может определять список поддерживаемых семейств, например AF_INET
, и для каждого будет краткий список. Однако, если появляется новый протокол, как, например, AF_INET6
сделал, вам может понадобиться добавить новые поля. Если бы вы сделали это с объединенной структурой, вы бы в итоге эффективно создали новую версию структуры с тем же именем, создавая бесконечные проблемы несовместимости. Вот почему сокеты UNIX решили использовать шаблон приведения, а не структуру объединения. Я уверен, что они рассмотрели это, и факт, что они думали об этом, является частью того, почему это не пахнет, когда они используют это.
Вы также можете использовать союз по-настоящему. Профсоюзы экономят память, так как они больше, чем самый крупный член, но у них есть свои проблемы. Это, вероятно, не вариант для вашего кода, но всегда следует учитывать.
Еще одно интересное решение boost::variant
. Boost - это отличная библиотека, полная многоразовых кроссплатформенных решений. Это, вероятно, один из лучших кодов C ++, когда-либо написанных. Boost.Variant - это, в основном, версия союзов на C ++. Это контейнер, который может содержать много разных типов, но только по одному за раз. Вы можете сделать свои Sword
, Shield
и MagicCloth
классы, а затем сделать инструмент совсем немного!), Но этот шаблон может быть невероятно полезным. Вариант часто используется, например, в деревьях разбора, которые берут строку текста и разбивают ее, используя грамматику для правил.boost::variant<Sword, Shield, MagicCloth>
, то есть он содержит один из этих трех типов. Это все еще страдает от той же самой проблемы с будущей совместимостью, которая препятствует тому, чтобы сокеты UNIX использовали это (не говоря уже о сокетах UNIX, C, предшествующийboost
Окончательное решение, которое я бы посоветовал рассмотреть, прежде чем окунуться и использовать универсальный подход приведения объектов, - это шаблон проектирования Visitor . Visitor - это мощный шаблон проектирования, использующий преимущество наблюдения, заключающегося в том, что вызов виртуальной функции эффективно выполняет кастинг, который вам нужен, и делает это за вас. Поскольку компилятор делает это, он никогда не может ошибаться. Таким образом, вместо хранения перечисления Visitor использует абстрактный базовый класс, который имеет виртуальную таблицу, которая знает, какой тип объекта. Затем мы создаем аккуратный маленький вызов двойного косвенного действия, который выполняет работу:
class Tool;
class Sword;
class Shield;
class MagicCloth;
class ToolVisitor {
public:
virtual void visit(Sword* sword) = 0;
virtual void visit(Shield* shield) = 0;
virtual void visit(MagicCloth* cloth) = 0;
};
class Tool {
public:
virtual void accept(ToolVisitor& visitor) = 0;
};
lass Sword : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int attack;
};
class Shield : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int defense;
};
class MagicCloth : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int attack;
int defense;
};
Так что же это за ужасная картина? Ну, Tool
есть виртуальная функция accept
. Если вы передадите ему посетителя, ожидается, что он развернется и вызовет правильную visit
функцию для этого посетителя для типа. Это то, что visitor.visit(*this);
делает для каждого подтипа. Сложно, но мы можем показать это на вашем примере выше:
class AttackVisitor : public ToolVisitor
{
public:
int& currentAttack;
int& currentDefense;
AttackVisitor(int& currentAttack_, int& currentDefense_)
: currentAttack(currentAttack_)
, currentDefense(currentDefense_)
{ }
virtual void visit(Sword* sword)
{
currentAttack += sword->attack;
}
virtual void visit(Shield* shield)
{
currentDefense += shield->defense;
}
virtual void visit(MagicCloth* cloth)
{
currentAttack += cloth->attack;
currentDefense += cloth->defense;
}
};
void Player::attack()
{
int currentAttack = this->attack;
int currentDefense = this->defense;
AttackVisitor v(currentAttack, currentDefense);
for (Tool* t: tools) {
t->accept(v);
}
//some other functions to start attack
}
Так что здесь происходит? Мы создаем посетителя, который будет выполнять некоторую работу за нас, как только он узнает, какой тип объекта он посещает. Затем мы перебираем список инструментов. Ради аргумента, скажем, первый объект - это a Shield
, но наш код этого еще не знает. Это звонки t->accept(v)
, виртуальная функция. Поскольку первый объект - это щит, он в конечном итоге вызывает void Shield::accept(ToolVisitor& visitor)
, что вызывает visitor.visit(*this);
. Теперь, когда мы ищем, что visit
вызывать, мы уже знаем, что у нас есть Shield (потому что эта функция была вызвана), поэтому мы в конечном итоге вызовем void ToolVisitor::visit(Shield* shield)
нашу AttackVisitor
. Теперь выполняется правильный код для обновления нашей защиты.
Посетитель громоздкий. Это настолько неуклюже, что я почти думаю, что у него есть собственный запах. Писать шаблоны плохих посетителей очень легко. Однако у него есть одно огромное преимущество, которого нет у других. Если мы добавим новый тип инструмента, мы должны добавить новую ToolVisitor::visit
функцию для него. В тот момент, когда мы делаем это, каждый ToolVisitor
в программе откажется компилировать, потому что отсутствует виртуальная функция. Это позволяет очень легко поймать все случаи, когда мы что-то пропустили. Это гораздо сложнее гарантировать, если вы используете if
или switch
заявления, чтобы сделать работу. Эти преимущества достаточно хороши, чтобы Visitor нашел небольшую нишу в генераторах 3D-сцен. Им, скорее всего, нужно именно то поведение, которое предлагает посетитель, поэтому он прекрасно работает!
В общем, помните, что эти шаблоны затрудняют работу следующего разработчика. Потратьте время, чтобы им было легче, и код не пахнет!
* Технически, если вы посмотрите на спецификацию, у sockaddr есть один член sa_family
. Здесь, на уровне Си, делаются некоторые хитрости, которые не имеют значения для нас. Вы можете взглянуть на фактическую реализацию, но для этого ответа я собираюсь использовать sa_family
sin_family
и другие полностью взаимозаменяемо, используя тот, который наиболее интуитивно понятен для прозы, полагая, что этот трюк на Си заботится о неважных деталях.