Например, у меня есть игра, в которой есть несколько инструментов для повышения способности игрока:
Tool.h
class Tool{
public:
std::string name;
};
И некоторые инструменты:
Sword.h
class Sword : public Tool{
public:
Sword(){
this->name="Sword";
}
int attack;
};
Shield.h
class Shield : public Tool{
public:
Shield(){
this->name="Shield";
}
int defense;
};
MagicCloth.h
class MagicCloth : public Tool{
public:
MagicCloth(){
this->name="MagicCloth";
}
int attack;
int defense;
};
И тогда игрок может держать некоторые инструменты для атаки:
class Player{
public:
int attack;
int defense;
vector<Tool*> tools;
void attack(){
//original attack and defense
int currentAttack=this->attack;
int currentDefense=this->defense;
//calculate attack and defense affected by tools
for(Tool* tool : tools){
if(tool->name=="Sword"){
Sword* sword=(Sword*)tool;
currentAttack+=sword->attack;
}else if(tool->name=="Shield"){
Shield* shield=(Shield*)tool;
currentDefense+=shield->defense;
}else if(tool->name=="MagicCloth"){
MagicCloth* magicCloth=(MagicCloth*)tool;
currentAttack+=magicCloth->attack;
currentDefense+=magicCloth->shield;
}
}
//some other functions to start attack
}
};
Я думаю, что это трудно заменить if-else
виртуальными методами в инструментах, потому что каждый инструмент имеет разные свойства, и каждый инструмент влияет на атаку и защиту игрока, для чего необходимо обновить обновление атаки и защиты игрока внутри объекта Player.
Но я не был удовлетворен этим дизайном, так как он содержит уныние, с длинным if-else
заявлением. Нужно ли «исправлять» этот дизайн? Если так, что я могу сделать, чтобы исправить это?
Ответы:
Да, это кодовый запах (во многих случаях).
В вашем примере довольно просто заменить if / else виртуальными методами:
Теперь больше нет необходимости в вашем
if
блоке, вызывающий может просто использовать его какКонечно, для более сложных ситуаций такое решение не всегда столь очевидно (но тем не менее почти всегда возможно). Но если вы попадаете в ситуацию, когда вы не знаете, как решить проблему с помощью виртуальных методов, вы можете снова задать новый вопрос здесь, в разделе «Программисты» (или, если это становится специфичным для языка или реализации, в Stackoverflow).
источник
Sword
этого пути в вашей кодовой базе. Вы можете простоnew Tool("sword", swordAttack, swordDefense)
из, например, файла JSON.Tool
со всеми возможными модификаторами, заполнить некоторыеvector<Tool*>
материалами, считанными из файла данных, а затем просто зациклить их и изменить статистику, как вы делаете сейчас. Однако у вас могут возникнуть проблемы, если вы хотите, чтобы предмет давал, например, 10% бонус за атаку. Возможно, этоtool->modify(playerStats)
другой вариант.Основная проблема с вашим кодом заключается в том, что всякий раз, когда вы вводите какой-либо новый элемент, вам нужно не только писать и обновлять код элемента, но и модифицировать проигрыватель (или везде, где используется элемент), что делает все это намного сложнее.
Как общее практическое правило, я думаю, что это всегда немного подозрительно, когда вы не можете полагаться на нормальное наследование / наследование и вынуждены выполнять пересмотр самостоятельно.
Я мог бы подумать о двух возможных подходах, делающих все это более гибким:
Как уже упоминалось другие, перемещения
attack
иdefense
членов базового класса и просто инициализировать их0
. Это также может быть двойной проверкой, действительно ли вы можете отбросить предмет для атаки или использовать его для блокирования атак.Создать какую-то систему обратного вызова / событий. Есть разные возможные подходы к этому.
Как насчет простоты?
virtual void onEquip(Owner*) {}
иvirtual void onUnequip(Owner*) {}
.virtual void onEquip(Owner *o) { o->modifyStat("attack", attackValue); }
иvirtual void onUnequip(Owner *o) { o->modifyStat("attack", -attackValue); }
.По сравнению с простым запросом значений атаки / защиты как раз вовремя, это не только делает все более динамичным, но и экономит ненужные вызовы и даже позволяет создавать предметы, которые будут постоянно влиять на вашего персонажа.
Например, представьте проклятое кольцо, которое будет устанавливать скрытую стат, когда он будет экипирован, помечая вашего персонажа как проклятого навсегда.
источник
Хотя @DocBrown дал хороший ответ, он не зашел достаточно далеко, имхо. Прежде чем вы начнете оценивать ответы, вы должны оценить свои потребности. Что тебе действительно нужно ?
Ниже я покажу два возможных решения, которые предлагают разные преимущества для разных нужд.
Первый очень упрощен и специально разработан для того, что вы показали:
Это позволяет очень легко сериализовать / десериализовывать инструменты (например, для сохранения или работы в сети) и вообще не требует виртуальной отправки. Если ваш код - это все, что вы показали, и вы не ожидаете, что он будет развиваться совсем иначе, чем использование более разных инструментов с разными именами и той статистикой, только в разных количествах, тогда это путь.
@DocBrown предлагает решение, которое все еще основано на виртуальной диспетчеризации, и это может быть преимуществом, если вы каким-то образом специализируете инструменты для частей вашего кода, которые не были показаны. Однако, если вам действительно нужно или вы хотите изменить другое поведение, я бы предложил следующее решение:
Состав по наследству
Что если вам позже понадобится инструмент, который изменяет ловкость ? Или скорость бега ? Мне кажется, вы делаете RPG. Одна вещь, которая важна для RPG - это быть открытым для расширения. . Решения, показанные до сих пор, не предлагают этого. Вам нужно будет изменить
Tool
класс и добавить в него новые виртуальные методы каждый раз, когда вам понадобится новый атрибут.Второе решение, которое я показываю, это то, на которое я намекал ранее в комментарии - оно использует композицию вместо наследования и следует принципу «закрыто для модификации, открыто для расширения *». Если вы знакомы с работой систем сущностей, некоторые вещи будет выглядеть знакомо (мне нравится думать о композиции как о младшем брате ES).
Обратите внимание, что то, что я показываю ниже, значительно более элегантно в языках с информацией о типах во время выполнения, таких как Java или C #. Поэтому код C ++, который я показываю, должен включать в себя некоторую «бухгалтерию», которая просто необходима, чтобы заставить композицию работать прямо здесь. Может быть, кто-то с большим опытом C ++ может предложить еще лучший подход.
Сначала мы снова смотрим на сторону звонящего . В вашем примере вы, как вызывающая сторона внутри
attack
метода, вообще не заботитесь об инструментах. Что вас волнует, так это два свойства - точки атаки и защиты. Ты не очень волнует , где те пришли, и вы не заботитесь о других свойствах (например , скорости бега, аджилити).Итак, сначала мы вводим новый класс
И затем мы создаем наши первые два компонента
После этого мы заставляем инструмент содержать набор свойств и делаем свойства доступными для запроса другими.
Обратите внимание, что в этом примере я поддерживаю только один компонент каждого типа - это упрощает работу. Теоретически вы также можете использовать несколько компонентов одного типа, но это очень быстро. Один важный аспект:
Tool
теперь закрыт для модификации - мы никогда больше не будем касаться источникаTool
- но открыт для расширения - мы можем расширить поведение инструмента, изменяя другие вещи и просто передавая в него другие компоненты.Теперь нам нужен способ извлечения инструментов по типам компонентов. Вы все еще можете использовать вектор для инструментов, как в вашем примере кода:
Вы также можете изменить это в свой собственный
Inventory
класс и хранить таблицы поиска, которые значительно упрощают извлечение инструментов по типу компонента и избегают повторения всей коллекции снова и снова.Какие преимущества имеет этот подход? В
attack
, вы обрабатываете инструменты, которые имеют два компонента - вам больше ничего не нужно.Давайте представим, что у вас есть
walkTo
метод, и теперь вы решили, что было бы неплохо, если бы какой-нибудь инструмент мог изменять вашу скорость ходьбы. Нет проблем!Сначала создайте новое
Component
:Затем вы просто добавляете экземпляр этого компонента в инструмент, который хотите увеличить скорость пробуждения, и меняете
WalkTo
метод для обработки только что созданного компонента:Обратите внимание, что мы добавили некоторое поведение в наши инструменты без изменения класса инструментов вообще.
Вы можете (и должны) переместить строки в макро или статическую переменную const, чтобы вам не приходилось вводить их снова и снова.
Если вы продолжите этот подход - например, создадите компоненты, которые могут быть добавлены к игроку, и создадите
Combat
компонент, который помечает игрока как способного участвовать в бою, то вы также можете избавиться отattack
метода и позволить этому быть обработанным. Компонентом или обрабатываться в другом месте.Преимущество в том, что игрок сможет получать Компоненты, также заключается в том, что вам даже не нужно будет менять игрока, чтобы он мог по-разному вести себя. В моем примере вы могли бы создать
Movable
компонент, так что вам не нужно реализовыватьwalkTo
метод на игроке, чтобы заставить его двигаться. Вы просто создадите компонент, подключите его к плееру и позволите кому-то другому обработать его.Вы можете найти полный пример в этой сути: https://gist.github.com/NetzwergX/3a29e1b106c6bb9c7308e89dd715ee20
Это решение, очевидно, немного сложнее, чем другие, которые были опубликованы. Но в зависимости от того, насколько вы хотите быть гибким, как далеко вы хотите пройти, это может быть очень мощный подход.
редактировать
Некоторые другие ответы предлагают прямое наследование (Создание мечей расширяет Инструмент, делает Щит расширяет Инструмент). Я не думаю, что это сценарий, где наследование работает очень хорошо. Что если вы решите, что блокировка щитом определенным образом также может повредить атакующему? С моим решением вы можете просто добавить компонент Attack к щиту и понять это без каких-либо изменений в вашем коде. С наследованием у тебя будут проблемы. Предметы / инструменты в RPG - главные кандидаты на составление или даже прямое использование систем сущностей с самого начала.
источник
Вообще говоря, если вам когда-либо понадобится использовать
if
(в сочетании с требованием указать тип экземпляра) на любом языке ООП, это признак того, что происходит что-то вонючее. По крайней мере, вы должны присмотреться к своим моделям.Я бы смоделировал ваш домен по-другому.
Для вашего варианта использования a
Tool
имеет aAttackBonus
и aDefenseBonus
- и то, и другое может быть0
в случае, если оно бесполезно для борьбы, как перья или что-то в этом роде.Для атаки у вас есть
baserate
+bonus
от используемого оружия. То же самое касается обороныbaserate
+bonus
.Следовательно, у вас
Tool
должен бытьvirtual
метод расчета бонусов атаки / защиты.ТЛ; др
С лучшим дизайном вы можете избежать хакерских
if
с.источник
if
меньше программирования. Главным образом в комбинациях, как будтоinstanceof
или что-то подобное. Но есть позиция, которая утверждаетif
, что это кодовая ячейка, и есть способы обойти это. И вы правы, это важный оператор, который имеет свое право.Как написано, это «пахнет», но это могут быть только те примеры, которые вы привели. Хранение данных в контейнерах универсальных объектов, а затем их преобразование для получения доступа к данным не является запахом кода. Вы увидите, что он используется во многих ситуациях. Однако, когда вы используете его, вы должны знать, что вы делаете, как вы это делаете и почему. Когда я смотрю на пример, использование сравнения на основе строк говорит мне, какой объект является тем, что является предметом, который срабатывает в моем личном измерителе запаха. Это говорит о том, что вы не совсем уверены в том, что вы здесь делаете (и это хорошо, поскольку у вас хватило мудрости прийти сюда к программистам. SE и сказать: «Эй, мне не нравится то, что я делаю, помогите меня нет! ").
Фундаментальная проблема с шаблоном приведения данных из общих контейнеров, как это, заключается в том, что производитель данных и потребитель данных должны работать вместе, но это может быть неочевидно, что они делают это на первый взгляд. В каждом примере этой модели, вонючей или не вонючей, это фундаментальная проблема. Это очень возможно для следующего разработчика , чтобы быть совершенно не знают , что вы делаете этот шаблон , и разорвать его случайно, поэтому , если вы используете этот шаблон вы должны позаботиться , чтобы помочь следующему отказу разработчиков. Вы должны упростить ему непреднамеренное нарушение кода из-за некоторых деталей, о которых он, возможно, не знал.
Например, что если я захочу скопировать плеер? Если я просто смотрю на содержимое объекта проигрывателя, это выглядит довольно просто. Мне просто нужно скопировать
attack
,defense
иtools
переменные. Проще простого! Что ж, я быстро выясню, что использование указателей делает его немного сложнее (в какой-то момент стоит взглянуть на умные указатели, но это уже другая тема). Это легко решается. Я просто создаю новые копии каждого инструмента и помещаю их в свой новыйtools
список. В конце концов,Tool
это действительно простой класс с одним членом. Поэтому я создал кучу копий, включая копиюSword
, но я не знал, что это меч, поэтому я только скопировалname
. Позже,attack()
функция смотрит на имя, видит, что это «меч», забрасывает его, и случаются плохие вещи!Мы можем сравнить этот случай с другим случаем в программировании сокетов, в котором используется тот же шаблон. Я могу настроить функцию сокета UNIX следующим образом:
Почему это тот же шаблон? Потому
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
. Серьезно, на такие ошибки могут уйти дни, пока они не поняли, что произошло.Перечисление работает намного лучше. Это быстро, дешево и намного менее подвержено ошибкам:
Этот пример также демонстрирует
switch
оператор, включающий перечисления, с единственной наиболее важной частью этого шаблона:default
регистр, который выбрасывает. Вы никогда не должны попадать в такую ситуацию, если вы все делаете идеально. Однако, если кто-то добавит новый тип инструмента, и вы забудете обновить свой код для его поддержки, вы захотите, чтобы что-то перехватило ошибку. На самом деле, я так рекомендую их, что вы должны добавить их, даже если они вам не нужны.Другим огромным преимуществом
enum
является то, что он дает следующему разработчику полный список допустимых типов инструментов, сразу же. Нет необходимости изучать код, чтобы найти специализированный класс Флейты Боба, который он использует в своей эпической битве с боссом.Да, я вставил «пустую» инструкцию по умолчанию, просто чтобы ясно указать следующему разработчику, что я ожидаю, если какой-то новый неожиданный тип появится у меня на пути.
Если вы сделаете это, шаблон будет меньше пахнуть. Однако, чтобы быть без запаха, последнее, что вам нужно сделать, это рассмотреть другие варианты. Эти приведения являются одними из наиболее мощных и опасных инструментов, которые есть в репертуаре C ++. Вы не должны использовать их, если у вас нет веских причин.
Одна очень популярная альтернатива - это то, что я называю «объединенная структура» или «объединенный класс». Для вашего примера это на самом деле очень хорошо подходит. Чтобы создать один из них, вы создаете
Tool
класс с перечислением, как раньше, но вместо того , чтобы создавать подклассыTool
, мы просто помещаем в него все поля из каждого подтипа.Теперь вам не нужны подклассы вообще. Вам просто нужно посмотреть на
type
поле, чтобы увидеть, какие другие поля действительно действительны. Это намного безопаснее и проще для понимания. Однако у него есть недостатки. Есть моменты, когда вы не хотите использовать это:Это решение не используется сокетами 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 использует абстрактный базовый класс, который имеет виртуальную таблицу, которая знает, какой тип объекта. Затем мы создаем аккуратный маленький вызов двойного косвенного действия, который выполняет работу:
Так что же это за ужасная картина? Ну,
Tool
есть виртуальная функцияaccept
. Если вы передадите ему посетителя, ожидается, что он развернется и вызовет правильнуюvisit
функцию для этого посетителя для типа. Это то, чтоvisitor.visit(*this);
делает для каждого подтипа. Сложно, но мы можем показать это на вашем примере выше:Так что здесь происходит? Мы создаем посетителя, который будет выполнять некоторую работу за нас, как только он узнает, какой тип объекта он посещает. Затем мы перебираем список инструментов. Ради аргумента, скажем, первый объект - это 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
и другие полностью взаимозаменяемо, используя тот, который наиболее интуитивно понятен для прозы, полагая, что этот трюк на Си заботится о неважных деталях.источник
В общем, я избегаю реализации нескольких классов / наследования, если это просто для передачи данных. Вы можете придерживаться одного класса и реализовать все оттуда. Для вашего примера этого достаточно
Возможно, вы ожидаете, что ваша игра будет реализовывать несколько видов мечей и т. Д., Но у вас будут другие способы реализовать это. Класс взрыва редко бывает лучшей архитектурой. Будь проще.
источник
Как уже говорилось ранее, это серьезный кодовый запах. Однако можно подумать, что источником вашей проблемы является использование наследования вместо композиции в вашем дизайне.
Например, учитывая то, что вы нам показали, у вас есть три понятия:
Обратите внимание, что ваш четвертый класс - это просто сочетание двух последних понятий. Поэтому я бы предложил использовать композицию для этого.
Вам нужна структура данных для представления информации, необходимой для атаки. И вам нужна структура данных, представляющая информацию, необходимую для защиты. Наконец, вам нужна структура данных для представления вещей, которые могут иметь или не иметь одно или оба из этих свойств:
источник
Attack
иDefense
усложняться без изменения интерфейсаTool
.Tool
полностью закрытой для модификации, но при этом оставить ее открытой для расширения.Tool
не изменяя его. И если у меня есть право изменить его, то я не вижу необходимости в произвольных компонентах.Почему бы не создавать абстрактные методы
modifyAttack
иmodifyDefense
вTool
классе? Тогда у каждого ребенка будет своя реализация, и вы называете этот элегантный способ:Передача значений в качестве ссылки сэкономит ресурсы, если вы сможете:
источник
Если кто-то использует полиморфизм, то всегда лучше, если весь код, который заботится о том, какой класс используется, находится внутри самого класса. Вот как я мог бы написать это:
Это имеет следующие преимущества:
источник
Я думаю, что один из способов признать недостатки этого подхода - развить вашу идею до ее логического завершения.
Это похоже на игру, поэтому на каком-то этапе вы, вероятно, начнете беспокоиться о производительности и поменяете местами сравнения строк на
int
илиenum
. По мере того, как список предметов становится длиннее, онif-else
становится довольно громоздким, поэтому вы можете рассмотреть возможность его реорганизации вswitch-case
. На этом этапе у вас также есть достаточно текста, так что вы можете включить действие внутри каждогоcase
в отдельную функцию.Как только вы достигнете этой точки, структура вашего кода начинает выглядеть знакомой - она начинает выглядеть как доморощенный, свернутый вручную vtable * - базовая структура, на которой обычно реализуются виртуальные методы. Кроме того, это vtable, который вы должны вручную обновлять и обслуживать каждый раз, когда добавляете или изменяете тип элемента.
Придерживаясь «реальных» виртуальных функций, вы можете сохранить реализацию поведения каждого элемента в самом элементе. Вы можете добавлять дополнительные элементы более автономным и последовательным способом. И когда вы делаете все это, компилятор будет заботиться о реализации вашей динамической диспетчеризации, а не вы.
Для решения вашей конкретной проблемы: вы изо всех сил пытаетесь написать простую пару виртуальных функций для обновления атаки и защиты, потому что некоторые элементы влияют только на атаку, а некоторые - только на защиту. Хитрость в простом случае, подобном этому, заключается в том, чтобы реализовать оба поведения в любом случае, но без эффекта в определенных случаях.
GetDefenseBonus()
может вернуться0
илиApplyDefenseBonus(int& defence)
может просто уйтиdefence
без изменений. То, как вы поступите, будет зависеть от того, как вы хотите справиться с другими действиями, которые действительно оказывают влияние. В более сложных случаях, когда существует более разнообразное поведение, вы можете просто объединить действие в один метод.* (Хотя и транспонированный относительно типичной реализации)
источник
Наличие блока кода, который знает обо всех возможных «инструментах», не очень хороший дизайн (тем более, что в вашем коде будет много таких блоков); но ни у одного из них нет базовых
Tool
заглушек для всех возможных свойств инструмента: теперьTool
класс должен знать обо всех возможных применениях.То, что знает каждый инструмент, это то, что он может внести в персонажа, который его использует. Так что предоставьте один метод для всех инструментов
giveto(*Character owner)
. Он будет корректировать статистику игрока соответствующим образом, не зная, что могут делать другие инструменты, и, что лучше, ему также не нужно знать о нерелевантных свойствах персонажа. Например, щит не нужно даже знать об атрибутахattack
,invisibility
, иhealth
т.д. Все , что нужно применять инструмент для персонажа , чтобы поддержать атрибуты , что объект требует. Если вы попытаетесь дать меч ослу, а у осла нетattack
характеристик, вы получите ошибку.У инструментов также должен быть
remove()
метод, который обращает их влияние на владельца. Это немного сложно (возможно, в конечном итоге с инструментами, которые оставляют ненулевой эффект, когда они даны, а затем убраны), но, по крайней мере, он локализован для каждого инструмента.источник
Там нет ответа, который говорит, что это не пахнет, поэтому я буду тем, кто поддерживает это мнение; этот код полностью в порядке! Мое мнение основано на том факте, что иногда легче двигаться дальше и постепенно увеличивать свои навыки по мере создания новых вещей. Вы можете на несколько дней застрять в создании идеальной архитектуры, но, вероятно, никто даже не увидит ее в действии, потому что вы никогда не заканчивали проект. Ура!
источник