【问题标题】:Removing circular dependencies between classes in C++去除 C++ 中类之间的循环依赖
【发布时间】:2019-04-30 03:33:32
【问题描述】:

假设我们正在制作一个两人纸牌游戏,我们有名为 Game、Player 和 Card 的类。游戏包含指向两个玩家的指针并为玩家提供接口。玩家包括玩家的健康和他们的魔法以及他们的手牌向量。 Card 是一个抽象类。每张牌都需要消耗魔法才能打出,并且可以打出。

问题在于,每张牌在使用时都可以通过多种方式改变游戏状态。例如,我们可以有一张使玩家生命值翻倍的卡片,一张使敌方玩家中毒两回合的卡片,一张摧毁板上所有随从的卡片,一张为已经在棋盘上的每个随从创建副本的卡片,等等. 可能性真的是无穷无尽的。我能看到的唯一解决方案是在每张卡片中都有一个指向游戏的指针,但这似乎相当不雅。

还有其他解决方案吗?

【问题讨论】:

  • 为什么每张卡都需要知道游戏状态才能影响它?每张卡片都有属性\方法,游戏对象可以用来改变其状态。
  • 如果游戏拥有玩家,玩家拥有卡片,在我看来,卡片可以直接修改游戏变量,也许通过游戏方法。我可能会误解一些东西
  • 修改游戏状态不是卡片的责任。在处理女巫卡的情况下,修改自己的状态是游戏的责任。获得它的简单途径是模式策略
  • @auburg: 如果你让Game 通过它的属性应用Card 的行为你最终会得到很多意大利面条代码,你必须让每个Card(或者更确切地说是Effect)照顾好自己。

标签: c++ class oop circular-dependency


【解决方案1】:

只要你有交叉依赖,比如:

class Card
{
   Game* game;
   void f() {
     game->method1();
   }
}; 

class Game
{
   std::vector<Card> cards;
 public:
   void method1();
};

其中一个依赖项应该实现如下接口:

class IGame
{
 public:
   virtual void method1()=0;
};

class Card
{
  IGame* game;
  void f() {
     game->method1();
   }
}; 

class Game : public IGame
{
   std::vector<Card> cards;
 public:
   virtual void method1();
};

你可以看到没有更多的交叉依赖。

【讨论】:

  • 交叉依赖并不意味着需要接口。前向声明和将代码与标头和源代码分开就足够了,您需要一个抽象接口,它不仅仅用于解决编译问题。
  • @jack 任何交叉依赖都是设计问题。就像你提到的那样,这个问题可能会以多种方式解决,但它仍然是一个设计问题。许多模式都在处理避免循环依赖,比如观察者等等。
  • @SHR 那我需要为每张卡片创建一个新类吗?例如,对于 Card_1,我创建 IGame_1、Card_2 -> IGame_2 等?
  • @Saad 您不必这样做,这是您的选择。所有Card 动作都可以在单个IGame 界面中。也可以设置一种方法,以Card&amp;为参数,Game可以根据给定的Card决定做什么。
【解决方案2】:

您的基本想法是正确的,但您应该进一步增强它以保持高水平的抽象和封装。

因此,在您的示例中,假设您有一个 Card 类,它是现有类的一个实例。

你可以做的第一件事就是将卡牌的效果与卡牌本身分开,例如:

class CardEffect {
 // TODO ...
};

class Card {
private:
  const CardEffect* effect;
};

所以现在卡本身不需要知道任何关于游戏的信息。但是让我们更深入一点:CardEffect 不需要知道Game 类的每一个细节,它需要能够将效果应用到游戏中。这可以通过为效果提供一个单独的接口来完成,该接口只公开应用效果所需的内容,所以就像这样。

class GameInterface {
public:
  virtual const std::vector<Player*>& getPlayers() = 0;

  virtual damagePlayer(Player* player, int amount) = 0;
  virtual applyPeriodicDamage(Player* player, int turns, int amount) = 0

  ..
};

class CardEffect {
  virtual applyEffect(GameInterface* interface) = 0;
};

class Card {
private:
  const CardEffect* effect;
};

现在这只是一个示例,您的问题没有明确的解决方案,因为每个特定场景都不同并且有不同的要求,这只是为了让您了解如何在保持代码优雅和封装的同时保持它描述性强,易于管理。

【讨论】:

  • 谢谢!我曾想过这样做,但一个问题是,如果我想添加一张新卡片,我可能必须更改 gameInterface 并为该卡片创建一个类,而如果我使用游戏指针,我只需要做后者..仍然,我同意这是一种更好的方法(尽管我不明白为什么 GameInterface 中的函数是纯虚拟的..)
  • 因为你可以有一个 LocalGame : public GameInterface 或者一个 RemoteGame : public GameInterface 来实现不同的功能。拥有一个纯粹的界面更简单。你没有添加一个新的Card,你实现了一个新的CardEffectCard 保持不变,因为它通过组合使用效果。这样您就可以使用Card* card = new Card(new MyEffect()), card2 = new Card(new MyEffect2());std::function,您可以直接拥有std::function&lt;void(GameInterface*)&gt; 并忘记大量的内存管理。
  • 但如果我想要一张新卡,我仍然需要更改 GameInterface,对吧?我可以决定我想添加一张卡片,它允许小兵在攻击时分裂,如果这不在 GameInterface 中,那么我必须在那里实现它。另外,Card 中不应该有一个调用 effect->applyEffect 的公共方法吗?但是这种方法将调用一个纯虚函数.. C++ 允许吗?我看到的唯一其他选择是使 CardEffect 成为 Card 的公共字段,这应该没问题,因为我们已经将它声明为 const
  • 您需要提供GameInterface 以获取实现效果所需的构建块。要实现这样的效果,您需要一个由游戏逻辑调用的CardEffect::onMinionAttack(GameInterface* game, Minion* attacker, Minion* defender),它会做一些auto nears = game-&gt;getMinionsNearToMinion(defender); ...。设计一个合适的系统并不容易,这真的取决于你需要什么。
  • 谢谢。最后,我们如何在 GameInterface 中将指针传递给玩家?例如,如果我们想创建一个具体的 CardEffect 将敌方玩家的生命值降低 10,我们需要一个指向该玩家的指针。它可能是这样的:游戏接收输入以打牌 -> 游戏告诉活跃玩家打牌 -> 玩家调用 cardEffect 的 applyEffect 方法,但这个函数体会是什么样子?我认为我们需要在 GameInterface 中保留指向两个玩家的指针..
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-04
  • 2014-11-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多