如何为代表自身或另外两件事的事物创建数据类型


16

背景

这是我正在研究的实际问题:我想要一种在纸牌游戏“ 魔术:聚会”中表示纸牌的方法。游戏中的大多数卡都是外观正常的卡,但其中有些分为两部分,每部分都有自己的名称。这些分为两部分的卡的每半都被视为卡本身。因此,为清楚起见,我将Card仅指代普通卡或两部分卡的一半(换句话说,只有一个名字的东西)。

牌

因此,我们有一个基本类型Card。这些对象的目的实际上只是为了保留卡的属性。他们自己根本不做任何事情。

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

Card我称之为的两个子类PartialCard(两部分的卡的一半)和WholeCard(普通卡)。 PartialCard还有两个附加方法:PartialCard otherPart()boolean isFirstPart()

代表

如果我有一个牌组,它应该由WholeCards而不是Cards组成,因为a Card可能是a PartialCard,这没有任何意义。因此,我想要一个表示“物理卡”的对象,即可以表示一个WholeCard或两个PartialCards的对象。我暂时称呼这种类型Representative,并Card有方法getRepresentative()。A Representative几乎不会在其代表的卡上提供任何直接信息,而只会指向它/它们。现在,我的聪明/疯狂/愚蠢的想法(由您决定)是WholeCard继承自两个 CardRepresentative。毕竟,它们是代表自己的卡片!WholeCards可以实现getRepresentativereturn this;

至于PartialCards,它们并不代表自己,但是它们的外部Representative不是Card,而是提供了访问这两个PartialCard的方法。

我认为这种类型的层次结构很有意义,但是很复杂。如果我们将Cards视为“概念卡”而将Representatives视为“物理卡”,那么,大多数卡都是!我认为您可以提出一个论点,即物理卡实际上包含概念卡,并且它们不是一回事,但我认为它们是相同的。

类型转换的必要性

因为PartialCards和WholeCards都是Cards,通常没有充分的理由将它们分开,所以我通常只使用Collection<Card>。所以有时我需要转换PartialCards才能访问它们的其他方法。现在,我正在使用此处描述的系统,因为我真的不喜欢显式强制转换。和一样CardRepresentative需要将其强制转换为WholeCardComposite,以访问Card它们所代表的实际。

所以仅作总结:

  • 基本类型 Representative
  • 基本类型 Card
  • 类型WholeCard extends Card, Representative(无需访问,它代表自己)
  • 类型PartialCard extends Card(允许访问其他部分)
  • 类型Composite extends Representative(允许访问两个部分)

这是疯了吗?我认为这实际上很有意义,但是老实说我不确定。


1
除每张物理卡有两张卡之外,PartialCards是否有效?他们的演奏顺序重要吗?您是否可以只制作一个“ Deck Slot”和“ WholeCard”类,并允许DeckSlots具有多个WholeCard,然后执行DeckSlot.WholeCards.Play之类的事情,如果有1个或2个,则将它们当作单独的两个来播放牌?看来您的设计要复杂得多,肯定有我缺少的东西:)
enderland

我真的不觉得我可以在整个卡和部分卡之间使用多态来解决这个问题。是的,如果我想要一个“播放”功能,则可以轻松实现,但是问题是局部卡和整个卡具有不同的属性集。我真的需要能够将这些对象视为它们的外观,而不仅仅是它们的基类。除非对此问题有更好的解决方案。但是我发现,多态性不能与共享公共基类但类型不同的类型很好地混合使用(如果有道理的话)。
密码破解者

1
老实说,我认为这很荒谬。您似乎仅对“是”关系进行建模,从而导致具有紧密耦合的非常严格的设计。更有趣的是那些卡实际上在做什么?
Daniel Jour

2
如果它们只是“数据对象”,那么我的直觉是拥有一个包含第二类对象数组的类,然后将其留在那;没有继承或其他毫无意义的并发症。我不知道这两个类应该是PlayableCard和DrawableCard还是WholeCard和CardPart,因为我对您的游戏的运作方式知之甚少,但是我敢肯定您会想到一些称呼它们的方式。
Ixrec

