棋类的继承与组成


9

快速搜索此堆栈交换显示,通常认为合成比继承更灵活,但与往常一样,它取决于项目等,并且有时继承是更好的选择。我想制作一个3D象棋游戏,其中每个棋子都有一个网格,可能有不同的动画等等。在这个具体的例子中,似乎您可以为两种方法都辩护,我错了吗?

继承看起来像这样(使用适当的构造函数等)

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

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

当然遵循“是”原则,而构图看起来像这样

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)与继承层次结构相比,对我来说似乎更简单,更易于管理和维护。
蚂蚁P

@AntP很好,谢谢!
CanISleepYet

@AntP一个答案!...有很多优点!
svidgen

Answers:


4

乍一看,您的答案假装“组合”解决方案不使用继承。但是我想您只是忘了添加以下内容:

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

(以及更多此类推导,六种类型中的每一种都对应)。现在,这看起来更像是经典的“策略”模式,并且还在利用继承。

不幸的是,此解决方案有一个缺陷:Piece现在每个解决方案都冗余地保存了两次类型信息:

  • 在成员变量内 type

  • 内部movementComponent(由子类型代替)

这种冗余确实会给您带来麻烦-像这样的简单类Piece应该提供“单一事实来源”,而不是两个来源。

当然,您可以尝试仅将类型信息存储在中type,也不要创建其子类MovementComponent。但是这种设计很有可能导致在实现时出现巨大的“ switch(type)”陈述GetValidMoveSquares。这绝对是继承是此处更好的选择的有力指示

注意在“继承”的设计,这是很容易提供非冗余方式的“类型”字段:添加一个虚拟的方法GetType()BasePiece并在每个基类中遵照执行。

关于“促销”:@svidgen在这里,我发现@TheCatWhisperer的答案中提出的论点值得商bat

在我看来,将“促销”解释为件的物理交换,而不是将其解释为同一件类型的改变。因此,以类似的方式实现这一点-通过将另一件换成另一种不同类型的件-可能不会引起任何大问题-至少对于国际象棋的具体情况而言不会如此。


感谢您告诉我模式的名称,但我没有意识到它叫做Strategy。您也是对的,我在问题中错过了运动部件的继承。这种类型是否真的多余?如果我想照亮板上的所有皇后,检查他们拥有的机芯组件似乎很奇怪,再加上我需要投射机芯组件以查看它是什么类型,这是非常丑陋的吗?
CanISleepYet

@CanISleepYet:您建议的“类型”字段存在的问题是,它可能与的子类型有所不同movementComponent。请参阅我的编辑如何在“继承”设计中以安全的方式提供类型字段。
布朗

4

除非您有明确的,明确的理由或强烈的可扩展性和维护方面的问题,否则我可能更喜欢使用简单的选项

继承选项是更少的代码行和更少的复杂性。每种类型的棋子都具有其运动特性“不可变”的一对一关系。(与它的表示形式(一对一,但不一定是不变的-您可能要提供各种“皮肤”不同)。)

如果您将片段之间的这种不变的一对一关系以及它们的行为/运动分解为多个类,那么您可能只会增加复杂性 -而不会增加其他任何事情。因此,如果我正在查看或继承该代码,我希望能看到很好的,有据可查的复杂性原因。

话虽如此,一个更简单的选择 IMO是创建您的个人Pieces 实现Piece 接口。在大多数语言中,这实际上与继承有很大不同,因为一个接口不会限制您实现另一个接口。...在那种情况下,您只是免费获得任何基类行为:您希望将共享行为放在其他地方。


我不认为继承解决方案是“更少的代码行”。也许在这一类中,但不是整体。
吉米·詹姆斯(JimmyJames)

@JimmyJames 真的吗?...仅仅算一下OP中的代码行数...不可否认不是整个解决方案,但也不是哪个解决方案具有更高的LOC和维护复杂性的坏指示器。
svidgen

我不想对此提出过分的观点,因为这只是概念上的观点,但是是的,我真的是这么认为的。我的争辩是,与整个应用程序相比,这段代码可以忽略不计。如果这简化了规则的执行(有争议的话),那么您可能会节省数十甚至数百行代码。
吉米·詹姆斯(JimmyJames)

谢谢,我没有想到接口,但是您可能是对的,这是最好的方法。简单总是更好。
CanISleepYet

2

国际象棋的作用是规定和固定游戏规则和棋子规则。任何可行的设计都可以-随心所欲!实验并尝试所有方法。

但是,在商业世界中,没有像这样严​​格规定任何事情-业务规则和需求会随着时间而变化,并且程序必须随着时间而变化以适应。这就是is-a vs.has-a vs.data有所不同的地方。在这里,简单性使复杂的解决方案易于随着时间的推移和不断变化的需求进行维护。通常,在业务中,我们还必须处理持久性,持久性也可能涉及数据库。因此,我们有这样的规则,例如当一个类可以完成工作时就不要使用多个类,并且当组合足够时就不要使用继承。但是,这些规则都旨在使代码在面对不断变化的规则和要求的情况下可以长期维护,而国际象棋则并非如此。

使用国际象棋,最可能的长期维护途径是您的程序需要变得越来越智能,这最终意味着速度和存储优化将占据主导地位。为此,通常必须权衡取舍,以牺牲性能的可读性,因此,即使是最佳的OO设计也最终会被淘汰。


请注意,有许多成功的游戏都采用了概念,然后将一些新事物加入其中。如果您将来想在国际象棋游戏中做到这一点,则实际上应该考虑将来可能发生的变化。
平坦

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实例中插入不同的规则以支持不同的象棋变体。


有趣的方法-
值得深思的

1

