체스 조각에 대한 상속 대 구성


9

이 스택 교환에 대한 빠른 검색은 일반적으로 일반적으로 상속보다 유연성이 높은 것으로 간주되지만 항상 프로젝트 등에 의존하며 상속이 더 나은 선택 인 경우가 있음을 보여줍니다. 각 조각에 메쉬, 다른 애니메이션 등이있는 3D 체스 게임을 만들고 싶습니다. 이 구체적인 예에서 두 가지 접근법 모두에 대한 사례를 주장 할 수있는 것처럼 보입니다.

상속은 다음과 같이 보일 것입니다 (적절한 생성자 등)

class BasePiece
{
    virtual Squares GetValidMoveSquares() = 0;
    Mesh* mesh;
    // Other fields
}

class Pawn : public BasePiece
{
   Squares GetValidMoveSquares() override;
}

"is-a"원칙을 준수하는 반면 구성은 다음과 같습니다.

class MovementComponent
{
    virtual Squares GetValidMoveSquares() = 0;
}

class PawnMovementComponent
{
     Squares GetValidMoveSquares() override;
}

enum class Type
{
     PAWN,
     BISHOP, //etc
}


class Piece
{
    MovementComponent* movementComponent;
    MeshComponent* mesh;
    Type type;
    // Other fields
 }

개인적인 취향의 문제입니까 아니면 한 가지 접근법이 다른 방법보다 분명히 똑똑한 선택입니까?

편집 : 나는 모든 대답에서 무언가를 배웠으므로 하나만 골라서 기분이 좋지 않습니다. 내 최종 솔루션은 여기의 여러 게시물에서 영감을 얻습니다 (여전히 작업 중). 시간을내어 답변을 주신 모든 분들께 감사드립니다.


나는 둘 다 나란히 존재할 수 있다고 믿지만,이 둘은 다른 목적을위한 것이지만 이것들은 서로 배타적이지 않다. 체스는 다른 조각과 보드로 구성되며 조각은 다른 유형이다. 이 작품들은 몇 가지 기본 속성과 행동을 공유하고 특정 속성과 행동을 가지고 있습니다. 나에 따르면, 구성은 보드와 조각 위에 적용되어야하고 상속은 유형의 조각에 따라 적용되어야합니다.
Hitesh Gaur

모든 사람들이 모든 상속이 나쁘다고 주장하는 사람들을 얻을 수 있지만, 일반적인 아이디어는 인터페이스 상속이 훌륭하고 훌륭하며 구현 상속이 유용하지만 문제가 될 수 있다는 것입니다. 내 경험상 단일 상속 수준을 넘어서는 것은 의문의 여지가 있습니다. 그것 이상으로 괜찮을 수 있지만 복잡한 혼란을 일으키지 않고 벗어나는 것은 간단하지 않습니다.
JimmyJames

전략 패턴은 이유 때문에 게임 프로그래밍에서 일반적입니다. var Pawn = new Piece(howIMove, howITake, whatILookLike)상속 계층 구조보다 훨씬 간단하고 관리 가능하며 유지 관리가 용이합니다.
Ant P

@AntP 감사합니다!
CanISleepYet

@AntP 답을 만들어보세요! ... 그것에 많은 장점이 있습니다!
svidgen

답변:


4

언뜻보기에 귀하의 답변은 "조성"솔루션이 상속을 사용하지 않는 척합니다. 그러나 나는 당신이 단순히 이것을 추가하는 것을 잊었다 고 생각합니다 :

class PawnMovementComponent : MovementComponent
{
     Squares GetValidMoveSquares() override;
}

(그리고 이러한 파생물 중 6 개 유형 각각에 대해 하나씩). 이제 이것은 고전적인 "전략"패턴과 비슷해 보이며 상속도 활용하고 있습니다.

불행히도이 솔루션에는 결함이 있습니다. 각 솔루션에는 Piece유형 정보가 두 번 중복되어 있습니다.

  • 멤버 변수 내부 type

  • 내부 movementComponent(하위 유형으로 표시)

이 중복성은 실제로 문제를 일으킬 수있는 것입니다. 간단한 클래스는 Piece두 가지 소스가 아닌 "단일 진실 소스"를 제공해야합니다.

물론 유형 정보는에만 저장하고 type하위 클래스는 만들지 않아도 MovementComponent됩니다. 그러나이 디자인은 아마도 switch(type)구현에 있어 거대한 " "진술로 이어질 것이다 GetValidMoveSquares. 그리고 그것은 상속이 더 나은 선택 이라는 것을 확실히 보여주는 강력한 증거입니다 .