1
他们拥有什么样的财产?您可以举一些使用这些属性的操作示例吗?
Daniel Jour

Answers:


14

在我看来,你应该像

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

与物理卡有关的代码可以处理物理卡类,而与逻辑卡有关的代码可以处理物理卡类。

我认为您可以提出一个论点,即物理卡实际上包含概念卡,并且它们不是同一回事,但我认为它们是相同的。

您是否认为物理卡和逻辑卡是同一回事并不重要。不要仅仅因为它们是相同的物理对象就假定它们应该是代码中的相同对象。重要的是采用该模型是否会使编码器更易于读写。事实是,采用一种更简单的模型,其中每张物理卡在100%的时间被一致地视为逻辑卡的集合,将导致代码更简单。


2
赞成,因为这是最好的答案,尽管不是出于给定的原因。这是最好的答案,这不是因为它很简单,而是因为物理卡和概念卡不是一回事,并且此解决方案正确地为它们之间的关系建模。确实,一个好的OOP模型并不总是反映物理现实,而应该总是反映概念现实。当然,简单性是好的,但是它在概念上的正确性上却退居二线。
凯文·克鲁姆维德

2
@KevinKrumwiede,正如我所看到的那样,没有一个概念上的现实。不同的人以不同的方式思考同一件事,并且人们可以改变对事物的看法。您可以将物理卡和逻辑卡视为独立的实体。或者,您可以将拆分卡视为卡的一般概念的某种例外。两者都不是本质上不正确的,但是可以使其更简单地建模。
Winston Ewert 2015年

8

直言不讳,我认为所提出的解决方案过于严格,过于扭曲,与物理现实模型脱节,几乎没有优势。

我建议两种选择之一:

选项1. 将其视为单张卡片,标识为Half A // Half B,就像MTG网站列表Wear // Tear一样。但是,允许您的Card实体包含每个属性的N个:可播放名称,魔法消耗,类型,稀有度,文字,效果等。

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

选项2。并非所有与选项1不同的地方都根据物理现实对其进行建模。您有一个Card代表实体卡的实体。而且,其目的是容纳N Playable件东西。这些Playable的可各自具有独特的名称,法力消耗,效果列表,能力的列表,等等。而你的‘身体’ Card可以有自己的标识(或名称),这是每一个化合物Playable的名字S,很像MTG数据库似乎起作用了。

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

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

我认为这两种选择都非常接近物理现实。而且,我认为这对查看您的代码的所有人都是有益的。(像您自己的自己在6个月内。)


5

这些对象的目的实际上只是为了保留卡的属性。他们自己根本不做任何事情。

这句话表明您的设计有问题:在OOP中,每个类都应该只扮演一个角色,而缺乏行为则揭示了潜在的Data Class,这在代码中是一种难闻的气味。

毕竟,它们是代表自己的卡片!

恕我直言,这听起来有些奇怪,甚至有些怪异。类型为“卡”的对象应代表卡。期。

我对万智牌一无所知:聚会,但我想无论它们的实际结构如何,您都想以类似的方式使用您的牌:想要显示字符串表示形式,想要计算攻击值,等等。

对于您描述的问题,尽管此DP通常是为了解决更一般的问题而提出的,但我还是建议使用“ 复合设计模式”

  1. Card像已经创建的那样创建一个界面。
  2. 创建一个ConcreteCard实现Card和定义简单面部卡的。不要犹豫,将普通卡的行为放在此类中。
  3. 创建一个CompositeCard实现Card并具有两个附加(和先验私有)Card的。让我们称它们为leftCardrightCard

这种方法的优雅之处在于,其中CompositeCard包含两个卡,它们本身可以是ConcreteCard或CompositeCard。在你的游戏,leftCardrightCard可能会被系统ConcreteCardS,但设计模式可以让你设计,如果你想免费的更高层次组成。卡的操作不会考虑卡的实际类型,因此不需要诸如强制转换为子类之类的操作。

