OOP에서 순환 참조가 필요한이 실제 활동을 모델링하는 올바른 방법은 무엇입니까?


24

순환 참조에 대한 Java 프로젝트의 문제로 씨름하고 있습니다. 문제의 객체가 상호 의존적이며 서로에 대해 알아야 할 실제 상황을 모델링하려고합니다.

이 프로젝트는 보드 게임을하는 일반적인 모델입니다. 기본 수업은 비 특정이지만 체스, 주사위 놀이 및 기타 게임의 세부 사항을 다루기 위해 확장됩니다. 나는 11 년 전에 이것을 십여 개의 다른 게임으로 애플릿으로 코딩했지만 문제는 순환 참조로 가득하다는 것입니다. 얽힌 모든 클래스를 단일 소스 파일에 채워서 다시 구현했지만 Java에서는 잘못된 형식이라는 생각을 얻었습니다. 이제 Android 앱과 비슷한 것을 구현하고 올바르게 작업하고 싶습니다.

수업은 다음과 같습니다.

  • RuleBook : 보드의 초기 레이아웃, 먼저 이동 한 사람과 같은 다른 초기 게임 상태 정보, 사용 가능한 이동, 제안 된 이동 후 게임 상태에 대한 평가 및 평가 현재 또는 제안 된 보드 위치.

  • 보드 : 이동을 반영하도록 지시 할 수있는 게임 보드의 간단한 표현입니다.

  • MoveList : 이동 목록. 이것은 두 가지 목적입니다 : 주어진 지점에서 사용 가능한 이동 선택 또는 게임에서 만들어진 이동 목록. 그것은 거의 동일한 두 클래스로 나눌 수 있지만 그것은 내가 묻는 질문과 관련이 없으며 더 복잡 할 수 있습니다.

  • 이동 : 단일 이동. 여기에는 원자 목록으로 이동에 대한 모든 것이 포함됩니다. 여기에서 조각을 집어 들고, 거기에 놓고, 캡처 된 조각을 제거하십시오.

  • 상태 : 진행중인 게임의 전체 상태 정보. 보드 위치뿐만 아니라 MoveList 및 현재 이동할 사람과 같은 기타 상태 정보도 있습니다. 체스에서 각 플레이어의 왕과 루크가 이동했는지 여부를 기록합니다.

예를 들어, 순환 참조는 많이 있습니다. RuleBook은 주어진 시간에 어떤 움직임을 사용할 수 있는지 결정하기 위해 게임 상태에 대해 알아야하지만, 게임 상태는 초기 시작 레이아웃과 움직임에 따른 부작용에 대해 RuleBook을 쿼리해야합니다. 그것은 만들어집니다 (예 : 누가 다음으로 이동).

모든 것에 대해 알아야 할 RuleBook을 맨 위에 놓고 새로운 클래스 세트를 계층 적으로 구성하려고했습니다. 그러나 이로 인해 많은 메소드를 RuleBook 클래스 (예 : 이동)로 옮겨야하므로 모 놀리식이되고 특히 RuleBook이 무엇인지를 대표하지는 않습니다.

그렇다면 이것을 구성하는 올바른 방법은 무엇입니까? 실제 게임을 정확하게 모델링하려는 시도를 포기하면서 순환 참조를 피하기 위해 RuleBook을 BigClassThatDoesAlmostEverythingInTheGame으로 바꿔야합니까? 아니면 상호 의존적 인 클래스를 고수하고 컴파일러를 컴파일하여 실제 모델을 유지해야합니까? 아니면 내가 누락 된 명백한 유효한 구조가 있습니까?

도움을 주셔서 감사합니다.


7
어떤 경우는 RuleBook를 예했습니다 State인수로하고 유효한 반환 MoveList즉, "다음에 무엇을 할 수 있는지, 우리가 지금 어디에 여기를?"
jonrsharpe

@ jonrsharpe가 말한 것. 실제 보드 게임을 할 때 규칙 책은 실제 게임에 대해 알지 못합니다. 실제로 움직임을 계산하기 위해 다른 클래스를 소개 할 수도 있지만,이 RuleBook 클래스가 이미 얼마나 큰지에 따라 달라질 수 있습니다.
세바스티안 반 덴 브 ek