"inheritance"디자인에서 "type"필드를 비 중복 방식으로 제공하는 것은 매우 쉽습니다. 가상 메소드 GetType()를 추가하고 BasePiece각 기본 클래스에 따라이를 구현하십시오.

"홍보"와 관련하여 : 나는 @svidgen과 함께 여기에 @TheCatWhisperer의 대답에 논쟁의 여지가있는 것을 발견했습니다 .

"프로모션"을 같은 조각의 유형 변경으로 해석하는 대신 조각의 물리적 교환으로 해석하는 것은 나에게 훨씬 자연스러운 느낌입니다. 따라서 다른 유형의 다른 것으로 하나의 조각을 교환함으로써 비슷한 방식으로 이것을 구현하는 것은 적어도 특정 체스 경우에는 큰 문제를 일으키지 않을 것입니다.


패턴의 이름을 말해 주셔서 감사합니다. 나는 그것이 패턴이라는 것을 몰랐습니다. 당신은 또한 옳습니다. 제 질문에 운동의 상속 요소를 놓쳤습니다. 유형이 실제로 중복됩니까? 보드의 모든 여왕을 밝히고 싶다면 어떤 움직임 구성 요소가 있는지 확인하는 것이 이상하게 보일 것입니다.
CanISleepYet

@CanISleepYet : 제안 된 "유형"필드의 문제는의 하위 유형과 다를 수 있다는 것입니다 movementComponent. "상속"디자인에서 안전한 방식으로 유형 필드를 제공하는 방법을 편집 한 내용을 참조하십시오.
Doc Brown

4

아마 것 간단한 옵션을 선호하는 사용자가 명시 적으로 잘 관절 이유 또는하지 않는 한 강력한 확장 성 및 유지 보수 문제를.

상속 옵션은 코드 줄이 적고 복잡성이 적습니다. 각 유형의 부품은 이동 특성과 "불변"일대일 관계를 갖습니다. (일대일이지만 반드시 불변 일 필요는없는 표현과는 달리 다양한 "스킨"을 제공 할 수 있습니다.)

여러 클래스로 조각과 그들의 행동 / 운동 사이의 불변의 1 대 1 관계를 깰 경우거야 아마 에만 복잡성을 추가 - 그리고별로 다른. 따라서 해당 코드를 검토하거나 상속하는 경우 복잡성에 대한 문서화 된 이유를 알 수있을 것입니다.

말했다 모든, 더 간단한 옵션은 , IMO하는 만드는 것입니다 Piece 인터페이스 개별 조각이 것을 구현을 . 대부분의 언어에서 인터페이스는 다른 인터페이스 구현을 제한하지 않기 때문에 실제로 상속 과 는 매우 다릅니다 . ...이 경우에는 기본 클래스 동작이 무료로 제공되지 않습니다. 공유 동작을 다른 곳에 배치하고 싶을 것입니다.


상속 솔루션이 '코드 줄이 적다'는 것은 아닙니다. 아마도이 클래스에서는 전체가 아닐 수도 있습니다.
JimmyJames

@JimmyJames 정말요? ... OP의 코드 줄을 세어 보아도 ... 전체 솔루션은 아니지만 어떤 솔루션이 더 LOC이고 유지 관리가 복잡하다는 나쁜 지표는 아닙니다.
svidgen

나는이 시점에서 모두 명목이기 때문에 이것에 대해 너무 좋은 지적을하고 싶지 않지만, 그래, 정말 그렇게 생각합니다. 이 코드의 비트는 전체 응용 프로그램과 비교할 때 무시할 만하다는 주장입니다. 이것이 규칙의 구현을 단순화 (설명 가능)하면 수십 또는 수백 줄의 코드를 절약 할 수 있습니다.
JimmyJames

고마워, 나는 인터페이스를 생각하지 않았지만 이것이 최선의 방법이라는 것이 옳을 수도 있습니다. 단순할수록 거의 항상 좋습니다.
CanISleepYet

2

체스와 관련된 것은 게임 플레이와 조각 규칙이 규정되고 고정되어 있다는 것입니다. 작동하는 모든 디자인은 훌륭합니다. 원하는 것을 사용하십시오! 실험하고 모두 사용해보십시오.

