自身または他の2つのことを表すデータ型を作成する方法


16

バックグラウンド

私が取り組んでいる実際の問題は次のとおりです。カードゲームMagic:The Gatheringでカードを表現する方法が必要です。ゲーム内のほとんどのカードは見た目が普通のカードですが、一部は2つの部分に分かれており、それぞれに名前が付いています。これらの2部構成のカードの各半分は、カード自体として扱われます。そのため、明確にするためにCard、通常のカード、または2部構成のカードの半分(つまり、名前が1つだけのカード)を参照するためにのみ使用します。

カード

したがって、基本タイプのCardがあります。これらのオブジェクトの目的は、実際には単にカードのプロパティを保持することです。彼らは本当に自分で何もしません。

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

には2つのサブクラスがありCard、私はこれを呼び出していますPartialCard(2部構成のカードの半分)とWholeCard(通常のカード)。 PartialCardには2つの追加メソッドがあります:PartialCard otherPart()boolean isFirstPart()

代表者

私がデッキを持っている場合、それはWholeCardsではなくCardsで構成されている必要Cardがあり、a はである可能性がありPartialCard、それは意味がありません。そのため、「物理カード」を表すオブジェクト、つまり1つWholeCardまたは2つPartialCardのsを表すことができるオブジェクトが必要です。私は暫定的にこのタイプを呼んでいるRepresentative、とCard方法を持っているでしょうgetRepresentative()。Aは、Representativeカード(複数可)にほとんど直接的な情報を提供するだろう、それはそれ/それらへの唯一のポイントだろう、を表します。今、私の素晴らしい/クレイジー/愚かなアイデア(あなたが決める)は、WholeCardがとの両方 Cardを継承するということですRepresentative。結局のところ、彼らは自分自身を表すカードです!WholeCardsはgetRepresentativeとして実装できますreturn this;

に関してはPartialCards、それらは自分自身を表しRepresentativeてはいませんが、a Cardではなく、2つPartialCardのにアクセスするためのメソッドを提供する外部を持っています。

このタイプの階層は理にかなっていると思いますが、複雑です。Cardsを「概念的なカード」と考え、Representativesを「物理的なカード」と考えると、ほとんどのカードは両方です。物理的なカードには実際に概念的なカードが含まれており、それらは同じものではないと主張することができると思いますが、私はそれらが同じだと主張します。

型キャストの必要性

のでPartialCard、SとWholeCardsともにCard、S、及びそれらを分離するためには良い理由は、通常はありません、私は通常、ちょうどと仕事ができると思いますCollection<Card>。そのPartialCardため、追加のメソッドにアクセスするためにs をキャストする必要がある場合があります。今は、明示的なキャストが本当に好きではないので、ここで説明するシステムを使用しています。そしてのようにCard、それらが表す実際のにアクセスするにはRepresentativeWholeCardまたはのいずれかにキャストする必要があります。CompositeCard

要約のために:

  • ベースタイプ Representative
  • ベースタイプ Card
  • タイプWholeCard extends Card, Representative(アクセスは不要、それ自体を表す)
  • タイプPartialCard extends Card(他の部分へのアクセスを許可)
  • タイプComposite extends Representative(両方の部分にアクセスできます)

これは非常識ですか?実際にはかなり理にかなっていると思うが、正直なところわからない。


1
PartialCardsは、物理カードごとに2枚ある以外のカードですか?彼らがプレイされる順序は重要ですか?「Deck Slot」クラスと「WholeCard」クラスを作成し、DeckSlotsに複数のWholeCardsを持たせてから、DeckSlot.WholeCards.Playのような操作を行い、1つまたは2つがある場合は、両方が別々であるかのように再生できますか?カード?あなたのはるかに複雑なデザインが与えられているように思われます:)
エンダーランド

この問題を解決するために、カード全体と部分的なカードの間でポリモーフィズムを使用できるとは本当に思っていません。はい、「再生」機能が必要な場合は簡単に実装できますが、問題はカードの一部とカード全体の属性が異なることです。これらのオブジェクトを、単に基本クラスとしてではなく、それらが何であるかを表示できるようにする必要があります。その問題に対するより良い解決策がない限り。しかし、ポリモーフィズムは、共通の基本クラスを共有するタイプとうまく混ざり合わないことがわかりました。
コードブレーカー

1
正直なところ、これはとてつもなく複雑だと思います。モデルは "is a"関係のみであるように見え、密結合のある非常に硬直した設計になります。もっと興味深いのは、それらのカードが実際に何をしているのでしょうか?
ダニエルジュール