4
신 오브젝트 (BigClassThatDoesAlmostEverythingInTheGame)를 피하는 것은 순환 참조를 피하는 것보다 훨씬 중요합니다.
user281377

2
@ user281377 그러나 반드시 상호 배타적 인 목표는 아닙니다!
jonrsharpe

1
모델링 시도를 보여줄 수 있습니까? 예를 들어 도표?
사용자

답변:


47

순환 참조에 대한 Java 프로젝트의 문제로 씨름하고 있습니다.

Java의 가비지 수집기는 참조 계산 기술에 의존하지 않습니다. 순환 참조는 Java에서 어떤 종류의 문제도 일으키지 않습니다. Java에서 완벽하게 자연 순환 참조를 제거하는 데 소요되는 시간은 시간 낭비입니다.

나는 이것을 [...] 코딩했지만 문제는 순환 참조로 가득하다는 것입니다. 나는 하나의 소스 파일에 얽힌 모든 클래스를 채워서 다시 구현했다 .

필요하지 않습니다. 모든 소스 파일을 한 번에 컴파일하면 (예 javac *.java:) 컴파일러는 모든 순방향 참조를 문제없이 해결합니다.

또는 상호 의존적 인 클래스를 고수하고 컴파일러를 컴파일하여 어떻게 든 컴파일해야합니까 [...]

예. 응용 프로그램 클래스는 상호 의존적이어야합니다. 영리한 해킹 아닌 한 번에 같은 패키지에 속하는 모든 Java 소스 파일을 컴파일, 그것은 정확하게 자바하는 방법 가정 일에.


24
"순환 참조는 Java에서 어떤 종류의 문제도 일으키지 않습니다." 컴파일 측면에서 이것은 사실입니다. 그러나 순환 참조는 잘못된 설계간주됩니다 .
Chop

22
순환 참조는 많은 상황에서 완벽하게 자연 스럽기 때문에 Java와 다른 현대 언어는 간단한 참조 카운터 대신 정교한 가비지 수집기를 사용합니다.
user281377

3
Java가 순환 참조를 해결할 수있는 것은 훌륭하며 많은 상황에서 자연스럽게 참조되는 것은 사실입니다. 그러나 OP는 특정 상황을 제시했으며 고려해야 할 상황입니다. 얽힌 스파게티 코드는 아마도이 문제를 처리하는 가장 좋은 방법은 아닙니다.
Matthew 읽기

3
관련이없는 프로그래밍 언어에 대해 입증되지 않은 FUD를 배포하지 마십시오. 파이썬은 오래 전부터 GC 참조주기를 지원해 왔습니다 ( docs , SO : herehere ).
Christian Aichinger 11:30에

2
IMHO이 답변은 평범한 것입니다. 왜냐하면 OP의 예와 같이 순환 참조에 관한 단어가 하나도 없기 때문입니다.
Doc Brown

22

물론, 순환 의존성은 디자인 관점에서 의문의 여지가 있지만 금지되지는 않으며 순수한 기술적 관점에서 볼 때 반드시 문제 가되지는 않습니다 . 대부분의 시나리오는 경우에 따라 불가피하며, 드물게는 유용한 정보로 간주 될 수도 있습니다.

실제로 Java 컴파일러가 순환 종속성을 거부하는 시나리오는 거의 없습니다. (참고 : 더 많은 것이있을 수 있습니다. 지금은 다음에 대해서만 생각할 수 있습니다.)

  1. 상속 : 클래스 A는 클래스 B를 확장 할 수 없으며 클래스 B는 클래스 A를 확장합니다. 이는 대안이 논리적 인 관점에서 절대적으로 의미가 없으므로 이것을 가질 수 없다는 것이 합리적입니다.

  2. 메서드-로컬 클래스 중 : 메서드 내에서 선언 된 클래스는 서로 순환 적으로 참조 할 수 없습니다. 이것은 아마도 Java 컴파일러의 한계 일뿐입니다. 아마도 그러한 일을 할 수있는 능력은 그것을 지원하기 위해 컴파일러로 들어가야하는 추가 복잡성을 정당화하기에 충분하지 않기 때문에 가능합니다. (대부분의 Java 프로그래머는 메소드 내에서 클래스를 선언하고 여러 클래스를 선언 한 다음이 클래스를 서로 순환 참조 할 수 있다는 사실조차 알지 못합니다.)