그러나 비즈니스 세계에서는 이와 같이 엄격한 규정이 없습니다. 비즈니스 규칙 및 요구 사항은 시간이 지남에 따라 변경되며, 프로그램은 시간이 지남에 따라 변경되어야합니다. 이것은 대 대 대 대 데이터가 차이를 만드는 곳입니다. 여기서는 단순성으로 인해 복잡한 솔루션을 시간이 지남에 따라 유지 관리하고 변화하는 요구 사항을보다 쉽게 ​​유지할 수 있습니다. 일반적으로 비즈니스에서는 데이터베이스를 포함 할 수있는 지속성을 처리해야합니다. 따라서 단일 클래스가 작업을 수행 할 때 여러 클래스를 사용하지 않고 구성이 충분한 경우 상속을 사용하지 않는 규칙이 있습니다. 그러나 이러한 규칙은 모두 규칙과 요구 사항이 변경 될 때 장기적으로 코드를 유지 관리 할 수 ​​있도록하기위한 것입니다. 이는 체스의 경우가 아닙니다.

체스의 장기 유지 보수 경로는 프로그램이 더욱 똑똑하고 똑똑해 져야한다는 것입니다. 결국 속도와 스토리지 최적화가 지배적입니다. 이를 위해서는 일반적으로 성능에 대한 가독성을 희생하는 절충안을 만들어야하므로 최상의 OO 디자인조차 결국 방해가 될 것입니다.


개념을 취한 다음 새로운 것들을 믹스에 넣는 성공적인 게임이 많이있었습니다. 나중에 체스 게임에서이 작업을 수행하려면 실제로 가능한 변경 사항을 고려해야합니다.
Flater

2

문제 영역 (즉, 체스 규칙)에 대해 몇 가지 관찰을 시작합니다.

  • 조각의 유효한 이동 세트는 조각의 유형뿐만 아니라 보드의 상태에 따라 다릅니다 (사각형이 비어 있으면 앞면으로 움직일 수 있고 조각을 캡처하면 대각선으로 움직일 수 있음).
  • 특정 작업에는 기존 조각을 재생 (프로모션 / 캡처 캡처)에서 제거하거나 여러 조각을 이동 (캐스팅)하는 것이 포함됩니다.

이것들은 조각 자체의 책임으로 모델화하기 어색하며, 상속이나 구성도 여기에서 좋지 않습니다. 이러한 규칙을 일류 시민으로 모델링하는 것이 더 자연 스러울 것입니다.

// pseudo-code, not pure C++

interface MovementRule {
  set<Square> getValidMoves(Board board, Square from); // what moves can I make from the given square?
  void makeMove(Board board, Square from, Square to); // update board state to reflect a specific move
}

class Game {
  Board board;
  map<PieceType, MovementRule> rules;
}

MovementRule는 구현이 캐스터 링 및 승격과 같은 복잡한 이동을 지원하는 데 필요한 방식으로 보드 상태를 업데이트 할 수있는 단순하지만 유연한 인터페이스입니다. 표준 체스의 경우 각 유형의 조각에 대해 하나의 구현이 있지만 Game인스턴스의 다른 규칙을 연결하여 다른 체스 변형을 지원할 수도 있습니다.


흥미로운 접근 방식-생각하기 좋은 음식
CanISleepYet

1

이 경우 상속이 더 깨끗한 선택이라고 생각합니다. 작곡은 조금 더 우아 할 수 있지만 조금 더 강해 보입니다.

동작이 다른 움직이는 조각을 사용하는 다른 게임을 개발할 계획 인 경우, 특히 각 게임에 필요한 조각을 생성하기 위해 팩토리 패턴을 사용하는 경우 컴포지션이 더 나은 선택 일 수 있습니다.


2
상속을 사용하여 프로모션을 어떻게 처리 하시겠습니까?
TheCatWhisperer 19