CompositeCard必须实现中指定的方法Card,并且必须考虑到这样的事实:该卡是由两张卡构成的(如果需要,还可以添加该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像对任何一样使用Card,并且由于多态性,隐藏了特定行为。

如果确定a CompositeCard总是包含两个正常的Cards,则可以保留该想法,并简单地ConcreateCard用作leftCardand 的类型rightCard


你所谈论的复合模式,但事实上,因为你拿的两个引用CardCompositeCard您所用的装饰图案。我还建议OP使用此解决方案,装饰器是必经之路!
斑点

我不明白为什么拥有两个Card实例会使我的班级成为装饰者。根据您自己的链接,装饰器将功能添加到单个对象,并且它本身是与此对象相同的类/接口的实例。同时,根据您的其他链接,组合可以允许拥有相同类/接口的多个对象。但是最终的话并不重要,只有一个主意才是好主意。
mgoeminne 2015年

@Spotted这绝对不是装饰器模式,因为Java中使用了该术语。装饰器模式通过重写单个对象上的方法来实现,从而创建该对象唯一的匿名类。
凯文·克鲁姆维德

@KevinKrumwiede只要CompositeCard不公开其他方法,CompositeCard仅是一个装饰器。
斑点

“ ...设计模式使您可以免费设计更高层次的构图”-不,它不是免费提供的,相反,它的代价是拥有比要求更复杂的解决方案。
布朗

3

也许当它在甲板或坟墓场中时,所有东西都是一张卡牌,并且当您使用它时,您将通过一个或多个卡牌对象构造生物,土地,附魔等,所有这些对象都实现或扩展了可玩性。然后,组合成为单个Playable,其构造函数将使用两个部分Cards,而具有踢脚的纸牌将变为Playable,其构造函数将使用mana参数。类型反映了您可以使用它做什么(绘制,阻止,消除,点击)以及可以影响它的内容。或“可玩性”只是一张牌,如果不使用同一个界面来调用牌并预测其作用真的非常有用,那么在退出游戏时就必须对其进行认真还原(丢失任何奖金和计数器,将其拆分)。

也许Card和Playable 作用。


不幸的是,我不玩这些牌。它们实际上只是可以查询的数据对象。如果您需要甲板数据结构,则需要List <WholeCard>或其他内容。如果要搜索绿色的即时卡,则需要List <Card>。
密码破解者

啊好吧。那与您的问题不太相关。我应该删除吗?
戴维斯洛

3

访问者模式是用于恢复隐藏类型信息的经典技术。即使在将它们存储在高抽象变量中的情况下,我们也可以在此处使用它(此处略有变化)来区分这两种类型。

让我们从一个更高的抽象开始,一个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代表两个面孔的卡片:

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可以具有三个或四个或五个面的情况。与其添加新的方法来实现,不如让第二个方法采用和数组而不是两个参数。如果对多面卡进行了不同的处理,这是有道理的,但是对一张以上的面的数量都进行了类似的处理。

我们还可以转换CardVisitor为抽象类而不是接口,并为所有方法提供空的实现。这允许我们仅实现我们感兴趣的行为(也许我们仅对单面Cards 感兴趣)。我们还可以添加新方法,而不必强迫每个现有的类实现这些方法或无法编译。


1
我认为这可以轻松扩展到另一种两面卡(正面和背面,而不是并排)。++
RubberDuck

为了进行进一步的研究,有时将访问者用于开发特定于子类的方法的过程称为多次派发。Double Dispatch可能是解决该问题的有趣方法。
mgoeminne 2015年

1
我之所以投反对票,是因为我认为访问者模式会增加不必要的复杂性并与代码耦合。由于可以使用替代方法(请参见mgoeminne的答案),所以我不会使用它。

@Spotted访客一个复杂的模式,我在编写答案时也考虑了复合模式。我与访问者一起去的原因是,OP希望以不同的方式对待相似的事物,而不是以不同的方式对待不同的事物。如果这些卡具有更多的行为,并被用于游戏玩法,则复合模式允许组合数据以产生统一的统计数据。它们只是数据袋,尽管可能用于渲染卡片显示,在这种情况下,获得分开的信息似乎比简单的复合聚合器有用。
cbojar
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.