В этой серии постов в блоге Эрик Липперт описывает проблему объектно-ориентированного проектирования на примере мастеров и воинов, где:
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();
состояние изменяется Command
s и в соответствии с Rule
s:
... мы создаем
Command
объект с именем,Wield
который принимает два объекта состояния игры, aPlayer
и aWeapon
. Когда пользователь выдает команду системе «этот волшебник должен владеть этим мечом», тогда эта команда оценивается в контексте набораRule
s, который создает последовательностьEffect
s. У нас есть такой,Rule
который говорит, что когда игрок пытается владеть оружием, эффект состоит в том, что существующее оружие, если оно есть, сбрасывается, и новое оружие становится оружием игрока. У нас есть другое правило, которое усиливает первое правило, которое гласит, что эффекты первого правила не применяются, когда волшебник пытается владеть мечом.
Мне в принципе нравится эта идея, но я беспокоюсь о том, как ее можно использовать на практике.
Кажется, ничто не мешает разработчику обойти « Commands
и Rule
», просто установив « Weapon
на» Player
. Weapon
Собственности должны быть доступны по Wield
команде, так что это не может быть сделаны private set
.
Итак, что же предотвратить разработчик делать это? Они просто должны помнить, чтобы не делать?
Ответы:
Весь аргумент, к которому приводит серия постов в блоге, находится в пятой части :
Оружие, персонажи, монстры и другие игровые объекты не несут ответственности за проверку того, что они могут или не могут делать. За это отвечает система правил.
Command
Объект ничего с объектами игры не делает ни. Это просто попытка что-то с ними сделать. Затем система правил проверяет, возможна ли команда, и когда это правило, система выполняет команду, вызывая соответствующие методы на игровых объектах.Если разработчик хочет создать вторую систему правил, которая работает с персонажами и оружием, чего не позволила бы первая система правил, он может сделать это, потому что в C # вы не можете (без неприятных взломов отражений) выяснить, откуда происходит вызов метода от.
Обходной путь, который может работать в некоторых ситуациях, - поместить игровые объекты (или их интерфейсы) в одну сборку с механизмом правил и пометить любые методы-мутаторы как
internal
. Любые системы, которым нужен доступ только для чтения к игровым объектам, будут находиться в другой сборке, что означает, что они смогут получить доступ только кpublic
методам. Это все еще оставляет лазейку игровых объектов, вызывающих внутренние методы друг друга. Но делать это было бы очевидным запахом кода, потому что вы согласились, что классы игровых объектов должны быть глупыми держателями состояний.источник
Очевидная проблема исходного кода заключается в том, что он выполняет моделирование данных вместо объектного моделирования . Обратите внимание, что в связанной статье нет абсолютно никакого упоминания о реальных бизнес-требованиях!
Я бы начал с попыток получить реальные функциональные требования. Например: «Любой игрок может атаковать других игроков, ...». Вот:
«Игроки могут владеть оружием, которое используется при атаке, Волшебники могут владеть Посохом, Воинами - Мечом»:
«Каждое оружие наносит урон атакующему врагу». Хорошо, теперь у нас должен быть общий интерфейс для оружия:
И так далее ... Почему нет
Wield()
вPlayer
? Потому что не было требования, чтобы любой игрок мог владеть любым оружием.Я могу представить, что будет требование, гласящее: «Любой
Player
может попытаться овладеть любымWeapon
». Это было бы совсем другое дело, однако. Я бы смоделировал это возможно так:Резюме: смоделируйте требования и только требования. Не занимайтесь моделированием данных, это не моделирование.
источник
Одним из способов было бы передать
Wield
командуPlayer
. Затем игрок выполняетWield
команду, которая проверяет соответствующие правила и возвращает командуWeapon
, с которойPlayer
затем устанавливает свое собственное поле оружия. Таким образом, поле «Оружие» может иметь приватный сеттер и может быть установлено только путем передачиWield
команды игроку.источник
Ничто не мешает разработчику сделать это. На самом деле Эрик Липперт испробовал много разных техник, но у всех были свои слабости. В этом и заключалась вся суть этой серии, заключающаяся в том, что помешать разработчику сделать это нелегко, и все, что он пытался сделать, имело недостатки. Наконец он решил, что использование
Command
объекта с правилами - это путь.С помощью правил вы можете установить для
Weapon
свойства aWizard
значение a,Sword
но когда вы попроситеWizard
его владеть оружием (мечом) и атаковать его, оно не будет иметь никакого эффекта и, следовательно, не изменит никакого состояния. Как он говорит ниже:Другими словами, мы не можем навязать такое правило через
type
отношения, которые он пробовал разными способами, но либо не нравился, либо не работал. Таким образом, единственное, что он сказал, что мы можем сделать, - это сделать что-то с этим во время выполнения. Бросать исключение было бесполезно, потому что он не считает это исключением.В конце концов он решил пойти с вышеупомянутым решением. Это решение в основном говорит о том, что вы можете установить любое оружие, но если вы отдадите его, если не правильное оружие, оно будет по существу бесполезным. Но не будет исключений.
Я думаю, что это хорошее решение. Хотя в некоторых случаях я бы тоже использовал шаблон try-set.
источник
This solution basically says you can set any weapon but when you yield it, if not the right weapon, it would be essentially useless.
Я не смог найти его в этой серии, не могли бы вы указать мне, где предлагается это решение?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.
поэтому я думаю, что есть правило, проверяющее, является ли оружие мечом, поэтому оно не может быть использовано волшебником, поэтому оно не установлено. Вместо этого звучит грустный тромбон.Wield
здесь. Я думаю, что это немного вводящее в заблуждение название команды. Нечто подобноеChangeWeapon
будет точнее. Я полагаю, у вас может быть другая модель, в которой вы можете установить любое оружие, но если вы отдадите его, если не правильное оружие, оно будет по существу бесполезным . Это звучит интересно, но я не думаю, что это то, что описывает Эрик Липперт.Первое выброшенное автором решение состояло в том, чтобы представлять правила системой типов. Система типов оценивается во время компиляции. Если вы отсоедините правила от системы типов, они больше не проверяются компилятором, поэтому нет ничего, что мешало бы разработчику совершить ошибку как таковую.
Но с этой проблемой сталкивается каждая часть логики / моделирования, которая не проверяется компилятором, и общий ответ на это - (модульное) тестирование. Следовательно, предлагаемое автором решение нуждается в сильной проверке, чтобы обойти ошибки разработчиков. Чтобы подчеркнуть этот момент необходимости строгого использования тестов для ошибок, которые обнаруживаются только во время выполнения, посмотрите на эту статью Брюса Эккеля, которая приводит аргумент, что вам нужно заменить строгую типизацию для более сильного тестирования в динамических языках.
В заключение, единственное, что может помешать разработчикам делать ошибки, - это набор (модульных) тестов, проверяющих соблюдение всех правил.
источник
Возможно, я здесь упустил тонкость, но я не уверен, что проблема с системой типов. Может быть, это с соглашением в C #.
Например, вы можете сделать этот тип полностью безопасным,
Weapon
защитив сеттерPlayer
. Затем добавьтеsetSword(Sword)
иsetStaff(Staff)
кWarrior
иWizard
соответственно, что вызывает защищенный сеттер.Таким образом, отношение
Player
/Weapon
проверяется статически, и код, который не заботится, может просто использовать aPlayer
для полученияWeapon
.источник
Weapon
к aPlayer
. Но не существует системы типов, в которой вы не знаете конкретных типов во время компиляции, которые могли бы воздействовать на эти конкретные типы во время компиляции. По определению. Эта схема означает, что это только тот случай, который должен рассматриваться во время выполнения, так как он на самом деле лучше, чем любая из схем Эрика.Этот вопрос фактически совпадает с темой «святой войны», называемой « где поставить проверку » (скорее всего, также отмечая DDD).
Итак, прежде чем ответить на этот вопрос, следует спросить себя: какова природа правил, которым вы хотите следовать? Они высечены в камне и определяют сущность? В результате нарушения этих правил сущность перестает быть тем, чем она является? Если да, наряду с сохранением этих правил в проверке команд , также поместите их в сущность. Поэтому, если разработчик забудет проверить команду, ваши сущности не будут в недопустимом состоянии.
Если нет - хорошо, это по сути подразумевает, что эти правила являются специфическими для команды и не должны находиться в доменных объектах. Таким образом, нарушение этого правила приводит к действиям, которые не должны были выполняться, но не в недопустимом состоянии модели.
источник