4
@TheCatWhisperer 실제로 게임을 할 때 어떻게 처리 합니까? (
행복하게

0

"is-a"원칙을 확실히 준수합니다

맞습니다. 그래서 당신은 상속을 사용합니다. 여전히 Piece 클래스 내에서 작곡을 할 수는 있지만 최소한 백본이 있습니다. 컴포지션은 더 유연하지만 일반적으로 유연성은 과대 평가되며,이 경우 확실히 강렬한 모델을 포기해야하는 새로운 종류의 작품을 소개 할 가능성이 0이기 때문입니다. 체스 게임은 곧 변경되지 않을 것입니다. 상속은 안내 모델, 관련 코드, 교육 코드, 방향을 제공합니다. 구성은별로 중요하지 않으며, 가장 중요한 도메인 엔티티는 눈에 띄지 않으며, 척추가 없으며 코드를 이해하려면 게임을 이해해야합니다.


컴포지션을 사용하면 코드에 대해 추론하기가 어렵다는 점을 추가해 주셔서 감사합니다.
CanISleepYet

0

여기에서 상속을 사용하지 마십시오. 다른 대답은 확실히 많은 지혜의 보석을 가지고 있지만 상속을 사용하는 것은 분명히 실수 일 것입니다. 사용하여 구성하는 것은 뿐만 아니라 당신은 당신이 예상하지 않는 문제를 처리하는 데 도움이,하지만 난 이미 상속이 정상적으로 처리 할 수없는 여기에 문제를 참조하십시오.

폰이 더 가치있는 조각으로 변환 될 때 승격은 상속에 문제가 될 수 있습니다. 기술적으로는 한 조각을 다른 것으로 교체하여이 문제를 해결할 수 있지만이 방법에는 제한이 있습니다. 이러한 제한 중 하나는 판촉시 조각 정보를 재설정하지 않거나 조각을 복사하기 위해 추가 코드를 작성하지 않을 수있는 조각 별 통계를 추적하는 것입니다.

자, 당신의 질문에 당신의 작곡 디자인에 관해서는, 그것이 불필요하게 복잡하다고 생각합니다. 각 작업마다 별도의 구성 요소가 있어야한다고 생각하지 않으며 조각 유형 당 하나의 클래스를 고수 할 수 있습니다. 유형 열거 형도 필요하지 않을 수 있습니다.

나는 Piece클래스뿐만 아니라 PieceType클래스 를 이미 정의했다 . PieceType클래스를 정의 등과 같은 폰, 여왕, 요법 정의해야하는 모든 방법, CanMoveTo, GetPieceTypeName, 요법. 그러면 Pawnand Queen, ect 클래스는 사실상 PieceType클래스 에서 상속됩니다 .


3
승격 및 교체와 관련된 문제는 사소한 IMO입니다. 우려의 분리는 그러한 "추적 당 추적"(또는 무엇이든)이 어쨌든 독립적으로 처리되도록 요구 합니다 . ... 솔루션이 제공 할 수있는 모든 잠재적 이점은 해결하려는 가상의 관심사 인 IMO에서 비롯됩니다.
svidgen

5
명확히하기 위해 : 의심 할 여지없이 제안 된 솔루션의 장점이 있습니다. 그러나 그들은 당신의 대답에서 찾기가 어렵습니다. 주로 "상속 솔루션"에서 실제로 해결하기 쉬운 문제에 중점을 둡니다 . ... 당신의 의견에 대해 의사 소통하려는 어떤 장점에서든 (어쨌든) 그런 종류의 혼란.
svidgen 23.27에

1
경우 Piece가상하지, 난이 솔루션에 동의합니다. 클래스 디자인은 표면상에서 조금 더 복잡해 보일 수 있지만 구현은 전체적으로 더 간단 할 것이라고 생각합니다. 와 관련이있을 수도 Piece있고 그렇지 않을 수도있는 PieceType것은 정체성과 잠재적 인 이동 이력입니다.
JimmyJames 16:16에

1
@svidgen 구성된 조각을 사용하는 구현과 상속 기반 조각을 사용하는 구현은이 솔루션에 설명 된 세부 사항을 제외하고는 기본적으로 동일하게 보입니다. 이 구성 솔루션은 단일 수준의 간접 성을 추가합니다. 나는 그것을 피할 가치가있는 것으로 보지 않습니다.
JimmyJames 16:16에

2
@JimmyJames 맞아. 그러나 여러 조각의 이동 이력을 추적하고 상관시켜야합니다. IMO에서는 단일 조각이 책임 져야하는 것이 아닙니다. SOLID에 관심이 있다면 "별도의 문제"입니다.
svidgen

0

내 견해로는 제공된 정보를 바탕으로 상속이나 구성이 가야하는지 대답하는 것이 현명하지 않습니다.

  • 상속과 구성은 객체 지향 디자이너 툴 벨트의 도구 일뿐입니다.
  • 이 도구를 사용하여 문제 영역에 대한 다양한 모델을 설계 할 수 있습니다.
  • 도메인을 나타낼 수있는 이러한 모델이 많이 있기 때문에 요구 사항에 대한 디자이너의 관점에 따라 상속과 구성을 여러 방식으로 결합하여 기능적으로 동등한 모델을 만들 수 있습니다.

체스 조각의 개념을 중심으로 모델링 한 첫 번째 모델과 조각의 이동 개념을 중심으로 한 두 번째 모델에서는 모델이 완전히 다릅니다. 그것들은 기능적으로 동일 할 수 있으며 도메인을 더 잘 나타내는 도메인을 선택하고 더 쉽게 추론 할 수 있도록 도와줍니다.

또한 두 번째 디자인에서 Piece클래스에 type필드 가 있다는 사실 은 조각 자체가 여러 유형 일 수 있으므로 디자인 문제가 있음을 분명히 나타냅니다. 조각 자체가 유형 인 경우 일종의 상속을 사용해야한다고 들리지 않습니까?

따라서 솔루션에 대해 논쟁하기가 매우 어렵습니다. 중요한 것은 상속 또는 구성을 사용했는지 여부가 아니라 모델이 문제의 영역을 정확하게 반영하는지 여부와 문제를 추론하고 구현하는 것이 유용한 지 여부입니다.

상속과 컴포지션을 올바르게 사용하는 데 약간의 경험이 필요하지만 완전히 다른 툴이므로 컴포지션 만 사용하여 시스템을 완전히 디자인 할 수 있다고 동의 할 수는 있지만 서로를 "대체"할 수는 없다고 생각합니다.


도구가 아니라 중요한 모델이라는 점을 잘 알고 있습니다. 중복 유형과 관련하여 상속이 실제로 도움이됩니까? 예를 들어 모든 폰을 강조 표시하거나 조각이 왕인지 확인하려면 캐스팅이 필요하지 않습니까?
CanISleepYet

-1

글쎄, 나는 둘 다 제안하지 않는다.

객체 지향은 훌륭한 도구이며 종종 사물을 단순화하는 데 도움이되지만 툴 쉐드의 유일한 도구는 아닙니다.

간단한 바이트 배열을 포함하는 보드 클래스를 대신 구현하십시오.
이를 해석하고 계산하는 것은 비트 마스킹 및 전환을 사용하여 멤버 함수에서 수행됩니다.

소유자에 대해 MSB를 사용하고 조각에 7 비트를 남겨두고 비어있는 경우 0을 예약하십시오.
또는 비어있는 경우 0, 소유자의 경우 서명, 조각의 절대 값.

원하는 경우 비트 필드를 사용하여 수동 마스킹을 피할 수 있지만 이식 할 수는 없습니다.

class field {
    signed char data = 0;
public:
    constexpr field() = default;
    constexpr field(bool black, piece x) noexcept
    : data(x < piece::pawn || x > piece::king ? 0 : (black << 7) | x))
    {}
    constexpr bool is_black() noexcept { return data < 0; }
    constexpr bool is_white() noexcept { return data > 0; }
    constexpr bool empty() noexcept { return data == 0; }
    constexpr piece piece() noexcept { return piece(data & 0x7f); }
};