따라서 순환 종속성을 최소화하기위한 노력이 기술적 정확성을 추구하는 것이 아니라 설계 순도를 추구하는 것임을 깨닫고 빠져 나가는 것이 중요합니다.

내가 아는 한 순환 종속성을 제거하는 환원 주의적 접근 방법이 없다는 것은 순환 참조가있는 시스템을 가져 와서 차례로 적용하고 끝내기위한 간단한 미리 결정된 "두뇌가없는"단계로 구성된 레시피가 없다는 것을 의미합니다. 순환 참조가없는 시스템으로 마음을 움직여야하며 디자인의 특성에 따라 리팩토링 단계를 수행해야합니다.

당신이 가지고있는 특정한 상황에서, 당신이 필요로하는 것은 아마도 다른 모든 엔티티를 아는 "Game"또는 "GameLogic"이라고 불리는 새로운 엔티티 일 것입니다. ) 다른 엔티티가 서로를 알 필요가 없습니다.

예를 들어, RuleBook 엔터티는 GameState 엔터티에 대해 무엇이든 알아야 할 필요가 있습니다. 규칙 북은 재생하기 위해 우리가 상담하는 것이기 때문에 재생에 적극적으로 참여하는 것이 아닙니다. 따라서이 새로운 "게임"엔티티는 어떤 움직임이 이용 가능한지 결정하기 위해 룰북과 게임 상태를 모두 참조해야하며, 이는 순환 의존성을 제거합니다.

이제이 접근 방식으로 문제가 어떻게 될지 짐작할 수 있습니다. "게임"엔터티를 게임과 무관하게 코딩하는 것은 매우 어려울 것입니다. "RuleBook"및 "Game"엔티티와 같이 각기 다른 게임 유형에 대해 맞춤형 구현이 필요한 엔티티. 이는 우선 "RuleBook"엔티티를 갖는 목적을 상실합니다. 글쎄, 이것에 대해 말할 수있는 것은 아마도 많은 다른 유형의 게임을 할 수있는 시스템을 작성하려는 초기의 열망은 고귀했지만 아마도 생각조차 못했을 것이라는 것입니다. 내가 당신의 입장이라면 모든 다른 게임의 상태를 표시하는 일반적인 메커니즘과 이러한 모든 게임에 대한 사용자 입력을받는 일반적인 메커니즘을 사용하는 데 집중했을 것입니다.


1
고마워 마이크. 귀하는 Game 엔터티의 단점에 대해 옳습니다. 이전 애플릿 코드를 사용하여 새로운 RuleBook 서브 클래스와 적절한 그래픽 디자인으로 새로운 게임을 만들 수있었습니다.
Damian Walker

10

게임 이론은 게임을 이전 이동 목록 (플레이 한 사람을 포함한 값 유형) 및 ValidMoves (previousMoves) 함수로 취급합니다.

게임의 UI가 아닌 부분에 대해이 패턴을 따르고 보드 설정과 같은 것을 움직임으로 취급합니다.

그런 다음 UI는 논리를 참조하는 한 가지 방법으로 표준 OO 항목이 될 수 있습니다.


의견을 요약하여 업데이트

체스를 고려하십시오. 체스 게임은 일반적으로 움직임 목록으로 기록됩니다. http://en.wikipedia.org/wiki/Portable_Game_Notation

움직임 목록은 보드의 그림보다 게임의 완전한 상태를 훨씬 잘 정의합니다.

예를 들어 Board, Piece, Move 등의 객체 및 Piece.GetValidMoves ()와 같은 메서드를 만들기 시작한다고 가정 해 봅시다.

