Обход правил у волшебников и воинов

9

В этой серии постов в блоге Эрик Липперт описывает проблему объектно-ориентированного проектирования на примере мастеров и воинов, где:

abstract class Weapon { }
sealed class Staff : Weapon { }
sealed class Sword : Weapon { }

abstract class Player 
{ 
  public Weapon Weapon { get; set; }
}
sealed class Wizard : Player { }
sealed class Warrior : Player { }

а затем добавляет пару правил:

  • Воин может использовать только меч.
  • Волшебник может использовать только посох.

Затем он продолжает демонстрировать проблемы, с которыми вы сталкиваетесь, если вы пытаетесь применить эти правила с помощью системы типов C # (например, Wizardвозложить на класс ответственность за то, чтобы мастер мог использовать только персонал). Вы нарушаете принцип подстановки Лискова, рискуете получить исключения во время выполнения или получаете код, который сложно расширить.

Решение, которое он предлагает, заключается в том, что класс Player не выполняет проверку. Он используется только для отслеживания состояния. Затем вместо того, чтобы дать игроку оружие:

player.Weapon = new Sword();

состояние изменяется Commands и в соответствии с Rules:

... мы создаем Commandобъект с именем, Wieldкоторый принимает два объекта состояния игры, a Playerи a Weapon. Когда пользователь выдает команду системе «этот волшебник должен владеть этим мечом», тогда эта команда оценивается в контексте набора Rules, который создает последовательность Effects. У нас есть такой, Ruleкоторый говорит, что когда игрок пытается владеть оружием, эффект состоит в том, что существующее оружие, если оно есть, сбрасывается, и новое оружие становится оружием игрока. У нас есть другое правило, которое усиливает первое правило, которое гласит, что эффекты первого правила не применяются, когда волшебник пытается владеть мечом.

Мне в принципе нравится эта идея, но я беспокоюсь о том, как ее можно использовать на практике.

Кажется, ничто не мешает разработчику обойти « Commandsи Rule», просто установив « Weaponна» Player. WeaponСобственности должны быть доступны по Wieldкоманде, так что это не может быть сделаны private set.

Итак, что же предотвратить разработчик делать это? Они просто должны помнить, чтобы не делать?