2
それらが「単なるデータオブジェクト」である場合、私の本能は、2番目のクラスのオブジェクトの配列を含む1つのクラスを持ち、それをそのままにしておくことです。継承やその他の無意味な合併症はありません。これらの2つのクラスがPlayableCardとDrawableCardであるか、WholeCardとCardPartであるかはわからない。なぜなら、あなたのゲームがどのように機能するかについてはほとんど十分に知らないからである。
Ixrec

1
どのようなプロパティを保持していますか?これらのプロパティを使用するアクションの例を挙げることができますか?
ダニエルジュール

回答:


14

私はあなたが次のようなクラスを持つべきだと思う

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

物理カードに関係するコードは物理カードクラスを処理でき、論理カードに関係するコードはそれを処理できます。

物理的なカードには実際に概念的なカードが含まれており、それらは同じものではないという議論をすることができると思いますが、それらは同じだと主張します。

物理カードと論理カードが同じものだと思うかどうかは関係ありません。それらが同じ物理オブジェクトであるという理由だけで、コード内で同じオブジェクトであると仮定しないでください。重要なのは、そのモデルを採用することでコーダーの読み取りと書き込みが容易になるかどうかです。実際、すべての物理カードが一貫して論理カードのコレクションとして扱われる単純なモデルを採用すると、100%の時間でコードが単純になります。


2
これが最良の答えであるため、支持されたが、与えられた理由ではない。これは、単純な理由ではなく、物理カードと概念カード同じものではなく、このソリューションがそれらの関係を正しくモデル化するため、最良の答えです。良いOOPモデルが常に物理的な現実を反映しているわけではありませんが、常に概念的な現実を反映している必要があります。もちろん、単純さは優れていますが、概念の正確さは後回しになります。
ケビンクルムウィーデ

2
@KevinKrumwiede、私が見るように、単一の概念的現実はありません。異なる人々は同じことを異なる方法で考え、人々はそれに対する考え方を変えることができます。物理カードと論理カードを別々のエンティティと考えることができます。または、分割カードは、カードの一般的な概念に対するある種の例外と考えることができます。どちらも本質的に不正ではありませんが、より単純なモデリングに役立ちます。
ウィンストンイーバート

8

率直に言って、提案されたソリューションはあまりにも制限的であり、物理的現実からゆがめられ、切り離されすぎていると思いますが、モデルは利点がほとんどありません。

次の2つの選択肢のいずれかをお勧めします。

オプション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では、各クラスは1つの役割を正確に持つ必要があり、動作の欠如は潜在的なData Classを明らかにします。これはコードの悪臭です。

結局のところ、彼らは自分自身を表すカードです!

私見、それは少し奇妙に聞こえ、さらに少し奇妙に聞こえます。タイプ「カード」のオブジェクトは、カードを表す必要があります。限目。

Magic:The Gatheringについては何も知りませんが、実際の構造が何であれ、カードを同じように使用したいと思います。文字列表現を表示したい、攻撃値を計算したいなどです。

あなたが説明する問題については、より一般的な問題を解決するためにこのDPが通常提示されるという事実にもかかわらず、Composit Design Patternをお勧めします。

  1. Card既に行ったように、インターフェースを作成します。
  2. を作成してConcreteCardCardシンプルなフェイスカードを実装および定義します。通常のカードの動作をこのクラスに入れることをheしないでください。
  3. を作成しCompositeCardCard2つの追加の(およびアプリオリプライベート)を実装しますCard。それらleftCardを呼び出してみましょうrightCard

このアプローチの優雅さは、a CompositeCardに2枚のカードが含まれ、それ自体がConcreteCardまたはCompositeCardであるということです。ゲームではleftCardrightCardおそらく体系的にConcreteCardsになりますが、デザインパターンを使用すると、必要に応じてより高いレベルのコンポジションを無料でデザインできます。カードの操作では、カードの実際のタイプが考慮されないため、サブクラスへのキャストなどは必要ありません。

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();
   }

   ...
}

これを行うことによりCompositeCard、anyのCard場合とまったく同じようにを使用でき、特定の動作はポリモーフィズムのおかげで隠されます。

a CompositeCardが常に2つのnormalを含むことが確実な場合Card、アイデアを保持し、単にandのConcreateCard型として使用できます。 leftCardrightCard


あなたは話している合成パターンが、実際に、あなたは、の2つの参照保持しているので、Card中にCompositeCardあなたが実装しているDecoratorパターンを。また、このソリューションを使用するようにOPに推奨しています。
発見