먼저 보드를 조각 참조해야한다는 것을 알지만, 캐슬 링을 고려합니다. 이미 왕이나 루크를 옮기지 않은 경우에만 할 수 있습니다. 따라서 우리는 왕과 루크에게 MovedAlready 플래그가 필요합니다. 마찬가지로 폰은 첫 번째 움직임에서 2 개의 사각형을 움직일 수 있습니다.

그런 다음 우리는 왕의 유효한 움직임을 성을 짓는 데는 루크의 존재와 상태에 달려 있으므로 보드에는 조각이 있고 그 조각을 참조해야합니다. 우리는 당신의 순환 참조 문제에 빠지고 있습니다.

그러나 Move를 불변의 구조체 및 게임 상태로 이전 동작 목록으로 정의하면 이러한 문제가 사라집니다. 캐슬 링이 유효한지 확인하기 위해 캐슬 및 킹 무브의 존재 목록을 확인할 수 있습니다. 폰이 en-passent 할 수 있는지 확인하기 위해 다른 폰이 이전에 두 번 움직 였는지 확인할 수 있습니다. 규칙-> 이동 이외의 참조는 필요하지 않습니다.

이제 체스에는 정적 보드가 있으며, 피스는 항상 같은 방식으로 설정됩니다. 그러나 대체 설정을 허용하는 변형이 있다고 가정 해 봅시다. 핸디캡으로 일부 조각을 생략했을 수도 있습니다.

'상자에서 정사각형 X까지'이동으로 설정 이동을 추가하고 해당 이동을 이해하기 위해 규칙 객체를 수정하면 게임을 일련의 이동으로 나타낼 수 있습니다.

마찬가지로 게임에서 보드 자체가 정적이 아닌 경우 체스에 사각형을 추가하거나 보드에서 사각형을 제거하여 가로 질러 이동할 수 없습니다. 이러한 변경 사항은 규칙 엔진의 전체 구조를 변경하거나 유사한 보드 설정 개체 를 참조하지 않고도 이동으로 나타낼 수 있습니다.


이로 인해 ValidMoves 구현이 복잡해져 로직 속도가 느려집니다.
Taemyr

실제로는 보드 설정이 가변적이라고 가정하므로 어떻게 든 정의해야합니다. 설정을 변환하면 계산을 돕기 위해 다른 구조체 또는 객체로 이동하면 필요한 경우 결과를 캐시 할 수 있습니다. 일부 게임에는 보드가 있으며 플레이에 따라 변경 될 수 있으며 일부 유효한 이동은 현재 위치가 아닌 이전 이동에 따라 달라질 수 있습니다 (예 : 체스에서 캐슬 링)
Ewan

1
깃발과 물건을 추가하는 것은 이동 기록만으로 피할 수있는 복잡성입니다. 현재 보드 설정을 얻기 위해 100 개의 체스 이동을 반복하는 데 비용이 많이 들지 않으며 이동 간 결과를 캐시 할 수 있습니다.
Ewan

1
규칙을 반영하도록 객체 모델을 변경하지 않아도됩니다. 즉, 체스의 경우, validMoves-> Piece + Board를 만들면 주조, 실패, 폰 및 피스 프로모션을 위해 먼저 이동하며 오브젝트에 추가 정보를 추가하거나 세 번째 오브젝트를 참조해야합니다. 또한 누가 가고 있는지와 발견 된 수표와 같은 개념에 대한 아이디어를 잃어 버립니다
Ewan

1
@Gabe The boardLayout는 all의 함수입니다 priorMoves(즉, 상태로 유지했다면 각각 이외의 것은 기여하지 않습니다 thisMove). 따라서 이완의 제안은 본질적으로 "중세 인을 잘 라라"이다 validMoves( boardLayout( priorMoves ) ).
OJFord

8

객체 지향 프로그래밍에서 두 클래스 사이의 순환 참조를 제거하는 표준 방법은 그 중 하나에 의해 구현 될 수있는 인터페이스를 도입하는 것입니다. 따라서 귀하의 경우 어느 것을 RuleBook참조 할 수 있었을 것입니다 (이는 인터페이스로 구현 됨 ). 또한 테스트 목적으로 다른 (아마도 더 간단한) 초기 위치를 사용하는 이미지 를 만들 수 있으므로 테스트가 더 쉬워집니다 .StateInitialPositionProviderRuleBookState


