Смешение знания при реализации карточной игры


Исходный код

public TourResult NextTour()
      {
        TourResult lresult = new TourResult();
        lresult.TourNum = mCurrentTour;
        lresult.IsEnd = false;
        Card.Face[] lCurrFaces = getCurrentTourFace();
        int lMaxFace = 0;
        foreach (Card.Face lFace in lCurrFaces)
        {
          lMaxFace = ((int)lFace) > lMaxFace ? (int)lFace : lMaxFace;
        }
        int lMaxFaceCount = 0;
        int lNotZeroCount = 0;
        foreach (Card.Face lFace in lCurrFaces)
        {
          lMaxFaceCount = (int)lFace == lMaxFace ? lMaxFaceCount + 1 : lMaxFaceCount + 0;
          lNotZeroCount = (int)lFace != 0 ? lNotZeroCount + 1 : lNotZeroCount + 0;
        }

        if (lMaxFaceCount > 1)
        {
          mUnknowTour = mUnknowTour > -1 ? mUnknowTour : mCurrentTour;
          mCurrentTour++;
          lresult.Message = "Одинаковые карты, надо разиграть";
          lresult.PlayerIndex = -1;
          lresult.Face = (Face)lMaxFace;
          return lresult;
        }
        int lMaxFaceIndex;
        for (lMaxFaceIndex = 0; lMaxFaceIndex < lCurrFaces.Length; lMaxFaceIndex++)
        {
          if (((int)lCurrFaces[lMaxFaceIndex]) == lMaxFace)
            break;
        }
        if (lNotZeroCount < 2)
        {
          lresult.Message = "победил";
          lresult.PlayerIndex = lMaxFaceIndex;
          lresult.IsEnd = true;
          return lresult;
        }
        int lPlaerCardsCount = mArr[lMaxFaceIndex].Count;
        if (mUnknowTour > -1)
        {
          for (int i = mUnknowTour; i <= mCurrentTour; i++)
          {
            AddCardsToPlayer(i, lMaxFaceIndex);
          }
          mUnknowTour = -1;
        }
        else
        {
          AddCardsToPlayer(mCurrentTour, lMaxFaceIndex);
        }

        lresult.Message = "загреб краты";
        for (int i = lPlaerCardsCount; i < mArr[lMaxFaceIndex].Count; i++)
        {
          lresult.Message += string.Format(" {0},", GetCardName(mArr[lMaxFaceIndex][i]));
        }

        lresult.PlayerIndex = lMaxFaceIndex;
        mCurrentTour++;
        return lresult;
      }

Что не так в исходном коде

Приведённый метод заключает в себе знания о слишком многих вещах: как понять, какая ситуация сложилась в игре, и что делать в каждой ситуации. Все подробности свалены в одну кучу, и не сразу понятно, что к чему относится.

Положение усугубляются тем, что не все названия переменных дают чёткое представление об их смысле.

Также не в полной мере использованы возможности объектного программирования (например, метод получения названия карты не инкапсулирован в класс карты).

Как улучшить исходный код

Для рефакторинга длинных простыней кода я применяю «метод псевдокода». В общих чертах он заключается в том, что надо проанализировать существующий код и предельно кратко пересказать, что в этом коде происходит. Дальше этот пересказ нужно записать формально в виде псевдокода, а затем переписать один в один в виде кода на конечном языке. Самое важное в методе, чтобы в конечном коде не содержалось больше подробностей, чем есть в записи на псевдокоде.

После этого добавляются все недостающие подробности, но так, чтобы они размещались в отдельных методах и классах.

Кроме улучшения кода отдельной функции метод псевдокода может помочь улучшить и структуру классов.

Метод псевдокода на практике

Поскольку в основе алгоритма лежат правила карточной игры, то нет необходимости записывать псевдокод в точном соответствии с уже реализованным алгоритмом — вполне достаточно соответствия правилам.

снять на кон по одной карте каждого оставшегося игрока
находим всех игроков с самой старшей картой
если есть один игрок с самой старшей картой
 то он забирает все карты с кона, 
    игроки без карты выбывают из игры,
    если кон пустой, и есть только один игрок с картами
     то игра закончена и он победитель 
если есть несколько игроков с самой старшей картой
 то продолжаем игру

Переписываю на конечный язык:

public TourResult MakeTour()
{
  CardStake stake = activePlayers.DealCards();
  Player[] greaterCardPlayers = stake.playersWithGreaterCard();
  if (greaterCardPlayers.Count() == 1) {
    greaterCardPlayers[0].getStake(stake);

    activePlayers.ExcludeWithoutCards();
    if (activePlayers.isLeftOne()) {
      return new TourResultWin(activePlayers.LastPlayer());
    }
    return new TourResultCardsTaken(greaterCardPlayers[0], stake);
  }
  return new TourResultContinue();
}

Теперь алгоритм игры предельно понятен и доступен для внесения изменений. Чтобы спрятать все второстепенные знания, мне понадобились несколько дополнительных классов:

  • «кон» для операций с картами на кону игры (поиск игроков с самой старшей картой)
  • «карта на кону» — связывает конкретную карту с игроком, от которого она пришла
  • «активные игроки» — для операций с игроками (сдать карты на кон; исключить из игры игроков без карт; проверить, что остался только один)
  • иерархия классов «результат хода»: один родитель и три наследника по одному для каждого вида результата, каждый со своим набором полей

Не следует думать, что увеличившееся количество классов усложняет проект. Все эти сущности в любом случае присутствуют в предметной области, вопрос лишь в том, выразим ли мы их в коде явно как отдельные сущности со своими собственными свойствами и поведением, или же размажем эти свойства и поведение по коду каких-то других сущностей.

Моя личная практика программирования убедительно показала, что чем точнее множество классов проекта соответствует сущностям предметной области, тем проще такой проект писать, отлаживать, исправлять, улучшать и развивать.

Теория