마법사와 전사의 규칙 우회


9

에서 블로그 게시물이 시리즈 , 에릭 Lippert의이 마법사와 예제로 전사를 사용하여 객체 지향 설계에서 문제를 설명 :

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마법사가 직원 만 사용할 수 있도록 클래스를 책임지게 함) 계속해서 발생하는 문제를 보여줍니다 . Liskov 대체 원칙을 위반하거나 런타임 예외 위험이 발생하거나 확장하기 어려운 코드가 생길 수 있습니다.

그가 생각해 낸 해결책은 Player 클래스가 유효성을 검사하지 않는다는 것입니다. 상태를 추적하는 데만 사용됩니다. 그런 다음 플레이어에게 무기를주는 대신 :

player.Weapon = new Sword();

상태는 Commands에 의해 s에 따라 수정됩니다 Rule.

... 우리 는 두 개의 게임 상태 객체 a 와 a 를 취하는 Command객체 를 만듭니다 . 사용자가 시스템에 "이 마법사는 그 검을 휘 두어야합니다"명령을 발행하면 해당 명령은 일련의 의 컨텍스트에서 평가되어 일련의 s를 생성합니다 . 우리는 하나가 플레이어 시도가 무기를 휘두를 때, 효과가 기존의 무기가 존재하는 경우, 삭제하고 새로운 무기가 플레이어의 무기가되고 있다는 것을 말한다. 첫 번째 규칙을 강화하는 또 다른 규칙이 있는데, 이는 마법사가 검을 휘두를 때 첫 번째 규칙의 효과가 적용되지 않는다는 것입니다.WieldPlayerWeaponRuleEffectRule

저는이 아이디어가 원칙적으로 마음에 들지만 실제로 어떻게 사용되는지에 대해 우려하고 있습니다.

아무것도를 우회하는 개발자를 방지하기 위해 보인다 CommandsRule간단하게 설정하여이야 Weapon켜짐 Player. Weapon속성은 액세스 할 수 필요 Wield가 할 수 없도록 명령 private set.

그럼, 수행 이 일에서 개발자를 방지? 그들은 단지 기억하지 않아야합니까?


2
이 질문은 언어 별 (C #)이라고 생각하지 않습니다. 실제로 OOP 디자인에 대한 질문이기 때문입니다. C # 태그 제거를 고려하십시오.
Maybe_Factor

1
게시 된 코드가 c #이므로 @maybe_factor c # 태그가 적합합니다.
코딩 요시

@EricLippert에게 직접 물어 보지 않겠습니까? 그는 때때로이 사이트에서이 사이트에 등장하는 것 같습니다.
Doc Brown

@Maybe_Factor-C # 태그를 흔들었지만 언어 별 솔루션이있는 경우이를 유지하기로 결정했습니다.
벤 L

1
@DocBrown-나는 그의 블로그 에이 질문을 게시했습니다 (이틀 전에는 인정했습니다. 응답을 오래 기다리지 않았습니다). 여기에 내 질문을 집중시킬 수있는 방법이 있습니까?
벤 L

답변:


9

일련의 블로그 게시물이 이끌어내는 모든 주장은 Part 5에 있습니다 .

우리는 C # 유형 시스템이 Dungeons & Dragons의 규칙을 인코딩하기에 충분한 일반성을 갖도록 설계되었다고 믿을 이유가 없습니다. 왜 우리도 시도하고 있습니까?

우리는“시스템의 규칙을 표현하는 코드는 어디에 있습니까?”라는 문제를 해결했습니다. 게임 상태를 나타내는 개체가 아니라 시스템의 규칙을 나타내는 개체로 들어갑니다. 상태 객체의 관심사는 게임 규칙을 평가하는 것이 아니라 상태를 일관성있게 유지하는 것입니다.

무기, 캐릭터, 몬스터 및 기타 게임 개체는 할 수 있거나 할 수없는 것을 확인할 책임이 없습니다. 규칙 시스템이이를 담당합니다. Command개체 중 하나를 게임 오브젝트에 아무것도되지 않습니다. 그것은 그들과 함께 무언가를 하려는 시도 를 나타냅니다 . 그런 다음 규칙 시스템은 명령이 가능한지 확인하고 규칙 시스템 인 경우 게임 오브젝트에서 적절한 메소드를 호출하여 명령을 실행합니다.

개발자가 첫 번째 규칙 시스템이 허용하지 않는 문자 및 무기로 작업을 수행 하는 두 번째 규칙 시스템 을 만들려는 경우 C #에서는 불쾌한 반사 해킹없이 메소드 호출이 어디 있는지 알 수 없기 때문에 그렇게 할 수 있습니다 에서.

일부 상황 에서 작동 할 있는 해결 방법 은 게임 오브젝트 (또는 해당 인터페이스)를 규칙 엔진을 사용하여 하나의 어셈블리에 배치하고 모든 뮤 테이터 메소드를로 표시하는 것 입니다. 게임 오브젝트에 대한 읽기 전용 액세스가 필요한 모든 시스템은 다른 어셈블리에 있으므로 메소드 에만 액세스 할 수 있습니다 . 이것은 여전히 ​​서로의 내부 메소드를 호출하는 게임 오브젝트의 허점을 남깁니다. 그러나 게임 객체 클래스가 멍청한 국가 보유자라는 데 동의했기 때문에 코드 냄새가 분명합니다.internalpublic


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
}