6

나는 게임 흐름의 제어를 게임의 상태 및 규칙 모델에서 분리함으로써 귀하의 경우 순환 참조 및 신 객체를 쉽게 제거 할 수 있다고 생각합니다. 그렇게하면 많은 유연성을 얻을 수 있고 불필요한 복잡성을 제거 할 수 있습니다.

규칙 책이나 게임 상태에 이러한 책임을 부여하는 대신 게임 흐름을 제어하고 실제 상태 변경을 처리하는 컨트롤러 (원하는 경우 "게임 마스터")가 있어야한다고 생각합니다.

게임 상태 오브젝트는 스스로 변경하거나 규칙을 인식 할 필요가 없습니다. 이 클래스는 응용 프로그램의 나머지 부분에 대해 쉽게 처리 (작성, 검사, 변경, 지속, 기록, 복사, 캐시 등)하고 효율적인 게임 상태 객체의 모델을 제공하면됩니다.

규칙 책은 진행중인 게임에 대해 알지 못하거나 바이올린을 사용하지 않아도됩니다. 어떤 움직임이 합법적인지 알 수 있으려면 게임 상태에 대한 관점 만 있으면되며, 움직임이 게임 상태에 적용될 때 어떤 일이 발생하는지 물으면 결과적인 게임 상태로 응답하면됩니다. 또한 초기 레이아웃을 요청할 때 게임 시작 상태를 제공 할 수도 있습니다.

컨트롤러는 게임 상태와 규칙 책 및 게임 모델의 다른 객체를 알고 있어야하지만 세부 정보를 망칠 필요는 없습니다.


4
내 생각이야 OP는 같은 클래스에서 너무 많은 데이터절차 를 혼합하고 있습니다 . 이것들을 더 많이 나누는 것이 좋습니다. 이것은 주제에 대한 좋은 이야기입니다. Btw, "게임 상태보기"를 읽을 때 "기능에 대한 주장"이라고 생각합니다. 내가 할 수 있다면 +100
jpmc26

5

여기서 문제는 어떤 클래스가 어떤 작업을 처리해야하는지에 대한 명확한 설명이 없다는 것입니다. 각 수업이 무엇을해야하는지에 대한 좋은 설명이라고 생각하고 아이디어를 보여주는 일반적인 코드의 예를 들어 보겠습니다. 우리는 코드가 덜 결합되어 있으므로 실제로 순환 참조가 없습니다.

각 클래스의 기능을 설명하는 것으로 시작하겠습니다.

GameState클래스에는 게임의 현재 상태에 대한 정보 만 포함되어야합니다. 게임의 과거 상태 또는 향후 움직임에 대한 정보는 포함하지 않아야합니다. 여기에는 체스의 사각형에 어떤 조각이 있는지, 주사위 놀이의 점에 몇 개, 어떤 유형의 체커가 있는지에 대한 정보 만 포함되어야합니다. 여기 GameState에는 체스에서의 캐스터 링 또는 주사위 놀이의 배가 큐브에 대한 정보와 같은 추가 정보가 포함되어야합니다.

Move클래스는 조금 까다 롭습니다. 이동을 수행 GameState한 결과를 지정하여 재생할 이동을 지정할 수 있다고 말하고 싶습니다 . 따라서 이동을로 구현할 수 있다고 상상할 수 있습니다 GameState. 그러나, 예를 들어 보드에서 단일 지점을 지정하여 이동을 지정하는 것이 훨씬 쉽다고 상상할 수 있습니다. 우리는 Move클래스가 이들 중 하나를 처리 할 수있을 정도로 유연해야합니다. 따라서 Move클래스는 실제로 사전 이동을 수행 GameState하고 새로운 post-move를 반환하는 메소드가있는 인터페이스가 GameState됩니다.

