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


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 상속입니다 모두 Card 와 Representative. 결국, 그들은 자신을 나타내는 카드입니다! 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. 그들에게 전화 수 있습니다 leftCard와 rightCard.

접근 방식의 우아함은 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의 유형으로 leftCard와 rightCard.


당신은 얘기 복합 패턴 있지만, 사실은 당신의 두 개의 참조 유지하고 이후 Card에 CompositeCard당신이 구현하고있는 장식 패턴을 . 나는 또한이 솔루션을 사용하기 위해 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.