자체 또는 다른 두 가지를 나타내는 데이터 유형을 만드는 방법


16

배경

다음은 실제 작업중인 문제입니다. 카드 게임 Magic : The Gathering 에서 카드를 표현하는 방법을 원합니다 . 게임에서 대부분의 카드는 일반적인 모양의 카드이지만 일부는 각각 고유 한 이름을 가진 두 부분으로 나뉩니다. 이 두 부분으로 된 카드의 각 절반은 카드 자체로 취급됩니다. 명확성을 위해, 나는 Card단지 일반 카드이거나 두 부분으로 된 카드의 절반 (즉, 이름이 하나만있는)을 나타내는 데에만 사용 합니다.

카드

기본 유형 인 카드가 있습니다. 이러한 객체의 목적은 실제로 카드의 속성을 유지하는 것입니다. 그들은 실제로 스스로 아무것도하지 않습니다.

interface Card {
    String name();
    String text();
    // etc
}

의 두 개의 서브 클래스가 있는데 Card, PartialCard(2 파트 카드의 절반)과 WholeCard(일반 카드) 라고 부릅니다 . PartialCard두 가지 추가 방법이 있습니다 : PartialCard otherPart()boolean isFirstPart().

대표자

나는 갑판이있는 경우,이를 구성해야 WholeCard하지, S Card, A는 같은이야 Card될 수있다 PartialCard, 그것은 의미가 없다. 그래서 저는 "물리적 카드"를 나타내는 객체, 즉 하나 WholeCard또는 두 개의 객체를 원합니다 PartialCard. 나는이 타입을 잠정적으로 호출 Representative하고 Card있으며 메소드를 가질 것이다 getRepresentative(). A Representative는 자신이 나타내는 카드에 대한 직접적인 정보를 거의 제공하지 않으며 카드 만 가리 킵니다. 이제, 나의 화려한 / 미친 / 바보 같은 아이디어는 (당신이 결정)에서 해당 WholeCard 상속입니다 모두 CardRepresentative. 결국, 그들은 자신을 나타내는 카드입니다! WholeCards는로 구현할 수 getRepresentative있습니다 return this;.

에 관해서는 PartialCards, 그들은 자신을 대표하지 않지만, 외부 Representative가 아닌 Card두 가지에 액세스하는 방법을 제공합니다 PartialCard.

이 유형 계층 구조는 의미가 있다고 생각하지만 복잡합니다. 우리가 Card"개념적 카드"로 생각 Representative하고 "물리적 카드 "로 생각한다면 , 대부분의 카드는 둘 다입니다! 실제 카드에는 실제로 개념적인 카드가 포함되어 있으며 그것들이 같은 것은 아니라고 주장 할 수 있다고 생각합니다.

타입 캐스팅의 필요성

때문에 PartialCardS 그리고 WholeCards모두 Card들, 그리고 그들을 분리하는 것이 더 좋은 이유가, 나는 일반적으로 단지와 함께 일하게 될 것입니다 Collection<Card>. 그래서 때로는 PartialCard추가 방법에 액세스하기 위해을 캐스팅해야 합니다. 지금 은 명시 적 캐스트를 좋아하지 않기 때문에 여기에 설명 된 시스템을 사용 하고 있습니다 . 그리고 같은 Card, Representative중 하나에 캐스트 할 필요 WholeCard또는 Composite실제 액세스하기 위해, Card그들이 대표들.

요약하면 다음과 같습니다.

  • 기본 유형 Representative
  • 기본 유형 Card
  • 유형 WholeCard extends Card, Representative(액세스가 필요하지 않으며 자체를 나타냄)
  • 유형 PartialCard extends Card(다른 부분에 대한 액세스 권한 부여)
  • 유형 Composite extends Representative(두 부분 모두에 접근 가능)

이건 미쳤어? 나는 그것이 실제로 의미가 있다고 생각하지만 솔직히 확실하지 않습니다.


1
PartialCard는 실제 카드 당 2 개가 아닌 다른 카드입니까? 그들이하는 순서가 중요합니까? "데크 슬롯"과 "전체 카드"클래스를 만들고 DeckSlots에 여러 개의 WholeCard가있는 것을 허용 한 다음 DeckSlot.WholeCards.Play와 같은 작업을 수행 할 수 있습니까? 카드? 당신이 훨씬 더 복잡한 디자인을 가지고 있다면 내가 놓친 것이 있어야합니다 :)
enderland