흥미롭고 ... 그리고 C ++ 질문이라고 생각하는 경향이 있습니다. 그러나 C ++ 프로그래머가 아니라면 이것은 나의 첫 번째 (또는 두 번째 또는 세 번째 ) 본능 이 아닙니다 ! ... 이것은 아마도 가장 C ++ 답변 일 것입니다!
svidgen

7
이것은 실제 응용 프로그램이 따르지 않아야 할 미세 최적화입니다.
Lie Ryan

@LieRyan 당신이 가진 모든 것이 OOP라면, 모든 것이 물체처럼 냄새가 난다. 그리고 "마이크로"가 정말 흥미로운 질문인지 여부입니다. 당신 무엇을하는지에 따라 , 그것은 상당히 중요 할 수 있습니다.
중복 제거기

훗 ... 말했다 , C ++은 입니다 객체 지향. 나는 영업 이익은 대신 C ++를 사용하는 이유가 가정 단지 C가 .
svidgen

3
여기서는 OOP의 부족에 대해 덜 걱정하지만 비트 twiddling으로 코드를 난독 화합니다. 8 비트 Nintendo 용 코드를 작성하거나 보드 상태의 수백만 사본을 처리해야하는 알고리즘을 작성하지 않는 한, 이는 조기에 최적화되지 않았으므로 바람직하지 않습니다. 일반적인 시나리오에서는 정렬되지 않은 비트 액세스에 추가 비트 작업이 필요하기 때문에 어쨌든 더 빠르지 않을 것입니다.
거짓말 라이언
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.