Бен Л
источник
2
Я не думаю, что этот вопрос зависит от языка (C #), так как на самом деле это вопрос дизайна ООП. Пожалуйста, рассмотрите возможность удаления тега C #.
Maybe_Factor
1
Тег @maybe_factor c # подойдет, потому что отправленный код - c #.
CodingYoshi
Почему бы вам не спросить @EricLippert напрямую? Кажется, он появляется здесь на этом сайте время от времени.
Док Браун
@Maybe_Factor - я колебался над тегом C #, но решил оставить его на случай, если найдется решение для конкретного языка.
Бен Л
1
@DocBrown - я опубликовал этот вопрос в его блоге (правда, всего пару дней назад; я так долго не ждал ответа). Есть ли способ довести мой вопрос до его сведения?
Бен Л

Ответы:

9

Весь аргумент, к которому приводит серия постов в блоге, находится в пятой части :

У нас нет оснований полагать, что система типов C # была разработана с достаточной общностью для кодирования правил Dungeons & Dragons, так почему же мы даже пытаемся?

Мы решили проблему «куда идет код, выражающий правила системы?» Это относится к объектам, которые представляют правила системы, а не к объектам, представляющим состояние игры; забота о состоянии объектов заключается в том, чтобы поддерживать их состояние непротиворечивым, а не в оценке правил игры.

Оружие, персонажи, монстры и другие игровые объекты не несут ответственности за проверку того, что они могут или не могут делать. За это отвечает система правил. CommandОбъект ничего с объектами игры не делает ни. Это просто попытка что-то с ними сделать. Затем система правил проверяет, возможна ли команда, и когда это правило, система выполняет команду, вызывая соответствующие методы на игровых объектах.

Если разработчик хочет создать вторую систему правил, которая работает с персонажами и оружием, чего не позволила бы первая система правил, он может сделать это, потому что в C # вы не можете (без неприятных взломов отражений) выяснить, откуда происходит вызов метода от.

Обходной путь, который может работать в некоторых ситуациях, - поместить игровые объекты (или их интерфейсы) в одну сборку с механизмом правил и пометить любые методы-мутаторы как internal. Любые системы, которым нужен доступ только для чтения к игровым объектам, будут находиться в другой сборке, что означает, что они смогут получить доступ только к publicметодам. Это все еще оставляет лазейку игровых объектов, вызывающих внутренние методы друг друга. Но делать это было бы очевидным запахом кода, потому что вы согласились, что классы игровых объектов должны быть глупыми держателями состояний.

Philipp
источник
4

Очевидная проблема исходного кода заключается в том, что он выполняет моделирование данных вместо объектного моделирования . Обратите внимание, что в связанной статье нет абсолютно никакого упоминания о реальных бизнес-требованиях!

Я бы начал с попыток получить реальные функциональные требования. Например: «Любой игрок может атаковать других игроков, ...». Вот:

interface Player {
    void Attack(Player enemy);
}

«Игроки могут владеть оружием, которое используется при атаке, Волшебники могут владеть Посохом, Воинами - Мечом»:

public class Wizard: Player {
    ...
    public void Wield(Staff weapon) { ... }
    ...
}
public class Warrior: Player {
    ...
    public void Wield(Sword sword) { ... }
    ...
}

«Каждое оружие наносит урон атакующему врагу». Хорошо, теперь у нас должен быть общий интерфейс для оружия:

interface Weapon {
    void dealDamageTo(Player enemy);
}

И так далее ... Почему нет Wield()в Player? Потому что не было требования, чтобы любой игрок мог владеть любым оружием.

Я могу представить, что будет требование, гласящее: «Любой Playerможет попытаться овладеть любым Weapon». Это было бы совсем другое дело, однако. Я бы смоделировал это возможно так:

interface Player {
    void Attack(Player enemy);
    void TryWielding(Weapon weapon); // Throws UnwieldableException
}

Резюме: смоделируйте требования и только требования. Не занимайтесь моделированием данных, это не моделирование.

Роберт Бройтигам
источник
1
Вы читали сериал? Может быть, вы хотите сказать автору этой серии не моделировать данные, а требования. Требования, которые у вас есть в вашем ответе, являются вашими выдуманными требованиями, а НЕ требованиями, которые были у автора при сборке компилятора C #.
CodingYoshi
2
Эрик Липперт подробно описывает техническую проблему в этой серии, и это нормально. Однако этот вопрос касается самой проблемы, а не возможностей C #. Моя точка зрения заключается в том, что в реальных проектах мы должны следовать бизнес-требованиям (для которых я приводил выдуманные примеры, да), а не предполагать отношения и свойства. Вот как вы получаете модель, которая подходит. Какой был вопрос.
Роберт
Это первое, что я подумал, читая эту серию. Автор просто придумал некоторые абстракции, никогда не оценивая их дальше, просто придерживаясь их. Пытаюсь механически решить проблему, снова и снова. Вместо того, чтобы думать о предметной области и абстракциях, которые полезны, это должно идти первым. Мой upvote.
Вадим Самохин
Это правильный ответ. В статье выражаются противоречивые требования (одно требование говорит о том, что игрок может владеть [любым] оружием, в то время как другие требования говорят, что это не так.), А затем подробно описывается, насколько сложно системе правильно выразить конфликт. Единственный правильный ответ - устранить конфликт. В этом случае это означает, что отменить требование, согласно которому игрок может владеть любым оружием.
Даниэль Т.
2

Одним из способов было бы передать Wieldкоманду Player. Затем игрок выполняет Wieldкоманду, которая проверяет соответствующие правила и возвращает команду Weapon, с которой Playerзатем устанавливает свое собственное поле оружия. Таким образом, поле «Оружие» может иметь приватный сеттер и может быть установлено только путем передачи Wieldкоманды игроку.

Maybe_Factor
источник
На самом деле это не решает проблему. Разработчик, который создает командный объект, может передать любое оружие, и игрок установит его. Читайте сериал, потому что проблема сложнее, чем вы думаете. На самом деле он сделал эту серию, потому что он столкнулся с этой проблемой дизайна при разработке компилятора Roslyn C #.
CodingYoshi
2

Ничто не мешает разработчику сделать это. На самом деле Эрик Липперт испробовал много разных техник, но у всех были свои слабости. В этом и заключалась вся суть этой серии, заключающаяся в том, что помешать разработчику сделать это нелегко, и все, что он пытался сделать, имело недостатки. Наконец он решил, что использование Commandобъекта с правилами - это путь.

С помощью правил вы можете установить для Weaponсвойства a Wizardзначение a, Swordно когда вы попросите Wizardего владеть оружием (мечом) и атаковать его, оно не будет иметь никакого эффекта и, следовательно, не изменит никакого состояния. Как он говорит ниже:

У нас есть другое правило, которое усиливает первое правило, которое гласит, что эффекты первого правила не применяются, когда волшебник пытается владеть мечом. Эффекты для этой ситуации: «издавать грустный звук тромбона, пользователь теряет свои действия в этом ходу, состояние игры не изменяется

Другими словами, мы не можем навязать такое правило через typeотношения, которые он пробовал разными способами, но либо не нравился, либо не работал. Таким образом, единственное, что он сказал, что мы можем сделать, - это сделать что-то с этим во время выполнения. Бросать исключение было бесполезно, потому что он не считает это исключением.

В конце концов он решил пойти с вышеупомянутым решением. Это решение в основном говорит о том, что вы можете установить любое оружие, но если вы отдадите его, если не правильное оружие, оно будет по существу бесполезным. Но не будет исключений.

Я думаю, что это хорошее решение. Хотя в некоторых случаях я бы тоже использовал шаблон try-set.

CodingYoshi
источник
This solution basically says you can set any weapon but when you yield it, if not the right weapon, it would be essentially useless.Я не смог найти его в этой серии, не могли бы вы указать мне, где предлагается это решение?
Вадим Самохин
@zapadlo Он говорит это косвенно. Я скопировал эту часть в своем ответе и процитировал это. Здесь это снова. В цитате он говорит: когда волшебник пытается владеть мечом. Как волшебник может владеть мечом, если меч не был установлен? Должно быть, было установлено. Затем, если волшебник владеет мечом . Эффекты для этой ситуации - «
издать
Хм, я думаю, что владение мечами в основном означает, что это должно быть установлено, нет? Читая этот абзац, я понимаю, что эффект первого правила таков that the existing weapon, if there is one, is dropped and the new weapon becomes the player’s weapon. Хотя второе правило, that strengthens the first rule, that says that the first rule’s effects do not apply when a wizard tries to wield a sword.поэтому я думаю, что есть правило, проверяющее, является ли оружие мечом, поэтому оно не может быть использовано волшебником, поэтому оно не установлено. Вместо этого звучит грустный тромбон.
Вадим Самохин
На мой взгляд, было бы странно, что какое-то командное правило нарушается. Это как размытие проблемы. Зачем решать эту проблему после того, как правило уже нарушено, и переводить мастера в недопустимое состояние? У волшебника не может быть меча, но он есть! Почему бы не позволить этому случиться?
Вадим Самохин
Я согласен с @Zapadlo о том, как интерпретировать Wieldздесь. Я думаю, что это немного вводящее в заблуждение название команды. Нечто подобное ChangeWeaponбудет точнее. Я полагаю, у вас может быть другая модель, в которой вы можете установить любое оружие, но если вы отдадите его, если не правильное оружие, оно будет по существу бесполезным . Это звучит интересно, но я не думаю, что это то, что описывает Эрик Липперт.
Бен Л
2

Первое выброшенное автором решение состояло в том, чтобы представлять правила системой типов. Система типов оценивается во время компиляции. Если вы отсоедините правила от системы типов, они больше не проверяются компилятором, поэтому нет ничего, что мешало бы разработчику совершить ошибку как таковую.

Но с этой проблемой сталкивается каждая часть логики / моделирования, которая не проверяется компилятором, и общий ответ на это - (модульное) тестирование. Следовательно, предлагаемое автором решение нуждается в сильной проверке, чтобы обойти ошибки разработчиков. Чтобы подчеркнуть этот момент необходимости строгого использования тестов для ошибок, которые обнаруживаются только во время выполнения, посмотрите на эту статью Брюса Эккеля, которая приводит аргумент, что вам нужно заменить строгую типизацию для более сильного тестирования в динамических языках.

В заключение, единственное, что может помешать разработчикам делать ошибки, - это набор (модульных) тестов, проверяющих соблюдение всех правил.

larsbe
источник
Вы должны использовать API, и API должен гарантировать, что вы будете выполнять модульное тестирование или сделаете такое предположение? Вся проблема заключается в моделировании, поэтому модель не сломается, даже если разработчик, использующий модель, небрежен.
CodingYoshi
1
Я пытался подчеркнуть, что ничто не мешает разработчику совершать ошибки. В предлагаемом решении правила отделены от данных, поэтому, если вы не создаете свои собственные проверки, ничто не мешает вам использовать объекты данных без применения правил.
Larsbe
1

Возможно, я здесь упустил тонкость, но я не уверен, что проблема с системой типов. Может быть, это с соглашением в C #.

Например, вы можете сделать этот тип полностью безопасным, Weaponзащитив сеттер Player. Затем добавьте setSword(Sword)и setStaff(Staff)к Warriorи Wizardсоответственно, что вызывает защищенный сеттер.

Таким образом, отношение Player/ Weaponпроверяется статически, и код, который не заботится, может просто использовать a Playerдля получения Weapon.

Alex
источник
Эрик Липперт не хотел бросать исключения. Вы читали сериал? Решение должно соответствовать требованиям, и эти требования четко изложены в серии.
CodingYoshi
@CodingYoshi Зачем это исключение? Это безопасный тип, то есть проверяемый во время компиляции.
Алекс
Извините, я не смог изменить свой комментарий, как только я понял, что вы не бросаете исключение. Тем не менее, вы нарушили наследство, сделав это. Посмотрите на проблему, которую автор пытался решить, вы не можете просто добавить метод, как вы сделали, потому что теперь типы не могут быть обработаны полиморфно.
CodingYoshi
@CodingYoshi Полиморфное требование - у игрока есть оружие. И в этой схеме у игрока действительно есть оружие. Ни одно наследство не нарушено. Это решение будет компилироваться, только если вы правильно поняли правила.
Алекс
@CodingYoshi Теперь, это не значит, что вы не можете написать код, который потребовал бы проверки во время выполнения, например, если вы пытаетесь добавить a Weaponк a Player. Но не существует системы типов, в которой вы не знаете конкретных типов во время компиляции, которые могли бы воздействовать на эти конкретные типы во время компиляции. По определению. Эта схема означает, что это только тот случай, который должен рассматриваться во время выполнения, так как он на самом деле лучше, чем любая из схем Эрика.
Алекс
0

Итак, что мешает разработчику сделать это? Они просто должны помнить, чтобы не делать?

Этот вопрос фактически совпадает с темой «святой войны», называемой « где поставить проверку » (скорее всего, также отмечая DDD).

Итак, прежде чем ответить на этот вопрос, следует спросить себя: какова природа правил, которым вы хотите следовать? Они высечены в камне и определяют сущность? В результате нарушения этих правил сущность перестает быть тем, чем она является? Если да, наряду с сохранением этих правил в проверке команд , также поместите их в сущность. Поэтому, если разработчик забудет проверить команду, ваши сущности не будут в недопустимом состоянии.

Если нет - хорошо, это по сути подразумевает, что эти правила являются специфическими для команды и не должны находиться в доменных объектах. Таким образом, нарушение этого правила приводит к действиям, которые не должны были выполняться, но не в недопустимом состоянии модели.

Вадим Самохин
источник