나는이 문제를 해결하기 위해 전체 카드와 부분 카드 사이에 다형성을 사용할 수 있다고 생각하지 않습니다. 예, "플레이"기능을 원한다면 쉽게 구현할 수 있지만 문제는 부분 카드이며 전체 카드에는 다른 속성 세트가 있습니다. 이 객체를 기본 클래스가 아닌 객체로 볼 수 있어야합니다. 그 문제에 대한 더 나은 해결책이 없다면. 그러나 나는 다형성이 공통 기본 클래스를 공유하는 유형과 잘 혼합되지 않지만 그에 맞는 방법이 있다면 그 방법이 다릅니다.
codebreaker

1
솔직히, 나는 이것이 엄청나게 복잡하다고 생각합니다. 당신은 "is"관계만을 모델링하는 것처럼 보이며, 타이트한 커플 링으로 매우 견고한 디자인으로 이어집니다. 그 카드가 실제로하는 일이 더 흥미로울까요?
Daniel Jour

2
그것들이 "그냥 데이터 객체"라면, 제 본능은 두 번째 클래스의 객체 배열을 포함하는 하나의 클래스를 갖는 것입니다. 상속이나 다른 무의미한 합병증이 없습니다. 이 두 클래스가 PlayableCard와 DrawableCard 또는 WholeCard 및 CardPart 중 어느 것이되어야하는지 잘 모르겠습니다. 게임의 작동 방식에 대해 거의 알지 못하기 때문에 전화를 걸 무언가를 생각할 수 있다고 확신합니다.
Ixrec

1
어떤 종류의 속성을 보유하고 있습니까? 이러한 속성을 사용하는 작업의 예를 제공 할 수 있습니까?
Daniel Jour

답변:


14

당신은 같은 수업을해야한다고 생각합니다.

class PhysicalCard {
    List<LogicalCard> getLogicalCards();
}

실제 카드와 관련된 코드는 실제 카드 클래스를 처리 할 수 ​​있으며 논리 카드와 관련된 코드는이를 처리 할 수 ​​있습니다.

실제 카드에는 실제로 개념적인 카드가 포함되어 있으며 그것들이 같은 것은 아니라고 주장 할 수 있다고 생각합니다.

실제 카드와 논리 카드가 같은 것이라고 생각하는지 여부는 중요하지 않습니다. 동일한 물리적 객체이기 때문에 코드에서 동일한 객체 여야한다고 가정하지 마십시오. 중요한 것은 해당 모델을 채택하여 코더를 더 쉽게 읽고 쓸 수 있는지 여부입니다. 사실 모든 물리적 카드가 논리 카드 모음으로 일관되게 처리되는 더 간단한 모델을 채택하면 시간의 100 %가 코드가 더 간단 해집니다.


2
주어진 이유가 아니지만 이것이 최선의 답변이기 때문에 찬성했습니다. 이것은 단순한 것이 아니라 실제 카드와 개념 카드 동일 하지 않기 때문에 가장 좋은 대답 입니다.이 솔루션은 관계를 올바르게 모델링합니다. 좋은 OOP 모델이 항상 물리적 현실을 반영하지는 않지만 항상 개념적 현실을 반영해야한다는 것은 사실입니다. 물론 단순함은 좋지만 개념적 정확성에는 뒷좌석이 필요합니다.
Kevin Krumwiede

2
@KevinKrumwiede, 내가 본 것처럼 단일 개념적 현실은 없습니다. 다른 사람들은 다른 방법으로 같은 것을 생각하며 사람들은 자신의 생각을 바꿀 수 있습니다. 물리적 및 논리적 카드는 별도의 엔티티로 생각할 수 있습니다. 또는 분할 카드를 일반적인 카드 개념의 예외로 생각할 수 있습니다. 본질적으로 부정확하지는 않지만 더 간단한 모델링에 적합합니다.
Winston Ewert

8

솔직히 말해서, 제안 된 솔루션이 너무 제한적이고 왜곡되고 물리적 현실과 분리되어 있다고 생각합니다.

두 가지 대안 중 하나를 제안합니다.

