Обхід Правил у Чарівниках та Воїнах


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.Weapon = new Sword();

стан модифікується Commands і відповідно до Rules:

... ми робимо Commandоб'єкт під назвою, Wieldякий займає два об'єкти стану гри, a Playerі a Weapon. Коли користувач видає команду системі "цей майстер повинен володіти цим мечем", тоді ця команда оцінюється в контексті набору Rules, який створює послідовність Effects. У нас є одна, Ruleяка говорить про те, що коли гравець намагається скласти зброю, наслідком є ​​те, що існуюча зброя, якщо така є, скидається, а нова зброя стає зброєю гравця. У нас є ще одне правило, яке посилює перше правило, яке говорить про те, що ефекти першого правила не застосовуються, коли майстер намагається обрушити меч.

Мені в принципі подобається ця ідея, але я хвилююсь, як вона може бути використана на практиці.

Здається, ніщо не заважає розробнику обійти Commandsі Rules, просто встановивши значення Weapona Player. WeaponВласності повинні бути доступні по Wieldкоманді, так що це не може бути зроблено private set.

Отже, що ж запобігти розробник робити це? Вони просто повинні пам’ятати, щоб цього не робити?


2
Я не думаю, що це питання не є мовним (C #), оскільки це справді питання про дизайн OOP. Подумайте про видалення тега C #.
Можливо_Фактор

1
@maybe_factor c # тег добре, оскільки розміщений код - це #.
CodingYoshi

Чому ви не запитаєте @EricLippert безпосередньо? Він, схоже, з’являється тут на цьому сайті час від часу.
Док Браун

@Maybe_Factor - я розмахував над тегом C #, але вирішив зберегти його на випадок, коли є рішення, яке залежить від мови.
Бен Л

1
@DocBrown - я опублікував це питання у своєму блозі (лише пару днів тому, правда, я не так довго чекав відповіді). Чи є спосіб довести моє запитання до нього?
Бен Л

Відповіді:


9

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

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

Ми вирішили проблему "куди йде код, що виражає правила системи?" Він йде в об'єкти, які представляють правила системи, а не в об'єкти, які представляють ігровий стан; стурбованість державних об'єктів полягає у дотриманні їх стану, а не в оцінці правил гри.

Зброя, персонажі, монстри та інші ігрові об'єкти не несуть відповідальності за перевірку того, що вони можуть чи не можуть робити. За це відповідає система правил. CommandОб'єкт нічого з об'єктами гри не робить ні. Це просто представляє спробу зробити щось з ними. Потім система правил перевіряє, чи можлива команда, а коли це правило, система виконує команду, викликаючи відповідні методи на ігрових об'єктах.

Якщо розробник хоче створити другу систему правил, яка робить речі з символами та зброєю, які система перших правил не дозволила б, вони можуть це зробити, тому що в C # ви не можете (без неприємних рефлексійних хакків) з'ясувати, куди надходить метод-виклик з.

Вирішення, яке може спрацювати в деяких ситуаціях, - це розміщення ігрових об'єктів (або їх інтерфейсів) в одній збірці з механізмом правил і позначити будь-які мутаторні методи як internal. Будь-які системи, яким потрібен доступ лише для читання до ігрових об'єктів, будуть в іншій збірці, це означає, що вони матимуть доступ лише до publicметодів. Це все ще залишає лазівку ігрових об'єктів, що викликають внутрішні методи один одного. Але це буде очевидним запахом коду, оскільки ви погодилися, що класи об’єктів гри повинні бути німими державниками.


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 #. Моя думка, що в реальних проектах ми повинні слідувати діловим вимогам (для яких я наводив складені приклади, так), а не припускати відносини та властивості. Саме так ви отримуєте модель, яка підходить. Яка була черга.
Роберт Брутігам

Це перше, що я подумав, читаючи цю серію. Автор просто придумав деякі абстракції, ніколи не оцінюючи їх далі, просто дотримуючись їх. Намагаючись механічно вирішити проблему, знову і знову. Замість того, щоб думати про корисний домен та абстракції, це, мабуть, повинно бути першим. Мій підсумок.
Вадим Самохін

Це правильна відповідь. У статті висловлені суперечливі вимоги (одна вимога говорить про те, що гравець може володіти [будь-якою] зброєю, а інші вимоги говорять про те, що це не так). Єдина правильна відповідь - це усунути конфлікт. У цьому випадку це означає зняти вимогу, що гравець може мати будь-яку зброю.
Даніель Т.

2

Одним із способів було б передати Wieldкоманду до Player. Потім гравець виконує Wieldкоманду, яка перевіряє відповідні правила і повертає те Weapon, з чим Playerпотім встановлює власне поле Зброя. Таким чином, поле Weapon може мати приватний сеттер і встановлюється лише за допомогою передачі Wieldкоманди гравцеві.


Насправді це не вирішує проблему. Розробник, який складає командний об'єкт, може передавати будь-яку зброю, і гравець встановить її. Почитайте серію, оскільки проблема складніша, ніж ви думаєте. Насправді він зробив цю серію, оскільки натрапив на цю проблему дизайну, розробляючи компілятор Roslyn C #.
CodingYoshi

2

Ніщо не заважає розробнику цього робити. Насправді Ерік Ліпперт спробував багато різних методик, але всі вони мали слабкі сторони. У цьому і полягала вся суть цієї серії в тому, що зупинити розробника від цього непросто, і все, що він намагався, мало недоліки. Нарешті він вирішив, що використовувати Commandоб’єкт із правилами - це шлях.

За допомогою правил ви можете встановити Weaponвластивість a Wizardбути a, Swordале коли ви попросите, Wizardщоб він мав зброю (Меч) і нападав на неї, це не матиме жодного ефекту, і тому це не змінить жодного стану. Як він каже нижче:

У нас є ще одне правило, яке посилює перше правило, яке говорить про те, що ефекти першого правила не застосовуються, коли майстер намагається обрушити меч. Ефекти для такої ситуації "видають сумний звук тромбону, користувач втрачає свою дію за цей поворот, жоден ігровий стан не змінюється

Іншими словами, ми не можемо застосувати таке правило через typeстосунки, які він намагався різними способами, але або не сподобався, або не працював. Таким чином, єдине, що він сказав, що ми можемо зробити, - це зробити щось з цим під час виконання. Кидати виняток було не корисно, оскільки він не вважає це винятком.

Нарешті він вирішив піти з вищезазначеним рішенням. Це рішення в основному говорить про те, що ви можете встановити будь-яку зброю, але коли ви отримаєте її, як не правильну зброю, це було б по суті марним. Але не виняток буде кинутий.

Я думаю, що це гарне рішення. Хоча в деяких випадках я б також пішов із схемою спробувати.


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 Він говорить це побічно. Я скопіював цю частину у своїй відповіді і процитував її. Ось знову. У цитаті він говорить: коли чарівник намагається обвести меч. Як майстер може володіти мечем, якщо меч не був встановлений? Це, мабуть, було встановлено. Тоді, якщо майстер володіє мечем . Ефекти для такої ситуації "видають сумний звук тромбона, користувач втрачає свою дію за цей поворот
CodingYoshi

Гм, я думаю, що володіння мечами в основному означає, що його потрібно встановити, ні? Коли я читаю цей абзац, я тлумачу, що дія першого правила є 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

Першим відхиленим рішенням автора було представлення правил системою типів. Система типів оцінюється під час компіляції. Якщо ви від'єднаєте правила від системи типів, вони більше не перевіряються компілятором, тому нічого не заважає розробнику зробити помилку як такою.

Але з цією проблемою стикається кожна логіка / моделювання, що не перевіряється компілятором, і загальною відповіддю на це є (одиничне) тестування. Тому запропоноване автором рішення потребує міцного тестового використання, щоб обійти помилки розробників. Щоб підкреслити цю точку необхідності міцного тестового використання для помилок, виявлених лише під час виконання, перегляньте цю статтю Брюса Еккеля, яка робить аргумент, що вам потрібно обмінятись сильним типізацією для більш сильного тестування в динамічних мовах.

На закінчення, єдине, що може завадити розробникам помилитися - це набір (одиничних) тестів, які перевіряють, чи дотримуються всі правила.


Ви повинні використовувати API, і API повинен гарантувати, що ви будете робити тестування одиниць або робити це припущення? Вся проблема полягає в моделюванні, тому модель не ламається, навіть якщо розробник, який використовує модель, недбалий.
CodingYoshi

1
Я намагався зробити те, що ніщо не заважає розробнику помилитися. У запропонованому рішенні правила відлучаються від даних, тому, якщо ви не створюєте власні чеки, нічого не заважає використовувати об’єкти даних без застосування правил.
larsbe

1

Я, можливо, тут пропустив тонкощі, але я не впевнений, що проблема полягає в системі типів. Можливо, це з умовою в C #.

Наприклад, ви можете зробити цей повністю безпечним типом, зробивши Weaponзахищений сетер Player. Потім додати setSword(Sword)і setStaff(Staff)до Warriorі , Wizardвідповідно , цього виклику захищеного сетера.

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


Ерік Ліпперт не хотів кидати винятків. Ви читали серіал? Рішення повинно відповідати вимогам, і ці вимоги чітко викладені у серії.
CodingYoshi

@CodingYoshi Чому це кине виняток? Це безпечний тип, тобто перевіряється під час компіляції.
Олексій

Вибачте, не міг змінити мій коментар, як тільки я зрозумів, що ви не кидаєте хвилювання. Однак ви порушили спадщину, зробивши це. Дивіться проблему, яку намагався вирішити автор, - ви не можете просто додати такий метод, як ви робили, тому що зараз типи не можна лікувати поліморфно.
CodingYoshi

@CodingYoshi Поліморфна вимога полягає у тому, що у гравця є зброя. І в цій схемі Гравець справді має зброю. Жодна спадщина не порушена. Це рішення буде компілюватися лише в тому випадку, якщо ви правильно отримаєте правила.
Олексій

@CodingYoshi Тепер, це не означає, що ви не можете написати код, який потребував би перевірки виконання, наприклад, якщо ви намагаєтеся додати Weaponа Player. Але не існує системи типів, де ви б не знали конкретних типів під час компіляції, які могли б діяти на ці типи бетону під час компіляції. За визначенням. Ця схема означає, що вирішувати потрібно лише той випадок, коли цей час справді кращий за будь-яку схему Еріка.
Олексій

0

Отже, що заважає розробнику робити це? Вони просто повинні пам’ятати, щоб цього не робити?

Це питання фактично те саме, що стосується досить священної війни під назвою " куди слід перевірити " (швидше за все, відзначаючи і DDD).

Отже, перш ніж відповісти на це запитання, слід запитати себе: яка природа правил, яких ви хочете дотримуватися? Чи вирізані вони з каменю та визначають цілість? Чи призводить до порушення цих правил суб'єкт господарювання перестає бути таким, яким він є? Якщо так, поряд із збереженням цих правил у валідації команд , також введіть їх у сутність. Тож якщо розробник забуде перевірити команду, ваші сутності не знаходяться в недійсному стані.

Якщо ні - добре, це по суті означає, що ці правила є специфічними для команд і не повинні розміщуватися в доменних об'єктах. Таким чином, порушення цих правил призводить до дій, які не повинні були бути дозволені, але не в недійсному стані моделі.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.