이제 RuleBook수업에 규칙에 대한 모든 것을 알고 있습니다. 이것은 세 가지로 나눌 수 있습니다. 이니셜 GameState이 무엇인지 알아야하고, 합법적 인 움직임이 무엇인지 알아야하며, 플레이어 중 한 사람이 이겼는지 알 수 있어야합니다.

또한 GameHistory모든 동작과 GameStates발생한 모든 동작을 추적 하는 클래스를 만들 수 있습니다 . 우리는 싱글 GameStateGameState그 앞에 온 모든 것을 아는 것에 대한 책임을지지 않기로 결정했기 때문에 새로운 클래스가 필요 합니다.

이것으로 내가 논의 할 수업 / 인터페이스를 마칩니다. Board수업 도 있습니다 . 그러나 다른 게임의 보드는 보드로 일반적으로 무엇을 할 수 있는지 알기에는 충분히 다르다고 생각합니다. 이제 일반 인터페이스를 제공하고 일반 클래스를 구현하겠습니다.

첫 번째는 GameState입니다. 이 클래스는 특정 게임에 전적으로 의존하기 때문에 일반적인 Gamestate인터페이스 나 클래스 가 없습니다 .

다음은 Move입니다. 내가 말했듯이, 이것은 이동 전 상태를 취하고 이동 후 상태를 생성하는 단일 메소드를 갖는 인터페이스로 나타낼 수 있습니다. 이 인터페이스의 코드는 다음과 같습니다.

package boardgame;

/**
 *
 * @param <T> The type of GameState
 */
public interface Move<T> {

    T makeResultingState(T preMoveState) throws IllegalArgumentException;

}

유형 매개 변수가 있습니다. 예를 들어, ChessMovepre-move의 특정 사항에 대해 알고 있어야하기 때문 ChessGameState입니다. 따라서, 예를 들어,의 클래스 선언 ChessMove될 것이다

class ChessMove extends Move<ChessGameState>,

이미 ChessGameState클래스를 정의했을 것 입니다.

다음으로 일반 RuleBook수업에 대해 설명하겠습니다 . 코드는 다음과 같습니다.

package boardgame;

import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public interface RuleBook<T> {

    T makeInitialState();

    List<Move<T>> makeMoveList(T gameState);

    StateEvaluation evaluateState(T gameState);

    boolean isMoveLegal(Move<T> move, T currentState);

}

다시 GameState클래스에 대한 유형 매개 변수가 있습니다. RuleBook초기 상태가 무엇인지 알고 있어야 하므로 초기 상태를 제공하는 방법을 마련했습니다. RuleBook는 어떤 움직임이 합법적인지 알아야하기 때문에 , 우리는 움직임이 주어진 상태에서 합법적인지 테스트하고 주어진 상태에 대한 법적 움직임 목록을 제공하는 방법을 가지고 있습니다. 마지막으로를 평가하는 방법이 GameState있습니다. (가)에 주목 RuleBook하나 또는 다른 플레이어가 이미 이겼는지 여부를 설명하는 책임을 져야한다,하지만 게임의 중간에 더 나은 위치에있는 사람. 누가 더 나은 위치에 있는지 결정하는 것은 자신의 수업으로 옮겨야하는 복잡한 일입니다. 따라서 StateEvaluation클래스는 실제로 다음과 같이 간단한 열거 형입니다.

package boardgame;

/**
 *
 */
public enum StateEvaluation {

    UNFINISHED,
    PLAYER_ONE_WINS,
    PLAYER_TWO_WINS,
    DRAW,
    ILLEGAL_STATE
}

마지막으로 GameHistory수업에 대해 설명하겠습니다 . 이 수업은 게임에서 도달 한 모든 포지션과 플레이 한 움직임을 기억하는 역할을합니다. 할 수있는 가장 중요한 것은 Move연주 된 대로 녹음하는 것입니다. 의 실행 취소 기능을 추가 할 수도 있습니다 Move. 아래에 구현이 있습니다.

package boardgame;