요약 : 요구 사항과 요구 사항 만 모델링하십시오. oo 모델링이 아닌 데이터 모델링을 수행하지 마십시오.


1
시리즈를 읽었습니까? 해당 시리즈의 저자에게 데이터가 아니라 요구 사항을 모델링하도록 지시 할 수 있습니다. 귀하의 답변에 요구 사항은 C # 컴파일러를 빌드 할 때 작성자가 요구 한 요구 사항이 아닌 구성 요구 사항입니다.
CodingYoshi

2
Eric Lippert는 해당 시리즈의 기술적 문제를 자세히 설명하고 있습니다. 이 질문은 C # 기능이 아니라 실제 문제에 관한 것입니다. 내 요점은 실제 프로젝트에서 우리는 관계와 속성을 가정 하지 않고 비즈니스 요구 사항 (내가 예를 들어 만든 예)을 따라야 한다고 가정 합니다. 그것이 당신에게 맞는 모델을 얻는 방법입니다. 어느 것이 quesetion이었습니다.
Robert Bräutigam

그 시리즈를 읽을 때 처음으로 생각한 것입니다. 저자는 방금 추상을 생각해 냈으며 더 이상 평가하지 않고 그냥 고집했습니다. 반복적으로 문제를 기계적으로 해결하려고 노력합니다. 유용한 영역과 추상화에 대해 생각하는 대신, 먼저 시작해야합니다. 내 공감.
Vadim Samokhin

이것이 정답입니다. 이 기사는 상충되는 요구 사항을 표현하고 (한 요구 사항은 플레이어가 [모든] 무기를 사용할 수 있고 다른 요구 사항은 그렇지 않다고 말합니다.) 시스템이 충돌을 올바르게 표현하는 것이 얼마나 어려운지를 자세히 설명합니다. 정답은 갈등을 제거하는 것입니다. 이 경우 플레이어가 무기를 사용할 수 있어야한다는 요구 사항이 제거됩니다.
Daniel T.

2

한 가지 방법은 Wield명령을 에 전달하는 것 Player입니다. 플레이어는 다음 실행 Wield명령을하는 검사 적절한 규칙과 반환 Weapon(가), Player다음으로 자신의 무기 필드를 설정합니다. 이런 식으로 무기 필드는 전용 세터를 가질 수 있으며 Wield플레이어 에게 명령을 전달해야만 설정할 수 있습니다 .


실제로 이것은 문제를 해결하지 못합니다. 커맨드 오브젝트를 만드는 개발자는 무기를 전달할 수 있으며 플레이어는이를 설정합니다. 생각보다 문제가 더 어렵 기 때문에 시리즈를 읽으십시오. 실제로 Roslyn C # 컴파일러를 개발하는 동안이 디자인 문제가 발생했기 때문에이 시리즈를 만들었습니다.
CodingYoshi

2

개발자가 그렇게하는 것을 막는 것은 없습니다. 실제로 Eric Lippert는 다양한 기술을 시도했지만 모두 약점을 가지고있었습니다. 그 시리즈의 핵심은 개발자가 그렇게하지 못하게하는 것이 쉽지 않았으며 시도한 모든 것에 단점이 있다는 것입니다. 마지막으로 Command규칙이 있는 객체 를 사용하는 것이 좋습니다.

규칙을 사용하면 Weapona의 속성을 a Wizard로 설정할 수 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.이 시리즈에서 찾지 못했습니다.이 솔루션이 제안 된 곳을 알려 주시겠습니까?
Vadim Samokhin

@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.은 무기가 칼인지 확인하는 규칙이 있다고 생각하므로,이 무기로 휘두를 수 없기 때문에 설정되지 않습니다. 대신 슬픈 트롬본 소리가납니다.
Vadim Samokhin

내 생각에는 일부 명령 규칙을 위반하는 것이 이상 할 것입니다. 문제가 흐려지는 것과 같습니다. 규칙을 이미 위반하고 마법사를 잘못된 상태로 설정 한 후이 문제를 처리하는 이유는 무엇입니까? 마법사는 칼을 가질 수는 없지만 그렇게합니다! 왜 그런 일이 일어나지 않습니까?
Vadim Samokhin

Wield여기서 해석하는 방법에 대해서는 @Zapadlo에 동의합니다 . 나는 그것이 명령의 약간 오해의 소지가 있다고 생각합니다. 같은 뭔가 ChangeWeapon더 정확한 것입니다. 무기를 설정할 수 있는 다른 모델을 가질 수 있지만 올바른 무기가 아니라면 무기를 생산할 때 본질적으로 쓸모가 없습니다 . 흥미로운 것 같지만 에릭 리퍼 트가 묘사 한 것 같지는 않습니다.
벤 L

2