옵션 1. MTG 사이트 목록 Wear // Tear 와 같이 Half A // Half B 로 식별되는 단일 카드로 취급하십시오 . 그러나 엔터티 에 각 속성 중 재생 가능한 이름, 마나 비용, 유형, 희귀 성, 텍스트, 효과 등 N 개 를 포함 할 수 있습니다 .Card

interface Card {
  List<String> Names();
  List<ManaCost> Costs();
  List<CardTypes> Types();
  /* etc. */
}

옵션 2 옵션 1과 다른 것은 아니지만 실제 현실을 모델로 한 것입니다. 당신은이 Card나타내는 개체 의 물리적 카드 . 그리고, 목적은 N 개의 Playable 물건 을 보유하는 것입니다. 그것들 Playable은 각각 고유 한 이름, 마나 비용, 효과 목록, 능력 목록 등 Card을 가질 수 있습니다 . 그리고 당신의 "물리적" 은 각각 Playable의 이름 의 복합적인 고유 식별자 (또는 이름)를 가질 수 있습니다. MTG 데이터베이스가 나타납니다.

interface Card {
  String Name();
  List<Playable> Playables();
}

interface Playable {
  String Name();
  ManaCost Cost();
  CardType Type();
  /* etc. */
}

이러한 옵션 중 하나는 실제 현실과 매우 비슷하다고 생각합니다. 그리고 나는 그것이 당신의 코드를 보는 사람에게 도움이 될 것이라고 생각합니다. (6 개월 만에 자기 자신처럼)


5

이러한 객체의 목적은 실제로 카드의 속성을 유지하는 것입니다. 그들은 실제로 스스로 아무것도하지 않습니다.

이 문장은 디자인에 문제가 있음을 나타내는 신호입니다. OOP에서 각 클래스는 정확히 하나의 역할을 수행해야하며 동작이 없으면 잠재적 인 데이터 클래스가 나타납니다 . 이는 코드에서 나쁜 냄새입니다.

결국, 그들은 자신을 나타내는 카드입니다!

IMHO, 조금 이상하게 들리고 심지어 조금 이상하게 들립니다. "Card"유형의 개체는 카드를 나타내야합니다. 기간.

Magic : The gathering 에 대해서는 아무것도 모르지만 실제 구조와 상관없이 비슷한 방식으로 카드를 사용하고 싶다고 생각합니다. 문자열 표현을 표시하고 공격 값을 계산하고 싶습니다.

이 DP가 일반적으로보다 일반적인 문제를 해결하기 위해 제시된다는 사실에도 불구하고 설명하는 문제에 대해서는 Composit Design Pattern을 권장합니다 .

  1. Card이미 한 것처럼 인터페이스를 만듭니다 .
  2. 간단한 얼굴 카드 ConcreteCard를 구현 Card하고 정의하는를 만듭니다 . 이 수업에서 일반 카드 의 행동을 주저하지 마십시오 .
  3. CompositeCard를 구현 Card하고 두 개의 추가 (및 선험적 개인)를 갖는를 작성하십시오 Card. 그들에게 전화 수 있습니다 leftCardrightCard.

접근 방식의 우아함은 CompositeCard두 개의 카드를 포함하고 있으며이 카드는 그 자체가 ConcreteCard 또는 CompositeCard 일 수 있습니다. 게임에서, leftCard그리고 rightCard아마도 체계적 것 ConcreteCard들,하지만 디자인 패턴은 당신이 원하는 경우 무료 높은 수준의 조성을 설계 할 수 있습니다. 카드 조작은 실제 유형의 카드를 고려하지 않으므로 서브 클래스로 캐스트하는 것과 같은 것은 필요하지 않습니다.

CompositeCardCard물론에 지정된 메소드를 구현해야하며 , 그러한 카드가 2 개의 카드로 구성되어 있다는 사실을 고려하여이를 수행 할 CompositeCard것입니다. 다음 구현 :

public class CompositeCard implements Card
{ 
   private final Card leftCard, rightCard;
   private final double factor;

   @Override // Defined in Card
   public double attack(Player p){
      return factor * (leftCard.attack(p) + rightCard.attack(p));
   }

   @Override // idem
   public String name()
   {
       return leftCard.name() + " combined with " + rightCard.name();
   }

   ...
}

그렇게하면 CompositeCardany Card에 대해하는 것처럼 정확하게 사용할 수 있으며 다형성 덕분에 특정 동작이 숨겨집니다.