import java.util.ArrayList;
import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public class GameHistory<T> {

    private List<T> states;
    private List<Move<T>> moves;

    public GameHistory(T initialState) {
        states = new ArrayList<>();
        states.add(initialState);
        moves = new ArrayList<>();
    }

    void recordMove(Move<T> move) throws IllegalArgumentException {
        moves.add(move);
        states.add(move.makeResultingState(getMostRecentState()));
    }

    void resetToNthState(int n) {
        states = states.subList(0, n + 1);
        moves = moves.subList(0, n);
    }

    void undoLastMove() {
        resetToNthState(getNumberOfMoves() - 1);
    }

    T getMostRecentState() {
        return states.get(getNumberOfMoves());
    }

    T getStateAfterNthMove(int n) {
        return states.get(n + 1);
    }

    Move<T> getNthMove(int n) {
        return moves.get(n);
    }

    int getNumberOfMoves() {
        return moves.size();
    }

}

마지막으로 Game모든 것을 하나로 묶는 수업을 상상할 수 있습니다. 이 Game클래스는 사람들이 현재 상태 GameState를보고, 누군가가 있다면 누구가 어떤 동작을하는지 볼 수 있고, 움직임을 할 수 있게하는 메소드를 제공해야합니다 . 아래에 구현이 있습니다.

package boardgame;

import java.util.List;

/**
 *
 * @author brian
 * @param <T> The type of GameState
 */
public class Game<T> {

    GameHistory<T> gameHistory;
    RuleBook<T> ruleBook;

    public Game(RuleBook<T> ruleBook) {
        this.ruleBook = ruleBook;
        final T initialState = ruleBook.makeInitialState();
        gameHistory = new GameHistory<>(initialState);
    }

    T getCurrentState() {
        return gameHistory.getMostRecentState();
    }

    List<Move<T>> getLegalMoves() {
        return ruleBook.makeMoveList(getCurrentState());
    }

    void doMove(Move<T> move) throws IllegalArgumentException {
        if (!ruleBook.isMoveLegal(move, getCurrentState())) {
            throw new IllegalArgumentException("Move is not legal in this position");
        }
        gameHistory.recordMove(move);
    }

    void undoMove() {
        gameHistory.undoLastMove();
    }

    StateEvaluation evaluateState() {
        return ruleBook.evaluateState(getCurrentState());
    }

}

이 클래스 RuleBook에서 전류 GameState가 무엇인지 아는 것은 책임지지 않습니다 . 그게 GameHistory직업 이야 . 따라서 현재 상태가 무엇인지 Game묻고 합법적 인 움직임이 무엇인지 또는 누군가 이겼는지 말할 필요가 있을 GameHistory때이 정보를 제공합니다 .RuleBookGame

어쨌든이 대답의 요점은 일단 각 클래스가 담당하는 것을 합리적으로 결정하고 각 클래스를 적은 수의 책임에 중점을두고 각 책임을 고유 한 클래스에 할당 한 다음 분리되는 경향이 있으며 모든 것이 쉽게 코딩됩니다. 희망적으로 그것은 내가 준 코드 예제에서 분명합니다.


3

내 경험상, 순환 참조는 일반적으로 디자인이 잘 고려되지 않았 음을 나타냅니다.

귀하의 디자인에서, 나는 RuleBook이 왜 주에 대해 "알아야"하는지 이해하지 못합니다. 그것은 어떤 메소드에 대한 매개 변수 로서 State를받을 수 있지만, 왜 State에 대한 참조를 알아야 하는가 (즉, 인스턴스 변수로 보유 해야 하는가)? 그건 말이되지 않습니다. RuleBook은 작업을 수행하기 위해 특정 게임의 상태를 "알지"않아도됩니다. 게임의 규칙은 게임의 현재 상태에 따라 변하지 않습니다. 따라서 잘못 설계했거나 올바르게 설계했지만 잘못 설명하고 있습니다.


+1. 실제 보드 게임을 구입하면 상태없이 규칙을 설명 할 수있는 규칙 책을 얻게됩니다.
unperson325680

1

순환 종속성은 반드시 기술적 인 문제는 아니지만 일반적으로 단일 책임 원칙을 위반하는 코드 냄새로 간주해야합니다 .