저자의 첫 번째 폐기 솔루션은 유형 시스템별로 규칙을 나타내는 것이 었습니다. 형식 시스템은 컴파일 타임에 평가됩니다. 유형 시스템에서 규칙을 분리하면 더 이상 컴파일러에서 검사하지 않으므로 개발자가 실수를 저지르는 것을 막을 수있는 것은 없습니다.

그러나이 문제는 컴파일러가 확인하지 않는 모든 로직 / 모델링이 직면하며 이에 대한 일반적인 대답은 (단위) 테스트입니다. 따라서 저자가 제안한 솔루션에는 개발자의 오류를 피하기 위해 강력한 테스트 장치가 필요합니다. 런타임시에만 감지되는 오류에 대해 강력한 테스트 하네스가 필요한 시점을 강조하려면 동적 언어에서 강력한 테스트를 위해 강력한 타이핑을 교환해야한다는 Bruce Eckel 의이 기사를 참조하십시오.

결론적으로 개발자가 실수를 저 지르지 못하게 할 수있는 유일한 방법은 모든 규칙이 준수되는지 확인하는 일련의 (단위) 테스트를하는 것입니다.


API를 사용해야하고 API는 단위 테스트를 수행하거나 그 가정을 수행해야합니까? 모든 도전은 모델링에 관한 것이므로 모델을 사용하는 개발자가 부주의하더라도 모델이 중단되지 않습니다.
코딩 요시

1
내가 만들려고했던 요점 은 개발자가 실수를 저지르는 것을 막을 수있는 것이 없다는 것 입니다. 제안 된 솔루션에서 규칙은 데이터에서 분리되므로 직접 검사를 작성하지 않으면 규칙을 적용하지 않고 데이터 오브젝트를 사용할 수 없습니다.
larsbe

1

여기서 미묘한 부분을 놓쳤을 수도 있지만 문제가 형식 시스템에 있는지 확실하지 않습니다. 어쩌면 C #에서 규칙이있을 수 있습니다.

예를 들어에서 Weaponsetter를 보호 하여이 유형을 완전히 안전하게 만들 수 있습니다 Player. 그런 다음 추가 setSword(Sword)setStaff(Staff)WarriorWizard그 전화 보호 세터 각각.

이렇게하면 Player/ Weapon관계가 정적으로 검사되며 신경 쓰지 않는 코드는 a Player를 사용 하여을 얻을 수 있습니다 Weapon.


Eric Lippert는 예외를 던지기를 원하지 않았습니다. 시리즈를 읽었습니까? 솔루션은 요구 사항을 충족해야하며이 요구 사항은 시리즈에 명확하게 명시되어 있습니다.
CodingYoshi

@CodingYoshi 왜 이것이 예외를 던지나요? 형식이 안전합니다. 즉 컴파일 타임에 확인할 수 있습니다.
Alex

당신이 예외를 던지고 있지 않다는 것을 알게되면 죄송합니다 내 의견을 변경할 수 없습니다. 그러나 당신은 그렇게함으로써 상속을 끊었습니다. 저자가 해결하려고했던 문제를보십시오. 이제 유형을 다형성으로 처리 할 수 ​​없기 때문에 당신이했던 것처럼 메소드를 추가 할 수 없다는 것입니다.
CodingYoshi

@CodingYoshi 다형성 요구 사항은 플레이어에게 무기가 있어야한다는 것입니다. 그리고이 계획에서 플레이어는 실제로 무기를 가지고 있습니다. 상속이 끊어지지 않습니다. 이 솔루션은 규칙을 올바르게 얻은 경우에만 컴파일됩니다.
Alex

@CodingYoshi 이제, 그건 당신이를 추가하려고하면 런타임 검사 등을 필요로 쓰기하지 코딩 할 수 있습니다 의미하지 않는다 WeaponA를을 Player. 그러나 컴파일 타임에 콘크리트 유형에 대해 알 수있는 콘크리트 유형을 컴파일 타임에 알 수 없는 유형 시스템 은 없습니다 . 정의에 의해. 이 체계는 런타임에 처리해야하는 경우에만 해당한다는 것을 의미합니다. 실제로 Eric의 체계보다 낫습니다.
알렉스

0

그렇다면 개발자가 이것을 방해하는 것은 무엇입니까? 그들은 단지 기억하지 않아야합니까?

이 질문은 " 검증을 어디에 두어야합니까?"(대부분 ddd도 주목할 것) 라는 매우 거룩한 주제와 사실상 동일 합니다.

따라서이 질문에 대답하기 전에 스스로에게 묻어 야합니다. 따라야 할 규칙의 성격은 무엇입니까? 그들은 돌로 새겨지고 실체를 정의합니까? 이러한 규칙을 어기면 실체가 중지 되는가? 그렇다면 이러한 규칙을 명령 유효성 검사 로 유지하면서 엔티티에도 넣으십시오. 따라서 개발자가 명령 유효성 검사를 잊어 버린 경우 엔티티는 유효하지 않은 상태가 아닙니다.

그렇지 않은 경우 본질적으로이 규칙은 명령에 따라 다르며 도메인 엔터티에 상주해서는 안됩니다. 따라서이 규칙을 위반하면 수행 할 수 없었지만 잘못된 모델 상태가 아닌 작업이 수행됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.