당신은 확실히 경우 CompositeCard항상 두 정상이 포함됩니다 Card의, 당신은 아이디어를 유지하고, 간단하게 사용할 수 있습니다 ConcreateCardA의 유형으로 leftCardrightCard.


당신은 얘기 복합 패턴 있지만, 사실은 당신의 두 개의 참조 유지하고 이후 CardCompositeCard당신이 구현하고있는 장식 패턴을 . 나는 또한이 솔루션을 사용하기 위해 OP에 권장합니다. 데코레이터는 갈 길입니다!
발견

왜 두 개의 카드 인스턴스가 클래스를 데코레이터로 만드는지 모르겠습니다. 자신의 링크에 따르면 데코레이터는 단일 객체에 기능을 추가하며 그 자체는이 객체와 동일한 클래스 / 인터페이스의 인스턴스입니다. 다른 링크에 따르면 컴포지트를 사용하면 동일한 클래스 / 인터페이스의 여러 객체를 소유 할 수 있습니다. 그러나 궁극적으로 단어는 중요하지 않으며 아이디어 만 좋습니다.
mgoeminne

@Spotted이 용어는 Java에서 사용되는 데코레이터 패턴이 아닙니다. 데코레이터 패턴은 개별 객체의 메소드를 재정 의하여 해당 객체에 고유 한 익명 클래스를 작성하여 구현됩니다.
Kevin Krumwiede

@KevinKrumwiede는 CompositeCard추가 방법 을 제공 하지 않는 CompositeCard한 데코레이터입니다.
발견

"... 디자인 패턴을 사용하면 무료로 더 높은 수준의 컴포지션을 디자인 할 수 있습니다"-아니요, 무료로 제공되지는 않으며 그 반대의 경우도 요구 사항에 필요한 것보다 복잡한 솔루션을 제공하는 가격입니다.
Doc Brown

3

어쩌면 갑판이나 묘지에있을 때 모든 것이 카드 일 수도 있고, 게임을 할 때 하나 이상의 카드 객체로 생물, 땅, 마법 등을 생성 할 수 있습니다. 그런 다음 컴포지트는 생성자가 두 개의 부분 카드를 취하는 단일 Playable이되고, 키커가있는 카드는 생성자가 마나 인수를 취하는 Playable이됩니다. 유형에는 수행 할 수있는 작업 (그리기, 차단, 해제, 탭) 및 영향을 미칠 수있는 사항이 반영됩니다. 또는 Playable은 같은 인터페이스를 사용하여 카드를 불러 내고 카드의 기능을 예측하는 것이 실제로 유용한 경우, 게임을 할 때 조심스럽게 되돌려 야하는 (보너스와 카운터를 잃고, 분할되는) 카드입니다.

아마 카드 및 재생할 수는 있습니다-A 효과를.


불행히도, 나는이 카드를 사용하지 않습니다. 실제로 쿼리 할 수있는 데이터 개체 일뿐입니다. 데크 데이터 구조를 원한다면 List <WholeCard> 또는 무언가를 원합니다. 녹색 인스턴트 카드를 검색하려면 List <Card>가 필요합니다.
codebreaker

아, 알았어요 그렇다면 문제와 관련이 없습니다. 삭제해야합니까?
Davislor

3

방문자 패턴은 숨겨진 유형 정보를 복구하기위한 고전적인 기술이다. 여기서는 두 가지 유형이 더 높은 흡인력 변수에 저장되어있는 경우에도이를 식별하기 위해 여기에서 약간의 변형을 사용할 수 있습니다.

더 높은 추상화, Card인터페이스 부터 시작하겠습니다 .

public interface Card {
    public void accept(CardVisitor visitor);
}

Card인터페이스 에 약간의 동작이있을 수 있지만 대부분의 속성 getter는 새 클래스로 이동합니다 CardProperties.

public class CardProperties {
    // property methods, constructors, etc.

    String name();
    String text();
    // ...
}

이제 SimpleCard단일 속성 집합으로 전체 카드를 나타낼 수 있습니다 .

public class SimpleCard implements Card {
    private CardProperties properties;

    // Constructors, ...

    @Override
    public void accept(CardVisitor visitor) {
        visitor.visit(properties);
    }
}

우리 CardProperties는 아직 쓰여지지 않았지만 아직 작성되지 않은 방법 CardVisitor을 알았습니다. CompoundCard두 개의 얼굴이있는 카드를 나타내 도록하겠습니다 .