순환 의존성은 State객체 에서 너무 많은 일을하려고한다는 사실에서 비롯됩니다 .

상태 저장 개체는 해당 로컬 상태 관리와 직접 관련된 방법 만 제공해야합니다. 가장 기본적인 논리 이상의 것이 필요한 경우 더 큰 패턴으로 나눠야합니다. 어떤 사람들은 이것에 대해 다른 의견을 가지고 있지만, 일반적으로 데이터에 대해 게터와 세터보다 더 많은 일을하고 있다면 너무 많은 일을하고 있습니다.

이 경우, 당신은을 가진 더 나을 것 StateFactory약 알 수있는 Rulebook. StateFactory새 게임을 만드는 데 사용하는 다른 컨트롤러 클래스가있을 것입니다 . State에 대해 반드시 알아야 Rulebook합니다. 규칙 구현에 따라에 Rulebook대해 알 수 있습니다 State.


0

룰북 객체가 특정 게임 상태에 바인딩 될 필요가 있거나, 게임 상태가 주어지면 해당 상태에서 어떤 움직임을 사용할 수 있는지보고하는 방법으로 룰북 객체를 갖는 것이 더 합리적입니까? 신고 한 경우 해당 국가에 대해 아무것도 기억하지 마십시오)? 이용 가능한 움직임에 관한 질문을받는 오브젝트가 게임 상태의 메모리를 유지하도록함으로써 얻을 수있는 것이 없다면, 참조를 유지할 필요는 없습니다.

경우에 따라 규칙 평가 객체의 상태를 유지하는 것이 유리할 수 있습니다. 이러한 상황이 발생할 수 있다고 생각되면 "참조 자"클래스를 추가하고 규칙 책에 "createReferee"메소드를 제공하도록 제안하십시오. 한 경기 나 50 회에 대한 질문에 상관없이 룰북과 달리 심판 개체는 한 경기를 수행 할 것으로 기대합니다. 게임중인 게임과 관련된 모든 상태를 캡슐화 할 것으로 예상되지는 않지만 유용하다고 생각되는 게임에 대한 모든 정보를 캐시 할 수 있습니다. 게임이 "실행 취소"기능을 지원하는 경우, 심판에게 이전 게임 상태와 함께 저장 될 수있는 "스냅 샷"개체를 생성하는 수단을 포함시키는 것이 도움이 될 수 있습니다. 그 대상은

코드의 규칙 처리와 게임 상태 처리 측면간에 약간의 결합이 필요한 경우, 심판 오브젝트를 사용하면 이러한 결합 기본 규칙 책과 게임 상태 클래스 에서 유지할 수 있습니다. 또한 게임 상태 클래스가 관련이없는 것으로 간주되는 게임 상태의 측면을 고려한 새로운 규칙을 만들 수도 있습니다 (예 : "객체 X가 Z 위치에있을 경우 Y를 수행 할 수없는 규칙이 추가 된 경우) ", 심판은 게임 상태 클래스를 변경하지 않고 어떤 개체가 위치 Z에 있었는지 추적하도록 변경 될 수 있습니다).


-2

이를 처리하는 올바른 방법은 인터페이스를 사용하는 것입니다. 두 클래스가 서로에 대해 알도록하는 대신 각 클래스가 인터페이스를 구현하고 다른 클래스에서이를 참조하도록하십시오. 서로를 참조 해야하는 클래스 A와 클래스 B가 있다고 가정 해 봅시다. 클래스 A가 인터페이스 A를 구현하고 클래스 B가 인터페이스 B를 구현하면 클래스 A의 인터페이스 B와 클래스 B의 인터페이스 A를 참조 할 수 있습니다. 클래스 A는 클래스 B와 같이 자체 프로젝트에있을 수 있습니다. 인터페이스는 별도의 프로젝트에 있습니다. 다른 두 프로젝트 모두 참조합니다.


2
이것은 단지 몇 시간 전에 게시 된 이전 답변에서 제시 되고 설명 된 포인트를 반복하는 것으로 보인다
gnat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.