关于oop:组合类C中的冗余代码

Redundant code in composition class C++

我正在尝试拿起 C 。一切都很顺利,直到我的"练习"计划遇到了很小的障碍。我相信,这个障碍源于设计问题。

想想二十一点(21)。我做了几节课。

  • 卡片
  • 甲板
  • 播放器
  • 一副牌包括——为了简单起见——有一组卡片。
    - 它可以显示所有的卡片
    - 它可以洗牌
    -它可以移除卡片

    一手牌就是一副牌——它的好处是
    - 它可以计算它的手值
    -它可以添加卡片到手

    现在开始我的问题 - 播放器设计

    -玩家有手(私人访问)
    我对播放器的问题是,那只手有一个名为 addCardToHand 的方法函数。如果我必须创建一个名为 addCardToHand(Card c) 的 Player 方法,在该方法中调用并传递给手头的同一方法,我会感到冗余/糟糕的设计。

    将 Hand h 声明为可公开访问的成员,并在 'main()' 中执行类似的操作
    玩家 p;
    卡卡;
    p.h.addCard(aCard);

    任何建议都会很有启发性并受到高度赞赏。请记住我正在学习。


    这里最好的答案是:这取决于:) 不过,我会尽量澄清一下。

    第一个问题是:Player类有没有内部逻辑?如果是Hand的简单容器,我就直接写Player.GetHand().AddCard(),因为没有理由重复Player.AddCard()方法里面的代码,问题就解决了。

    现在让我们假设,需要实现额外的逻辑来将一张牌添加到玩家的手上。这意味着,在将牌添加到手牌时,必须调用 Player 类中的附加代码。在这种情况下,我看到了三种可能的解决方案。

    (来源仅用于演示目的,可能无法编译)

    • 限制对 Hand 的访问,这样任何人都无法从 Player 中检索它。播放器必须实现 AddToHand、RemoveFromHand 等方法。可行,但使用起来不舒服。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      class Player
      {
      private:
          Hand hand;

      public:
          void AddToHand(Card & card)
          {
              hand.Add(card);
          }
      };
    • 使用观察者模式。当用户(类用户)调用 Player.GetHand().AddCard() 时,Hand 会通知 Player,数据已更改,并且 Player 可以采取相应的行动。你可以很容易地使用 C 11 中的 std::function 来实现事件。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      class Deck
      {
      private:
          std::function<void(void)> cardsChanged;

      public:
          void Add(Card card)
          {
              // Add a card
              if (!(cardsChanged._Empty()))
                  cardsChanged();
          }

          void SetCardsChangedHandler(std::function<void(void)> newHandler)
          {
              cardsChanged = newHandler;
          }
      };

      // (...)

      class Player
      {
      private:
          Hand hand;

          void CardsChanged() { ... }
      (...)
      public:
          Player()
          {
              hand.SetCardsChangedHandler([&this]() { this.CardsChanged(); } );              
          }
      };
    • 使用所有必要的接口方法定义 IHand 接口。 Hand 显然应该实现 IHand 并且 Player.GetHand() 应该返回 IHand。诀窍是,Player 返回的 IHand 不一定必须是 Hand 实例,而是可以是充当用户和真实 Hand 实例之间桥梁的装饰器(参见装饰器模式)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      class IHand
      {
      public:
          virtual void Add(Card card) = 0;
          virtual void Remove(Card card) = 0;
      };

      class Hand : public IHand
      {
          // Implementations
      }

      class PlayersHand : public IHand
      {
      private:
          Hand & hand;
          Player & player;

      public:
          PlayersHand(Hand & newHand, Player & newPlayer)
          {
              hand = newHand;
              player = newPlayer;
          }

          void Add(Card card)
          {
              hand.Add(card);
              player.HandChanged();
          }

          // ...
      };

      class Player
      {
      private:
          Hand hand;
          PlayersHand * playersHand;

      public:
          Player()
          {
              playersHand = new PlayersHand(hand, this);
          }

          IHand GetHand()
          {
              return playersHand;
          }
      }

    就个人而言,在第二种情况下,我会选择第二种解决方案——它非常简单,易于扩展和重用,以防进一步需要。


    功能呼叫转移是一种常见的做法。您应该将其视为添加某种程度的抽象。这不是再次做同样的事情(冗余意味着),而是使用另一种方法实现一种方法。

    您可以想象将来会进行一些修改,例如添加 Player 的卡片缓存,或者在用户调用 addCardToHand 时需要更新的其他内容。如果你没有实现转发方法,你会在哪里添加缓存更新代码?

    另请注意,Player::addCardToHand 的"接口"不需要与 Card::addCard 相同,即这些函数中的参数和返回值可以不同。也许在这种情况下它不是那么重要,但通常转发功能是Player的接口和Hand的接口之间可能添加一些翻译的地方。