public class CompoundCard implements Card {
    private CardProperties firstFaceProperties;
    private CardProperties secondFaceProperties;

    // Constructors, ...

    public void accept(CardVisitor visitor) {
        visitor.visit(firstFaceProperties, secondFaceProperties);
    }
}

CardVisitor시작 이 시작됩니다. 지금 그 인터페이스를 작성해 봅시다 :

public interface CardVisitor {
    public void visit(CardProperties properties);
    public void visit(CardProperties firstFaceProperties, CardProperties secondFaceProperties);
}

(이것은 현재 인터페이스의 첫 번째 버전입니다. 개선 될 수 있습니다. 나중에 설명하겠습니다.)

우리는 이제 모든 부품을 완성했습니다. 이제 그것들을 정리하면됩니다 :

List<Card> cards = new LinkedList<>();
cards.add(new SimpleCard(new CardProperties(/* ... */)));
cards.add(new CompoundCard(new CardProperties(/* ... */), new CardProperties(/* ... */)));

 for(Card card : cards) {
     card.accept(new CardVisitor() {
         @Override
         public void visit(CardProperties properties) {
             // Do something for simple cards with a single face
         }

         public void visit(CardProperties firstFaceProperties, CardProperties secondFaceProperties) {
             // Do something else for compound cards with two faces
         }
     });
 }

런타임은 다형성을 통해 올바른 버전의 #visit메소드로 디스패치를 ​​처리 하지 않고 처리합니다.

CardVisitor동작을 재사용 할 수 있거나 런타임에 동작을 교환하려는 경우 익명 클래스를 사용하지 않고 내부 클래스 또는 전체 클래스로 승격시킬 수도 있습니다 .


우리는 지금처럼 클래스를 사용할 수 있지만 CardVisitor인터페이스 를 개선 할 수 있습니다 . 예를 들어, Cards가 3 개 또는 4 개 또는 5 개의 얼굴을 가질 수 있는 시간이 올 수 있습니다. 구현할 새 메소드를 추가하는 대신 두 번째 매개 변수 대신 두 번째 메소드를 가져 와서 배열 할 수 있습니다. 이것은다면 카드를 다르게 취급하는 경우에 적합하지만 하나 이상의 얼굴 수는 모두 비슷하게 취급됩니다.

또한 CardVisitor인터페이스 대신 추상 클래스 로 변환 하고 모든 메소드에 대해 빈 구현을 가질 수 있습니다. 이를 통해 우리가 관심있는 행동 만 구현할 수 있습니다 (단면에만 관심이있을 수 있습니다 Card). 기존의 모든 클래스가 해당 메소드를 구현하거나 컴파일하지 못하도록 강제하지 않고 새로운 메소드를 추가 할 수도 있습니다.


1
나는 이것이 다른 종류의 양면 카드로 쉽게 확장 될 수 있다고 생각합니다 (나란히가 아닌 앞면과 뒷면). ++
RubberDuck

추가 탐색을 위해 서브 클래스에 고유 한 메소드를 악용하기 위해 방문자를 사용하는 것을 다중 디스패치라고합니다. Double Dispatch는이 문제에 대한 흥미로운 해결책이 될 수 있습니다.
mgoeminne

1
방문자 패턴이 코드에 불필요한 복잡성과 결합을 추가한다고 생각하기 때문에 투표를 거부합니다. 대안이 가능하기 때문에 (mgoeminne의 답변 참조) 나는 그것을 사용하지 않을 것입니다.
발견

@Spotted 방문자 복잡한 패턴이며 답을 쓸 때 복합 패턴도 고려했습니다. 내가 방문자와 함께 갔던 이유는 OP가 비슷한 것을 다르게 취급하기보다는 비슷한 것을 다르게 취급하기 때문입니다. 카드가 더 많은 동작을 가지고 있고 게임 플레이에 사용되는 경우, 복합 패턴을 사용하면 데이터를 결합하여 통일 된 통계를 생성 할 수 있습니다. 그것들은 단지 데이터의 백일 뿐이지 만, 아마도 카드 디스플레이를 렌더링하는데 사용될 수 있는데,이 경우 분리 된 정보를 얻는 것이 단순한 복합 어 그리 게이터보다 더 유용한 것처럼 보입니다.
cbojar
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.