Як написано, це "пахне", але це можуть бути лише приклади, які ви навели. Зберігання даних у універсальних об'єктових контейнерах, а потім їх передавання для отримання доступу до даних не автоматично пахне кодом. Ви побачите, що він використовується в багатьох ситуаціях. Однак, використовуючи його, ви повинні знати про те, що ви робите, як це робите і чому. Коли я дивлюся на приклад, використання струнних порівнянь, щоб сказати мені, що є предметом, що є тим, що відключає мій особистий лічильник запаху. Це говорить про те, що ти не зовсім впевнений, що ти тут робиш (це прекрасно, оскільки ти мав мудрість прийти сюди до програмістів. СЕ і сказати "ей, я не думаю, що мені подобається те, що я роблю, допоможи мене виходять! »).
Принциповим питанням, що стосується схеми передачі даних із загальних контейнерів, як це, є те, що виробник даних та споживач цих даних повинні працювати разом, але може бути не очевидно, що вони роблять це на перший погляд. У кожному прикладі цієї закономірності, смердючої чи не смердючої, це основне питання. Це дуже можливо для наступного розробника , щоб бути абсолютно не знають , що ви робите цей шаблон , і розірвати його випадково, тому , якщо ви використовуєте цей шаблон ви повинні подбати , щоб допомогти наступному відмови розробників. Ви повинні полегшити йому ненавмисне порушення коду через деякі деталі, які він може не знати, що вони існували.
Наприклад, що робити, якщо я хотів скопіювати програвач? Якщо я просто дивлюся на вміст об’єкта програвача, це виглядає досить легко. Мені просто потрібно скопіювати 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
, адреса була б a 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
класи, а потім зробити інструмент "a" boost::variant<Sword, Shield, MagicCloth>
, тобто він містить один із цих трьох типів. Це все ще страждає від тієї ж проблеми з майбутнім сумісністю, яка не дозволяє UNIX-сокетам використовувати його (не кажучи вже про UNIX-сокети C, що передуєboost
зовсім небагато!), але цей візерунок може бути неймовірно корисним. Варіант часто використовується, наприклад, у деревах розбору, які беруть рядок тексту та розбивають його, використовуючи граматику для правил.
Остаточне рішення, яке я рекомендую переглянути, перш ніж зануритися і скористатися загальним підходом до кастингу об'єктів, є схема дизайну Visitor . Відвідувач - це потужна модель дизайну, яка користується перевагою зауваження, що виклик віртуальної функції ефективно робить потрібне вам кастинг, і це робить це за вас. Оскільки компілятор це робить, він ніколи не може помилитися. Таким чином, замість того, щоб зберігати перерахунок, Visitor використовує абстрактний базовий клас, у якому є vtable, який знає, який тип об'єкта. Потім ми створюємо акуратний маленький дзвінок з двосторонньою діяльністю, який виконує роботу:
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
}
То що ж тут відбувається? Ми створюємо відвідувача, який зробить певну роботу для нас, як тільки він дізнається, який тип об’єкта він відвідує. Потім ми повторюємо список інструментів. Для аргументу, скажімо, перший об’єкт - це Shield
, але наш код ще цього не знає. Це дзвінки t->accept(v)
, віртуальна функція. Оскільки перший об'єкт - щит, він закінчує дзвінок void Shield::accept(ToolVisitor& visitor)
, який дзвонить visitor.visit(*this);
. Тепер, коли ми шукаємо, куди visit
зателефонувати, ми вже знаємо, що у нас є Щит (тому що ця функція викликалася), тому ми закінчимо дзвонитиvoid ToolVisitor::visit(Shield* shield)
задати на нашому AttackVisitor
. Тепер він працює правильним кодом для оновлення захисту.
Відвідувач об’ємний. Це настільки незграбно, що я майже думаю, що він має власний запах. Дуже легко писати погані моделі відвідувачів. Однак він має одну величезну перевагу, яку ніхто з інших не має. Якщо ми додамо новий тип інструменту, ми повинні додати для нього нову ToolVisitor::visit
функцію. Щойно ми це зробимо, кожен ToolVisitor
в програмі відмовиться від компіляції, оскільки в ньому відсутня віртуальна функція. Це дозволяє дуже легко зловити всі випадки, коли ми щось пропустили. Набагато складніше гарантувати це, якщо ви користуєтесьif
або switch
заяви, щоб виконати роботу. Ці переваги досить хороші тим, що відвідувач знайшов непогану нішу в 3D-генераторах графічних сцен. Вони, як правило, потребують саме такої поведінки, яку пропонує відвідувач, тому це чудово працює!
Загалом, пам’ятайте, що ці зразки ускладнюють роботу наступного розробника. Витратьте час, полегшивши їх, і код не пахне!
* Технічно, якщо ви подивитеся на специфікацію, у sockaddr є один член на ім'я sa_family
. Тут проводяться якісь хитрощі на рівні С, які для нас не мають значення. Ви можете ознайомитись із реальною реалізацією, але для цієї відповіді я буду користуватися sa_family
sin_family
та іншими людьми повністю взаємозамінно, використовуючи той, який є найбільш інтуїтивним для прози, довіряючи, що ця хитрість C піклується про неважливі деталі.