我认为在这种情况下,继承将是更清洁的选择。构图可能会更优雅一些,但在我看来似乎有点强迫。

如果您打算开发使用行为不同的移动部件的其他游戏,则构图可能是更好的选择,尤其是如果您采用工厂模式来生成每个游戏所需的部件时。


2
您将如何使用继承处理晋升?
TheCatWhisperer '19

4
@TheCatWhisperer在实际玩游戏时如何处理(希望您能替换

0

当然遵循“是”原则

对,所以您使用继承。您仍然可以在Piece类中撰写文章,但是至少您将有骨干。一般而言,合成更灵活,但灵活性却被高估了,在这种情况下当然可以肯定,因为有人引入一种新的需要您放弃刚性模型的零件的机会为零。国际象棋游戏不会很快改变。继承为您提供了指导模型,也需要联系的东西,教学代码,指导。组成没有那么多,最重要的领域实体并不突出,它是无骨的,您必须了解游戏才能理解代码。


感谢您添加一点,即使用组合很难推理代码
CanISleepYet

0

请不要在这里使用继承。尽管其他答案肯定具有很多智慧,但是在这里使用继承肯定是一个错误。使用合成不仅可以帮助您处理您无法预料的问题,而且我已经在这里看到了继承无法正常处理的问题。

当典当转化为更有价值的物品时,升职可能是继承的问题。从技术上讲,您可以通过将一个零件替换为另一个零件来解决此问题,但是这种方法有局限性。这些限制之一是跟踪每个零件的统计信息,您可能不想在升级时重置零件信息(或编写额外的代码来复制它们)。

现在,就您所提出的构图设计而言,我认为它不必要地复杂。我不认为您需要为每个操作使用一个单独的组件,并且可以坚持每种类型的一个类。也可能不需要类型枚举。

我将定义一个Piece类(就像您已经拥有的)以及一个PieceType类。在PieceType类定义所有这些棋子,王后,ECT必须定义的方法,例如,CanMoveToGetPieceTypeName,等。然后Pawnand和Queenect类实际上将从PieceType该类继承。


3
IMO,您在升级和更换中看到的问题是微不足道的。关注点的分离在很大程度上要求这样的“逐件跟踪”(或其他方式)无论如何都要独立处理。...您想解决的假想问题IMO掩盖了您的解决方案所提供的任何潜在优势。
svidgen

5
需要说明的是:您提出的解决方案无疑有一些优点。但是,很难在您的答案中找到它们,答案主要集中在“继承解决方案” 中真正真正易于解决的问题。...这种方式(无论如何对我而言)都不利于您试图传达自己观点的优点。
svidgen

1
如果Piece不是虚拟的,我同意这种解决方案。尽管从表面上看,类的设计似乎有些复杂,但我认为总体上实现会更简单。与Piece而不是与关联的主要因素PieceType是它的身份以及可能的移动历史。
JimmyJames

1
@svidgen使用此组合件的实现和使用基于继承的件的实现除了此解决方案中描述的细节外,基本上看起来是相同的。此组合解决方案添加了一个间接级别。我认为这值得避免。
吉米·詹姆斯(JimmyJames),

2
@JimmyJames对。但是,多件物品的移动历史需要进行跟踪和关联-IMO不应负责任何一件物品。如果您对SOLID有所了解,这是一个“单独的问题”。
svidgen

0

从我的角度来看,利用所提供的信息来回答继承或组合应该是明智之举是不明智的。

  • 继承和组合只是面向对象设计人员工具栏中的工具。
  • 您可以使用这些工具针对问题领域设计许多不同的模型。
  • 由于有许多这样的模型可以表示一个域,这取决于设计者的需求观点,因此您可以通过多种方式组合继承和组合,以得出功能上等效的模型。

您的模型完全不同,在第一个模型中,您围绕棋子的概念建模,在第二个模型中,您围绕棋子的运动概念建模。它们在功能上可能是等效的,因此我会选择一个更好地表示该域并帮助我更轻松地进行推理的域。

同样,在您的第二个设计中,您的Piece类有一个type字段,这一事实清楚地表明这里存在设计问题,因为部件本身可以是多种类型。听起来不是应该使用某种继承,而片断本身就是类型吗?

因此,您会发现,很难就您的解决方案争论不休。重要的不是您是否使用继承或组合,而是您的模型是否准确地反映了问题的范围,以及对问题进行推理和提供实现是否有用。

正确使用继承和组合需要一些经验,但是它们是完全不同的工具,尽管我可以同意一个系统可以完全使用组合来设计,但我认为它们不能彼此“替代”。


您指出,重要的是模型而不是工具。关于冗余类型,继承真的有帮助吗?例如,如果我想突出显示所有棋子,或者检查棋子是否是国王,那么我不需要铸造吗?
CanISleepYet

-1

好吧,我也不建议。

虽然面向对象是一个很好的工具,通常可以简化操作,但它并不是工具栏中的唯一工具。

考虑改用一个包含简单字节数组的板类。
使用位掩码和切换在成员函数中进行解释并对其进行计算。

将MSB用作所有者,保留7位,但保留零以保留空白。
另外,零代表空白,所有者签名,件的绝对值。

如果需要,可以使用位域来避免手动屏蔽,尽管它们是不可移植的:

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 ++ 面向对象的。我认为还有的OP使用,而不是C ++的理由不仅仅是C
svidgen

3
在这里,我不太担心缺少OOP,而只是通过纠结来混淆代码。除非您正在为8位任天堂编写代码,或者正在编写需要处理数百万个电路板状态副本的算法,否则这是过早的微优化,是不可取的。在正常情况下,由于未对齐的位访问需要额外的位操作,因此它可能总不会更快。
Lie Ryan
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.