2つのCardインスタンスを持つことでクラスがデコレーターになる理由がわかりません。独自のリンクによると、デコレータは1つのオブジェクトに機能を追加し、それ自体がこのオブジェクトと同じクラス/インターフェイスのインスタンスです。一方、他のリンクによると、コンポジットを使用すると、同じクラス/インターフェースの複数のオブジェクトを所有できます。しかし、最終的には言葉は重要ではなく、アイデアだけが素晴らしいです。
mgoeminne

@Spottedこれは、用語がJavaで使用されているため、間違いなくデコレーターパターンではありません。デコレータパターンは、個々のオブジェクトのメソッドをオーバーライドして、そのオブジェクトに固有の匿名クラスを作成することで実装されます。
ケビンクルムウィーデ

@KevinKrumwiede はCompositeCard、追加のメソッドを公開しない限り、CompositeCard単なるデコレータです。
発見

「... Design Patternを使用すると、より高いレベルのコンポジションを無料で設計できます」-いいえ、無料ではありませんがその逆です-要件に必要なものよりも複雑なソリューションが必要です。
Doc Brown

3

デッキや墓地にあるときはすべてがカードであり、プレイするときは、プレイ可能を実装または拡張する1つ以上のカードオブジェクトからクリーチャー、土地、エンチャントなどを構築できます。次に、コンポジットは、コンストラクターが2つの部分的なカードを取る単一のPlayableになり、キッカーを持つカードは、コンストラクターがマナ引数を取るPlayableになります。タイプは、あなたがそれで何ができるか(ドロー、ブロック、ディスペル、タップ)とそれがもたらすことができるものを反映しています。または、Playableは、同じインターフェースを使用してカードを呼び出してそれが何をするかを予測することが本当に有用である場合、プレイから除外されたときに慎重に元に戻す(ボーナスとカウンターを失い、分割される)必要がある単なるカードです。

おそらくCardとPlayableに効果があります。


残念ながら、これらのカードは使用しません。これらは、実際に照会できる単なるデータオブジェクトです。デッキのデータ構造が必要な場合は、List <WholeCard>または何かが必要です。緑色のインスタントカードの検索を実行する場合は、リスト<カード>が必要です。
コードブレーカー

うん、いいよ。あなたの問題にはあまり関係ありません。削除する必要がありますか?
ダビスラー

3

ビジターパターンは、隠された型情報を回復するための古典的な手法です。ここでそれを使用して(ここでは少し変更します)、2つのタイプがより抽象化された変数に格納されている場合でも区別できます。

その高度な抽象化、Cardインターフェースから始めましょう:

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

Cardインターフェイスにはもう少し動作がありますが、ほとんどのプロパティゲッターは新しいクラスに移動します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。2つの面を持つカードを表すためにa をしましょう。

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つの顔を持つことができる場合があります。実装する新しいメソッドを追加するのではなく、2番目のメソッドに2つのパラメーターの代わりに配列を取得させることができます。多面カードの扱いが異なる場合、これは理にかなっていますが、1面より上の面の数はすべて同様に処理されます。

CardVisitorインターフェイスの代わりに抽象クラスに変換し、すべてのメソッドの実装を空にすることもできます。これにより、関心のある動作のみを実装できます(おそらく、片面のみに関心がありCardます)。また、既存のすべてのクラスにそれらのメソッドを実装させたり、コンパイルに失敗させたりせずに、新しいメソッドを追加することもできます。


1
これは、他の種類の2面カード(サイドバイサイドではなくフロント&バック)に簡単に拡張できると思います。++
ラバーダック

さらに調査するために、サブクラスに固有のメソッドを悪用するためのVisitorの使用は、Multiple Dispatchと呼ばれることもあります。Double Dispatchは、この問題の興味深い解決策になる可能性があります。
mgoeminne

1
ビジターパターンが不必要な複雑さとコードへのカップリングを追加すると思うので、私は投票しています。代替手段が利用できるので(mgoeminneの答えを参照)、私はそれを使用しません。
発見

@Spotted訪問者複雑なパターンであり、答えを書くときに複合パターンも考慮しました。私が訪問者と一緒に行った理由は、OPが異なるものを同じように扱うのではなく、異なるものを異なるように扱いたいからです。カードの動作がより多く、ゲームプレイに使用されていた場合、複合パターンを使用すると、データを結合して統一された統計情報を生成できます。ただし、これらは単なるデータの袋であり、カードディスプレイのレンダリングに使用される可能性があります。この場合、単純な複合アグリゲーターよりも、分離された情報を取得する方が便利なように思われます。
cbojar
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.