Вы здесь: Home > Статьи > Эпизод Экстремального Программирования
Что здесь происходит
Правила XP
Статьи по XP
Книги по XP
Ссылки по XP
Обсудить
Написать нам

Эпизод Экстремального Программирования

Оригинал статьи здесь.

Роберт Мартин и Роберт Косс.

Эта статья - часть главы будущей гниги "Advanced Principles, Patterns and Process of Software Development", Robert C. Martin, Prentice Hall, 2001. Copyright © 2000 by Robert C. Martin, all rights reserved.

Если вы не знаете правила боулинга. (Спасибо Alex Kapranoff).


Для того чтобы продемонстрировать практику XP Bob Koss(RSK) и Bob Martin (RCM) парно запрограммируют простую программку на ваших глазах. Мы будем использовать "test first" подход и кучу рефакторинга для создания этой программки.

RCM: "Поможешь мне написать программульку которая считает счет в боулинге?"

RSK: (Сказал себе: В XP парном программировании принято что если кто-то просит о помощи, то отказываться нельзя. Особенно если просит босс.) "Конечно, Боб, не вопрос."

RCM: "Здорово. Я хочу написать приложение которое отслеживает ход лиги по боулингу. Оно должно записывать игры, определять ранг команд, определять победителей и проигравших в еженедельных матчах и вести счет каждой игры."

RSK: "Круто. Я был неплохим игроком в свое время. Это будет здорово. Ты произнес несколько User Story, с какой начнем?"

RCM: "Начнем с подсчета результата игры"

RSK: "Хорошо, что это значит? Что на входе и что на выходе?"

RCM: "Похоже, на входе - последовательность бросков. Бросок - просто целое число, говорящее о том как много фишек сбил шар. Выход - данные для стандартной карточки счета, набора ячеек заполненных количеством фишек, сбитых в каждом броске и значки, обозначающие spares и strikes. Самая важная цифра в каждой ячейке - текущий счет."

RSK: "Дай-ка я нарисую такую карточку чтобы иметь визуальное представление"

[Image]
RCM: "Чувак хреново играет."

RSK: "Или пьяный очень. Но как acceptance test сойдет."

RCM: "Нам и другие понадобятся, но ими позже займемся. Как начнем? Будем дизайнить систему?"

RSK: "Не бей меня, но я не против набольшой UML диаграммы, отражающей проблемную область, которую мы можем видеть по этой карточке. Это покажет нам несколько кандидатов в обьекты котороые мы сможем позже закодировать".

RCM: (Надевает шапку супердизайнера обьектов) "Ok, ясно что обьект Game состоит из последовательности из десяти ячеек. Каждая ячейка - два или три броска (throws)"

RSK: "Голова. Это как раз то что и я подумал. Дай-ка я нарисую диаграммку, только Кенту не говори, я буду все отрицать"

[Image]
Кент: "Я все вижу"

RSK: "Ну выбирай любой класс. Начнем снизу цепочки зависимостей и пойдем наверх? Так тестировать проще."

RCM: "Почему нет? Давай напишем тест для класса Throw"

RSK: (Набирает)

//TestThrow.java---------------------------------
import junit.framework.*;

public class TestThrow extends TestCase
{
 public TestThrow(String name)
 {
   super(name);
 }
// public void test????
}

RSK: "У тебя есть хоть какое-нибудь представление что должен делать класс Throw?"

RCM: "Он хранит число фишек сбитых игроком"

RSK: "Если короче, то на самом деле он ничего не делает. Может лучше нам вернуться и сфокусироваться на классе который имеет какое-нибудь поведение кроме хранения данных."

RCM: "Ммм... Ты имеешь в виду что класс Throw на самом деле не существует?"

RSK: (Покрывается холодным потом - это ведь его босс.) "Ну, даже если у него есть какое-то поведение, то какое это сейчас имеет значение? Я еще не знаю - существует он или нет. Я считаю что более продуктивно будет работать над обьектом у которого есть методы отличные от Get и Set. Но если ты хочешь вести..." (Подвигает клаву к RCM).

RCM: "Хорошо, давай двигаться по цепочке зависимости к Frame и посмотрим можем ли мы написать какие-нибудь тесты для того, чтобы закончить Throw." (Двигает клаву обратно к RSK.)

RSK: (Раздумывая хочет ли RCM проучить его или просто согласился) "Хорошо, новый файл, новый тест."

//TestFrame.java------------------------------------
import junit.framework.*;

public class TestFrame extends TestCase
{
 public TestFrame( String name )
 {
   super( name );
 }

 //public void test???
} 

RCM: "OK, еще раз ты это набрал. Теперь можешь ли ты придумать какой-нибудь интересный тест для Frame (Ячейка в карточке)?"

RSK: "Ячейка должна говорить свой счет, число сбитых фишек в каждом броске, а также был ли strike или spare..."

RCM: "Хватит говорить, набирай!"

RCM: (набирает)

//TestFrame.java---------------------------------
import junit.framework.*;

public class TestFrame extends TestCase
{
 public TestFrame( String name )
 {
   super( name );
 }
 public void testScoreNoThrows()
 {
   Frame f = new Frame();
   assertEquals( 0, f.getScore() );
 }
}
//Frame.java---------------------------------------
public class Frame
{
 public int getScore()
  {

   return 0;
 }
}

RCM: "Ok, тесты прошли. Но Score, на самом деле, тупая функция. Она не будет работать, если если мы добавим бросок к ячейке. Давай напишем тестовый код, который добавляет несколько бросков и затем проверяет счет."

//TestFrame.java---------------------------------
 public void testAddOneThrow()
 {
  Frame f = new Frame();
  f.add(5);
  assertEquals(5, f.getScore());
 }

RCM:"Не компиляется.Метода add нет у Frame."

RSK:"Скомпиляется если добавишь этот метод."

RCM:

//Frame.java---------------------------------------
public class Frame
{
 public int getScore()
 {
   return 0;
 }

 public void add(Throw t)
 {
 }
}

RCM: (Рассуждая вслух) "Это не компиляется, потому что у нас нет класса Throw"

RSK: "Давай поговорим. Тест передает integer, а метод ожидает обьект Throw. Это несоответствие. Перед тем как им заняться, не опишешь ли еще раз его поведение?"

RCM: "Ой, я и не заметил, что написал f.add(5), я должен был написать f.add(new Throw(5)), но это коряво до безобразия. Что я действительно хотел бы написать - f.add(5)."

RSK: "Коряво или нет, давай пока оставим эстетику в покое. Ты можешь описать поведение Throw или нет? Двоичный ответ."

RCM: "1011010111010101. Да не знаю я, есть ли у него какое-нибудь поведение, я начинаю подозревать что Throw это просто int. Но я думаю, что нам не о чем беспокоиться сейчас, поскольку мы можем вызывать Frame.add с параметром int"

RSK: "Тогда я думаю, что мы должны это сделать хотя бы потому, что это просто. Когда это будет неудобно - перепишем на что-нибудь посложнее."

RCM: "Согласен"

//Frame.java---------------------------------------
public class Frame
{
 public int getScore()
 {
   return 0;
 }

 public void add(int pins)
 {
 }
}

RCM: "Ok, компиляется, но тест не проходит. Давай сделаем чтобы работало."

//Frame.java---------------------------------------
public class Frame
{
 public int getScore()
 {
   return itsScore;
 }

 public void add(int pins)
 {
   itsScore += pins;
 }
 private int itsScore = 0;
}

RCM: "Компиляется и тесты проходят. Все пока упрощенно. Какой следующий тест?"

RSK: "Может перерывчик сначала?"


RCM: "Так-то получше будет. Frame.add- хрупкая функция. Что если ее вызовут с числом 11?"

RSK: "Она может выкидывать exception. Но кто ее зовет? Будет ли это application framework который будут использовать тысячи людей и мы должны защищаться от подобных вызовов, или это будем использовать мы и только мы? Если последнее, то не вызывай ее с параметром 11 (гы-гы)."

RCM: "Правильно, тесты в других местах системы отловят неправильный аргумент. Если появятся проблемы, то добавим проверку потом. Итак, функция add пока не может работать со strike и spare. Давай напишем тест который это выявляет."

RSK: "Хмм... если мы вызовем add(10) для srtike, что должен вернуть getScore? Я не знаю как написать проверку, поэтому может быть мы задаем не тот вопрос. Или мы задаем тот вопрос не тому обьекту."

RCM: "Когда ты вызываешь add(10) или add(3) и сразу за этим add(7) то последующий вызов Frame.getScore не имеет смысла. Ячейка (Frame) должна будет посмотреть на предыдущие ячейки чтобы вычислить свой счет. Если эти предыдущие ячейки не существуют, то ей придется возвращать какую нибудь левую -1. Я не хочу возвращать -1."

RSK: "Ага, я тоже терпеть не могу -1. Ты высказал идею о том что ячейки знают о других ячейках. А кто содержит эти ячейки."

RCM: "Обьект Game"

RSK: "Итак Game зависит от Frame и Frame, в свою очередь, зависит от Game. Мне это совсем не нравится."

RCM: "Frame не зависит от Game. Они могут быть организованы в связанный список. Каждый Frame может иметь указатель на предыдущий и следующий Frame. Чтобы получить счет для Frame, Frame посмотрит счет предыдущего Frame и посмотрит вперед есть ли spare или srike."

RSK: "Я чувствую себя идиотом, потому что не могу это представить себе визуально. Покажите мне код, босс."

RCM: "Точно. Значит, сначала нам нужен тест."

RSK: "Для Game или еще один тест для Frame?"

RCM: "Похоже, нам нужен для Game, так как Game будет составлять список из Fram-ов."

RSK:" Ты хочешь оставить то что мы делаем с Frame и переключиться на Game или просто слепить обьект [заглушку] MockGame просто чтобы Frame заработал?"

RCM: "Нет, давай бросим Frame и поработаем над Game. Тесты для Game должны показать что нам нужен связанный список из Fram-ов."

RSK: "Не уверен, что тесты покажут необходимость в списке. Я хочу увидеть код."

RCM:

//TestGame.java------------------------------------------
import junit.framework.*;

public class TestGame extends TestCase
{
 public TestGame(String name)
 {
   super(name);
 }

 public void testOneThrow()
 {
   Game g = new Game();
   g.add(5);
   assertEquals(5, g.score());
 }
}

RCM: "Нормально смотрится?"

RSK: "Ага, только я все еще не вижу доказательств что нам нужен этот самый список из Frame."

RCM: "Такая же фигня. Давай продолжим эти тесты и посмотрим что выйдет."

//Game.java----------------------------------

public class Game
{
 public int score()
 {
   return 0;
 }
 
  public void add(int pins)
 {
 }
}

RCM: "Ok, скомпилялось, но тест не проходит. Давай сделаем чтобы тест проходил."

//Game.java----------------------------------
public class Game
{
 public int score()
 {
   return itsScore;
 }

 public void add(int pins)
 {
   itsScore += pins;
 }
 private int itsScore = 0;
}

RCM: "Прокатило. Круто."

RSK: "Не могу не согласиться. Действительно круто. Только я все жду этого великого доказательства необходимости в связном списке из Frame-ов. Ведь поэтому мы полезли в Game."

RCM: "Угу, я тоже ищу его. Я ожидаю, что как только мы начнем писать тесты для strike и spare, нам понадобиться строить Frame-ы и вязать их в список. Но я не хочу этого делать пока код нас не заставит."

RSK: "Здорово сказал. Давай идти маленькими шажками к Game. Как насчет еще одного теста, который бросает, но без spare?"

RCM: "Ok, такое должно прокатить сразу. Пробую."

//TestGame.java------------------------------------------
public void testTwoThrowsNoMark()
 {
   Game g = new Game();
   g.add(5);
   g.add(4);
   assertEquals(9, g.score());
}

RCM: "Да, проходит. Теперь попробуем четыре шара без очков."

RSK: "Ну этот тоже пройдет. Я этого не ожидал. Мы можем добавлять и добавлять броски и нам все равно не понадобится Frame. Но мы не делали strike или spare еще. Может пора уже сделать."

RCM: "Ну на это я и рассчитываю. Но давай посмотрим этот тест:"

//TestGame.java------------------------------------------
public void testFourThrowsNoMark()
 {
   Game g = new Game();
   g.add(5);
   g.add(4);
   g.add(7);
   g.add(2);
   assertEquals(18, g.score());
   assertEquals(9, g.scoreForFrame(1));
   assertEquals(18, g.scoreForFrame(2));
 }

RCM: "Нормально?"

RCM: "Да. Я забыл, что мы должны быть способны показывать счет для каждого Frame. А, наш набросок послужил подставкой для Кока-Колы. Да, поэтому я и забыл."

RCM: "Ok, сначала давай сделаем чтобы тест не прошел - добавим метод scoreForFrame в Game."

//Game.java------------------------------------------
public int scoreForFrame(int frame)
{
 return 0;
}

RCM: "Здорово, компиляется и тест не проходит. Ну и как теперь сделать чтобы тест проходил?"

RSK: "Можем начать клепать обьекты Frame. Но есть ли это самый простой способ сделать так чтобы тест проходил?"

RCM: "Да нет, мы можем просто создать массив целых чисел в Game. Каждый вызов add будет добавлять новое целое в массив. Каждый вызов scoreForFrame будет пробегать по массиву и вычислять счет."

//Game.java----------------------------------
public class Game
{
 public int score()
 {
   return itsScore;
 }

 public void add(int pins)
   {
     itsThrows[itsCurrentThrow++]=pins;
     itsScore += pins;
   }

 public int scoreForFrame(int frame)
   {
   int score = 0;
   for ( int ball = 0;
      frame > 0 && (ball < itsCurrentThrow);
      ball+=2, frame--)
   {
     score += itsThrows[ball] + itsThrows[ball+1];
   }
   return score;
 }
 private int itsScore = 0;
 private int[] itsThrows = new int[21];
 private int itsCurrentThrow = 0;
}

RCM: (Жутко довольный собой) "Вот, работает."

RSK: "Что за магическое число 21?"

RCM: "Максимальное число бросков в игре."

RSK: "Ох, кажись в юности ты был юниксоидом - хакером который гордился тем, что мог написать целое приложение одним оператором и никто его не мог понять.
ScoreForFrame надо отрефакторить чтобы она стала более читабельной. Но пока не начали рефакторить, позволь задать еще один вопрос: является ли Game лучшим местом для этого метода? По-моему, Game нарушает принцип Бертрана Майера SRP (Принцип Единственной Ответственности). Он получает throws и он знает как посчитать счет в каждом frame. А как ты относишься к обьекту Scorer (счетчик)?"

RCM: (Делает руками вращения похожие на непристойные жесты.) "Да не знаю я, где эта функция должна быть; сейчас мне надо только чтобы тест заработал. Как заработает, тогда и побазарим о SRP.

Однако, я понял намек насчет юниксоида - давай упростим цикл."

public int scoreForFrame(int theFrame)
{
 int ball = 0;
 int score=0;
 for (int currentFrame = 0; 
       currentFrame < theFrame;
       currentFrame++)
 {
   score += itsThrows[ball++] + itsThrows[ball++];
 }

 return score;
}

RCM: "Так немного получше, но есть побочные эффекты в выражении score+=. Они тут не играют роли, потому что неважно в каком порядке два addend выражения вычисляются. (Или важно? Возможно же что два инкремента вызовутся до первой операции с массивом)"

RSK: "Я думаю, что мы можем произвести эксперимент чтобы проверить отсутствие побочных эффектов, но эта функция не работает со spare и strike. Надо ли делать ее более читабельной или сначала закончим функциональность?"

RCM: "Эксперимент будет иметь смысл не на всех компиляторах. Другие компиляторы могут в другом порядке вычислять выражения. Давай избавимся от зависимости от порядка вычисления и затем зафигачим еще тестов."

public int scoreForFrame(int theFrame)
{
 int ball = 0;
 int score=0;
 for (int currentFrame = 0;
       currentFrame < theFrame;
       currentFrame++)
 {
   int firstThrow = itsThrows[ball++];
   int secondThrow = itsThrows[ball++];
   score += firstThrow + secondThrow;
 }
 
  return score;
}

RCM: "Ok, следующий тест. Давай попробуем spare."

public void testSimpleSpare()
{
 Game g = new Game();
}

RCM: "Задолбался я уже это набирать. Давай зарефакторим тест и поместим создание Game в функцию setUp."

//TestGame.java------------------------------------------
import junit.framework.*;

public class TestGame extends TestCase
{
 public TestGame(String name)
 {
   super(name);
 }

 private Game g;

 public void setUp()
 {
   g = new Game();
 }

 public void testOneThrow()
 {
   g.add(5);
   assertEquals(5, g.score());
 }

 public void testTwoThrowsNoMark()
 {
   g.add(5);
   g.add(4);
   assertEquals(9, g.score());
 }

 public void testFourThrowsNoMark()
 {
   g.add(5);
   g.add(4);
   g.add(7);
   g.add(2);
   assertEquals(18, g.score());
   assertEquals(9, g.scoreForFrame(1));
   assertEquals(18, g.scoreForFrame(2));
 }

 public void testSimpleSpare()
 {
 }
}

RCM: "Так-то покруче будет, теперь давай напишем тест для spare."

RSK: "Только я поведу" (Берет клаву)

public int scoreForFrame(int theFrame)
{
 int ball = 0;
 int score=0;
 for (int currentFrame = 0;
       currentFrame < theFrame;
       currentFrame++)
 {
   int firstThrow = itsThrows[ball++];
   int secondThrow = itsThrows[ball++];

   int frameScore = firstThrow + secondThrow;
   // spare needs next frames first throw
   if ( frameScore == 10 )
      score += frameScore + itsThrows[ball++];
   else
     score += frameScore;
 }

 return score;
}

RCM: (Забирает клаву назад) "Только я думаю, что инкремент шара когда frameScore==10 не должен происходить. Смотри, вот тест который это показывает."

public void testSimpleFrameAfterSpare()
{
 g.add(3);
 g.add(7);
 g.add(3);
 g.add(2);
 assertEquals(13, g.scoreForFrame(1));
 assertEquals(18, g.score());
}

RCM: "Ха! Видишь, свалился. Теперь убираем этот дурацкий инкремент..."

if ( frameScore == 10 )
    score += frameScore + itsThrows[ball];

RCM: "Опа, опять свалился... Может метод score лажается? Я попробую поменять чтобы использовать scoreForFrame(2)."

public void testSimpleFrameAfterSpare()
{
 g.add(3);
 g.add(7);
 g.add(3);
 g.add(2);
 assertEquals(13, g.scoreForFrame(1));
 assertEquals(18, g.scoreForFrame(2));
}

RCM: "Хмм... Это прокатило. Значит метод score не фурычит. Давай посмотрим."

public int score()
{
 return itsScore;
}

public void add(int pins)
{
 itsThrows[itsCurrentThrow++]=pins;
 itsScore += pins;
}

RCM: "Ну ясный пень. Метод score просто возвращает число сбитых фишек, а не счет. Надо чтобы score вызывал scoreForFrame для текущего frame."

RSK: "А мы не знаем текущего frame. Давай добавим такой метод для текущих тестов, по одному, конечно."

RCM: "Точно."

//TestGame.java------------------------------------------
 public void testOneThrow()
 {
   g.add(5);
   assertEquals(5, g.score());
   assertEquals(1, g.getCurrentFrame());
   }

//Game.java----------------------------------
 
public int getCurrentFrame()
{
   return 1;
}

RCM: "Ok, так работает. Но это же идиотизм. Давай следующий тест."

public void testTwoThrowsNoMark()
{
 g.add(5);
 g.add(4);
 assertEquals(9, g.score());
 assertEquals(1, g.getCurrentFrame());
}Б.

RCM: "Этот не интересный. Давай попробуем следующий."

public void testFourThrowsNoMark()
{
 g.add(5);
 g.add(4);
 g.add(7);
 g.add(2);
 assertEquals(18, g.score());
 assertEquals(9, g.scoreForFrame(1));
 assertEquals(18, g.scoreForFrame(2));
 assertEquals(2, g.getCurrentFrame());
}

RCM: "Не проходит. Давай чинить."

RSK:" Я думаю, что алгоритм тривиальный. Просто подели число бросков на два, поскольку всегда два броска во Frame. За исключением strike... но пока у нас нет strike-ов, так что можно их игнорировать."

RCM: (Добавляет и удаляет везде код пока не начинает работать.)

public int getCurrentFrame()
{

 return 1 + (itsCurrentThrow-1)/2;
}

RCM: "Не очень-то здорово."

RSK: "Что если мы не будем считать каждый раз? Что если мы будем менять переменную currentFrame после каждого броска?"

RCM: "Ok, давай попробуем."

//Game.java----------------------------------
 public int getCurrentFrame()
 {
   return itsCurrentFrame;
 }

 public void add(int pins)
 {
   itsThrows[itsCurrentThrow++]=pins;
   itsScore += pins;
   if (firstThrow == true)
   {
     firstThrow = false;
     itsCurrentFrame++;
   }
   else
   {
     firstThrow=true;;
   }
 }

 private int itsCurrentFrame = 0;
 private boolean firstThrow = true;
}

RCM: "Ok, работает. Но это также подразумевает что текущий Frame это в котором бросили последний шар, а не тот в котором следующий будет брошен. Пока мы об этом помним, то все в порядке."

RSK: "Ну у меня с памятью туго, поэтому давай сделаем ее более читабельной. Но пока мы не начали все разбирать, давай вынесем этот код из add и положим его в приватный метод adjustCurrentFrame или типа того."

RCM: "Звучит неплохо. Давай."

public void add(int pins)
 {
   itsThrows[itsCurrentThrow++]=pins;
   itsScore += pins;
   adjustCurrentFrame();
 }

 
private void adjustCurrentFrame()
 {
   if (firstThrow == true)
   {
     firstThrow = false;
     itsCurrentFrame++;
   }
   else
   {
     firstThrow=true;;
   }
 
}

RCM: "Теперь давай поменяем имена переменных и функции, чтобы было яснее. Назовем ее itsCurrentFrame?"

RSK: "Мне типа нравится это название. Однако я не думаю что мы инкрементим в правильном месте. По мне текущий фрейм - это тот в котором я сейчас бросаю. Поэтому он должен инкрементиться прямо после последнего броска во Frame."

RCM: "Согласен. Давай поменяем тесты чтобы это отразить. Потом пофиксим adjuctCurrentFrame."

//TestGame.java------------------------------------------
 public void testTwoThrowsNoMark()
 {
   g.add(5);
   g.add(4);
   assertEquals(9, g.score());
   assertEquals(2, g.getCurrentFrame());
 }

 public void testFourThrowsNoMark()
 {
   g.add(5);
   g.add(4);
   g.add(7);
   g.add(2);
   assertEquals(18, g.score());
   assertEquals(9, g.scoreForFrame(1));
   assertEquals(18, g.scoreForFrame(2));
   assertEquals(3, g.getCurrentFrame());
 }
//TestGame.java------------------------------------------
 private void adjustCurrentFrame()
 {
   if (firstThrow == true)
   {
     firstThrow = false;
   }
   else
   {
     firstThrow=true;
     itsCurrentFrame++;
   }
 }

 private int itsCurrentFrame = 1;
}

RCM: "Ok, работает. Давай тестировать getCurrentFrame двумя тестами со spare."

public void testSimpleSpare()
 {
   g.add(3);
   g.add(7);
   g.add(3);
   assertEquals(13, g.scoreForFrame(1));
   assertEquals(2, g.getCurrentFrame());
 }

 public void testSimpleFrameAfterSpare()
 {
   g.add(3);
   g.add(7);
   g.add(3);
   g.add(2);
   assertEquals(13, g.scoreForFrame(1));
   assertEquals(18, g.scoreForFrame(2));
   assertEquals(3, g.getCurrentFrame());
 }

RCM: "Работает. Теперь назад, к исходной проблеме. Надо чтобы score заработал. Теперь можно написать чтобы score вызывал scoreForFrame (getCurrentFrame()-1)."

publicvoidtestSimpleFrameAfterSpare()
 {
   g.add(3);
   g.add(7);
   g.add(3);
   g.add(2);
   assertEquals(13,g.scoreForFrame(1));
   assertEquals(18, g.scoreForFrame(2));
   assertEquals(18, g.score());
   assertEquals(3, g.getCurrentFrame());
 }

//Game.java----------------------------------
 public int score()
 {
   return scoreForFrame(getCurrentFrame()-1);
 }

RCM: "TestOneThrow не проходит. Поглядим на него."

public void testOneThrow()
 {
   g.add(5);
   assertEquals(5, g.score());
   assertEquals(1, g.getCurrentFrame());
 }

RCM: "С одним единственным броском первый Frame не закончен. Метод score вызывает scoreForFrame (0). Это криво."

RSK: "Может быть, а может и нет. Для кого мы пишем эту программу и кто собирается вызывать метод score? Имеет ли смысл предполагать что его вызовут для незаконченного Frame?"

RCM: "Да. Но это меня беспокоит. Чтобы обойти это, мы должны вытащить score из теста testOneThrow. Мы это хотим сделать?"

RSK: "Мы могли бы. Можно даже вообще убрать тест testOneThrow. Он нужен был чтобы привести нас к остальным интересным тестам. Имеет ли он какую либо полезную функцию сейчас? Другие тесты вроде все и так покрывают."

RCM: "Точно. Нафиг его." (Редактирует код, запускает и получает зеленый квадратик). "Ага! Вот так-то лучше."

"Теперь пора бы уж и над strike тестом поработать. В конце концов, мы хотели посмотреть этот самый список из Frame."

public void testSimpleStrike()
 {
   g.add(10);
   g.add(3);
   g.add(6);
   assertEquals(19, g.scoreForFrame(1));
   assertEquals(28, g.score());
   assertEquals(3, g.getCurrentFrame());
 }

RCM: "Компиляется, как можно было догадаться. Теперь сделаем чтобы тест проходил."

//Game.java----------------------------------
public class Game
{
 public void add(int pins)
 {
   itsThrows[itsCurrentThrow++]=pins;
   itsScore += pins;
   adjustCurrentFrame(pins);
 }

 private void adjustCurrentFrame(int pins)
 {
   if (firstThrow == true)
   {
     if( pins == 10 ) // strike
       itsCurrentFrame++;
     else
       firstThrow = false;
   }
   else
   {
     firstThrow=true;
     itsCurrentFrame++;
   }
 }

 public int scoreForFrame(int theFrame)
 {
   int ball = 0;
   int score=0;
   for (int currentFrame = 0;
         currentFrame < theFrame;
         currentFrame++)
   {
     int firstThrow = itsThrows[ball++];
     if (firstThrow == 10)
     {
       score += 10 + itsThrows[ball] + itsThrows[ball+1];
     }
     else
     {
       int secondThrow = itsThrows[ball++];

       int frameScore = firstThrow + secondThrow;
       // spare needs next frames first throw
       if ( frameScore == 10 )
         score += frameScore + itsThrows[ball];
       else
       score += frameScore;
     }
 
    
    }

   return score;
 }
 private int itsScore = 0;
 private int[] itsThrows = new int[21];
 private int itsCurrentThrow = 0;
 private int itsCurrentFrame = 1;
 private boolean firstThrow = true;
}

RCM: "Ok, этобыло несложно. Давай посмотрим, сможет ли она посчитать счет идеальной игры."

public void testPerfectGame()
 {
   for (int i=0; i<12; i++)
   {
     g.add(10);
   }
   assertEquals(300, g.score());
   assertEquals(10, g.getCurrentFrame());
 }

RCM: "Ой, она говорит что счет 330. Почему бы это?"

RCM: "Потому что текущая игра все время инкрементируется до 12."

RCM: "Упс! Мы должны ограничить ее до 10."

private void adjustCurrentFrame(int pins)
 {
   if (firstThrow == true)
   {
     if( pins == 10 ) // strike
       itsCurrentFrame++;
     else
       firstThrow = false;
   }
   else
   {
     firstThrow=true;
     itsCurrentFrame++;
   }
   itsCurrentFrame = Math.min(10, itsCurrentFrame);
 }

RCM: "@#$#@$!! Теперь она говорит что счет 270. Да что за хреновня?"

RSK: "Боб, функция score вычитает единицу из getCurrentFrame, так что она дает тебе счет для 9-го frame, а не для 10-го."

RCM: "Чего? Ты имеешь в виду что я должен был ограничить текущий frame 11, а не 10? Щас попробую."

 itsCurrentFrame = Math.min(11, itsCurrentFrame);

RCM: "Ok, теперь она выдает нормальный счет, но падает потому что текущий frame 11 а не 10. Этот текущий фрейм уже всех достал. Мы хотим чтобы он обозначал фрейм в котором игрок бросает, но какой он имеет смысл по окончании игры?"

RSK: "Может вернемся к варианту когда текущий фрейм - это в котором бросали последний раз?"

RCM: "Может быть примем концепцию completed frame? В конце концов счет игры в любой момент времени - это счет последнего завершенного фрейма."

RSK: "Завершенный фрейм - это ячейка в которую можно уже записать счет, правильно?"

RCM: "Да. Frame со spare завершается после следующего броска. frame со strike завершается после следующих двух бросков. frame без отметок завершается после второго броска в нем."

"Погоди... Мы пытаемся добиться чтобы метод score заработал, верно? Все что для этого надо - заставить score вызвать scoreForFrame(10) если игра завершилась."

RSK: "Но как же мы узнаем что игра завершилась?"

RCM: "Если adjustCurrentFrame попробует инкрементировать itsCurrentFrame после 10-го фрейма, значит игра завершилась."

RSK: "Погоди, ты имеешь в виду что если getCurrentGame возвращает 11 то игра закончена; так же сейчас и работает!"

RCM: "Хмм... То есть мы должны изменить тест в соответствии с кодом?"

 public void testPerfectGame()
 {
   for (int i=0; i<12; i++)
   {
     g.add(10);
   }
   assertEquals(300, g.score());
   assertEquals(11, g.getCurrentFrame());
 }

RCM: "Ну, так работает. Хотя это не хуже чем getMonth, возвращающий 0 для Января, но мне все равно это не нравится."

RSK: "Может что-нибудь позже изменится. А сейчас я вижу баг. Можно? (Хватает клаву.)

 public void testEndOfArray()
 {
   for (int i=0; i<9; i++)
   {
     g.add(0);
     g.add(0);
   }
   g.add(2);
   g.add(8); // 10th frame spare
   g.add(10); // Strike in last position of array.
   assertEquals(20, g.score());
 }
 

RSK: "Интересно. Проходит. Я думал что поскольку на 21 позиции был strike, счетчик попытается добавить 22-ю и 23 позиции в счет. Но, похоже, нет. "

RCM: "Хмм, ты все думаешь об обьекте scorer (счетчик), не правда ли? Ладно, я вижу что ты хотел сказать, но поскольку score никогда не вызывает scoreForFrame с числом большим 10, последний strike на самом деле не засчитался как strike. Он засчитался как 10 в добавок к последнему spare. Мы никогда не выходим за пределы массива. "

RSK: "OK, Давай запихаем оригинальную карточку в программу."

 public void testSampleGame()
 {
   g.add(1);
   g.add(4);
   g.add(4);
   g.add(5);
   g.add(6);
   g.add(4);
   g.add(5);
   g.add(5);
   g.add(10);
   g.add(0);
   g.add(1);
   g.add(7);
   g.add(3);
   g.add(6);
   g.add(4);
   g.add(10);
   g.add(2);
   g.add(8);
   g.add(6);
   assertEquals(133, g.score());
 }
 

RSK: "Это работает. Есть ли еще какие-нибудь тесты у тебя?"

RCM: "Да, давай потестируем некоторые другие граничные условия. Как насчет несчастного лузера который попал 11 страйков и в конце выбил 9."

 public void testHeartBreak()
 {
   for (int i=0; i<11; i++)
     g.add(10);
   g.add(9);
    assertEquals(299, g.score());
 }
 

RCM: "Это работает. Ok, может10-й spare?"

 public void testTenthFrameSpare()
 {
   for (int i=0; i<9; i++)
     g.add(10);
   g.add(9);
   g.add(1);
   g.add(1);
    assertEquals(270, g.score());
 }
}

RCM: (Радостно щерится на зеленый квадратик) "Тоже работает. Я не знаю что еще тестировать, а ты?"

RSK: "Тоже нет. По-моему мы все покрыли. Только я хочу отрефакторить эту помойку. Тут где-то обьект scorer. "

RCM: "OK, например scoreForFrame довольно корявая. Давай ею займемся."

 public int scoreForFrame(int theFrame)
 {
   int ball = 0;
   int score=0;
   for (int currentFrame = 0;
         currentFrame < theFrame;

         currentFrame++)
   {
     int firstThrow = itsThrows[ball++];
     if (firstThrow == 10)
     {
       score += 10 + itsThrows[ball] + itsThrows[ball+1];
     }
     else
     {
       int secondThrow = itsThrows[ball++];
 
        int frameScore = firstThrow + secondThrow;
       // spare needs next frames first throw
       if ( frameScore == 10 )
         score += frameScore + itsThrows[ball];
       else
         score += frameScore;
     }

   }

   return score;
 }
 

RCM: "Я бы выделил тело else в отдельную функцию handleSecondThrow, но не могу – она использует локальные переменные ball, firstThrow, и secondThrow."

RSK: "Мы можем сделать их полями класса."

RCM: "Да, это типа подтверждает твою идею о выносе расчета счета в класс scorer. OK, Давай попробуем."

RSK:(Хватает клаву.)

 private void adjustCurrentFrame(int pins)
 {
   if (firstThrowInFrame == true)
   {
     if( pins == 10 ) // strike
       itsCurrentFrame++;
     else
       firstThrowInFrame = false;
   }
   else
   {
     firstThrowInFrame=true;
     itsCurrentFrame++;
   }
   itsCurrentFrame = Math.min(11, itsCurrentFrame);
 }

 public int scoreForFrame(int theFrame)
 {
   ball = 0;
   int score=0;
   for (int currentFrame = 0;
         currentFrame < theFrame;
         currentFrame++)
   {
     firstThrow = itsThrows[ball++];
     if (firstThrow == 10)
     {
       score += 10 + itsThrows[ball] + itsThrows[ball+1];
     }
     else
     {
       secondThrow = itsThrows[ball++];

       int frameScore = firstThrow + secondThrow;
       // spare needs next frames first throw
       if ( frameScore == 10 )
         score += frameScore + itsThrows[ball];
       else
         score += frameScore;
     }
    }

   return score;
 }
 private int ball;
 private int firstThrow;
 private int secondThrow;

 private int itsScore = 0;
 private int[] itsThrows = new int[21];
 private int itsCurrentThrow = 0;
 private int itsCurrentFrame = 1;
 private boolean firstThrowInFrame = true;
 

RSK: "Не думал что имена совпадут. У нас уже была переменная класса с именем firstThrow. Но на самом деле ей лучше называться firstThrowInFrame. Ну, по-любому, это работает. Теперь мы можем перетащить else в функцию."

 public int scoreForFrame(int theFrame)
 {
   ball = 0;
   int score=0;
   for (int currentFrame = 0;
         currentFrame < theFrame;
         currentFrame++)
   {
     firstThrow = itsThrows[ball++];
     if (firstThrow == 10)
     {
       score += 10 + itsThrows[ball] + itsThrows[ball+1];
     }
     else
     {
       score += handleSecondThrow();
     }
    }

   return score;
 }

 private int handleSecondThrow()
 {
   int score = 0;
   secondThrow = itsThrows[ball++];

   int frameScore = firstThrow + secondThrow;
   // spare needs next frames first throw
   if ( frameScore == 10 )
     score += frameScore + itsThrows[ball];
   else
     score += frameScore;
   return score;
 }

RCM: "Посмотри на структуру scoreForFrame! На псевдокоде это типа:”

if strike
 score += 10 + nextTwoBalls();
else
 handleSecondThrow.

RCM: "Что если заменить на:"

if strike
 score += 10 + nextTwoBalls();
else if spare
 score += 10 + nextBall();
else
 score += twoBallsInFrame()

RSK: "Твою мать! Выглядит как описание правил ведения счета в боулинге, не правда ли? Ok, посмотрим сможем ли мы сделать из этого псевдокода настоящую функцию. Сначала давай поменяем то как переменная ball инкрементируется, так чтобы эти три варианта работали с ней независимо."

 public int scoreForFrame(int theFrame)

 {
   ball = 0;
   int score=0;
   for (int currentFrame = 0;
         currentFrame < theFrame;
         currentFrame++)
   {
     firstThrow = itsThrows[ball];
     if (firstThrow == 10)
     {
       ball++;
       score += 10 + itsThrows[ball] + itsThrows[ball+1];
     }
     else
     {
       score += handleSecondThrow();
     }
    }

   return score;
 }

 private int handleSecondThrow()
 {
   int score = 0;
   secondThrow = itsThrows[ball+1];

   int frameScore = firstThrow + secondThrow;
   // spare needs next frames first throw
   if ( frameScore == 10 )
   {
     ball+=2;
     score += frameScore + itsThrows[ball];
   }
   else
   {
     ball+=2;
     score += frameScore;
   }
   return score;
  }
 

RCM: (Тащит клаву к себе.) "OK, теперь давай пошлем подальше переменные firstThrow и secondThrow и заменим их функциями."

 public int scoreForFrame(int theFrame)
 {
   ball = 0;
   int score=0;
   for (int currentFrame = 0;
         currentFrame < theFrame;
         currentFrame++)
   {
     firstThrow = itsThrows[ball];
     if (strike())
     {
       ball++;
       score += 10 + nextTwoBalls();
     }
     else
     {
       score += handleSecondThrow();
     }
    }

   return score;
 }
 
  private boolean strike()
 {
   return itsThrows[ball] == 10;
 }

 private int nextTwoBalls()
 {
   return itsThrows[ball] + itsThrows[ball+1];
 }
 

RCM: "Работает. Пошли дальше."

 private int handleSecondThrow()
 {
   int score = 0;
   secondThrow = itsThrows[ball+1];

   int frameScore = firstThrow + secondThrow;
   // spare needs next frames first throw
   if ( spare() )
   {
     ball+=2;
     score += 10 + nextBall();
   }
   else
   {
     ball+=2;
     score += frameScore;
   }
   return score;
  }

 private boolean spare()
 {
   return (itsThrows[ball] + itsThrows[ball+1]) == 10;
 }

 private int nextBall()
 {
   return itsThrows[ball];
 }

RCM: "OK, и это работает. Теперь разберемся с frameScore."

 private int handleSecondThrow()
 {
   int score = 0;
   secondThrow = itsThrows[ball+1];


   int frameScore = firstThrow + secondThrow;
   // spare needs next frames first throw
   if ( spare() )
   {
     ball+=2;
     score += 10 + nextBall();
   }
   else
   {
     score += twoBallsInFrame();
     ball+=2;
   }
   return score;
  }

 private int twoBallsInFrame()
 {
   return itsThrows[ball] + itsThrows[ball+1];
 }
 

RSK: "Боб, ты не инкрементишь ball последовательным образом. В случае spare и strike ты инкрементишь перед вычислением score. В случае twoBallsInFrame ты инкременишь после вычисления score. И код зависит от порядка. Что за фигня?"

RCM: "Извиняюсь, я должен был сразу обьяснить. Я планировал перенести инкременты в strike, spare, и twoBallsInFrame. Тогда они исчезнут из функции scoreForFrame и она будет работать точно как тот псевдокод."

RSK:" OK, поверю на несколько шагов вперед, но помни – я смотрю."

Kent:"И я."

RCM:" OK, теперь, поскольку никто больше не использует firstThrow, secondThrow и frameScore, мы можем избавиться от них."

 public int scoreForFrame(int theFrame)
 {
   ball = 0;
   int score=0;
   for (int currentFrame = 0;
         currentFrame < theFrame;
         currentFrame++)
   {
     if (strike())
     {
       ball++;
       score += 10 + nextTwoBalls();
     }
     else
     {
       score += handleSecondThrow();
     }
    }

   return score;
 }

 private int handleSecondThrow()
 {
   int score = 0;
   // spare needs next frames first throw
   if ( spare() )
   {
     ball+=2;
     score += 10 + nextBall();
   }
   else
   {
     score += twoBallsInFrame();
     ball+=2;
   }
   return score;
  }

RCM: (В глазах отражается зеленый квадратик.) "Теперь, поскольку единственной переменной связывающей три варианта является ball, и поскольку с ball работают каждый раз независимо, мы можем соединить эти три ветки вместе."

 public int scoreForFrame(int theFrame)
 {
   ball = 0;
   int score=0;
   for (int currentFrame = 0;
         currentFrame < theFrame;
         currentFrame++)
   {
     if (strike())
     {
       ball++;
       score += 10 + nextTwoBalls();
     }
     else if ( spare() )
     {
       ball+=2;
       score += 10 + nextBall();
     }
     else
     {
       score += twoBallsInFrame();
       ball+=2;
     }
   }

   return score;
 }
 

RSK: (Хрипит как Peter Lorrie) "Хозяин... Хозяин... Дайте я сделаю. Пожалуйста, дайте мне."

RCM: "А, Игорь, ты хочешь переместить инкременты?"

RSK: "Да, Хозяин. О, да,Хозяин." (Забирает клаву.)

 public int scoreForFrame(int theFrame)
 {
   ball = 0;
   int score=0;
   for (int currentFrame = 0;
         currentFrame < theFrame;
         currentFrame++)
   {
     if (strike())
       score += 10 + nextTwoBalls();
     else if (spare())
       score += 10 + nextBall();
     else
       score += twoBallsInFrame();
   }

   return score;
 }

 private boolean strike()
 {
   if (itsThrows[ball] == 10)
   {
     ball++;
     return true;
   }
   return false;
 }

 private boolean spare()
 {
   if ((itsThrows[ball] + itsThrows[ball+1]) == 10)
   {
     ball += 2;
     return true;
   }
   return false;
 }

 private int nextTwoBalls()
 {
   return itsThrows[ball] + itsThrows[ball+1];
 }

 private int nextBall()
 {
   return itsThrows[ball];
 }

 private int twoBallsInFrame()
 {
   return itsThrows[ball++] + itsThrows[ball++];
 }

RCM: "Хорошая работа, Игорь!"

RSK: "Спасибо, Хозяин."

RCM: "Посмотри на функцию scoreForFrame. Вот наиболее кратко сформулированные правила боулинга."

RSK: "Но Боб, а как же связный список обьектов Frame?" (гнусно хихикает)

RCM: (Крестится) "Нас попутал демон диаграммного передизайна. Бог мой, всего три прямоугольника нарисованные на салфетке, Game, Frame, и Throw, и все равно это было переусложнено и совсем неверно."

RSK: "Мы ошиблись, начав с класса Throw. Надо было начинать с Game!"

RCM: "Точно! То есть в следующий раз начнем сверху и пойдем вниз."

RSK: (Шепчет) "Дизайн сверху вниз (TopDown)!??!?! То есть DeMarco все это время был прав?"

RCM: "Поправка: TopDownTestFirstDesign (Сверху вних и тест сначала). На самом деле, я не знаю хорошее это правило или плохое. Это то, что помогло нам в данном случае. В следующий раз попробуем и посмотрим что получится."

RSK: "Ага, OK. По-любому, нам осталось сделать кое-какой рефакторинг. Переменная ball это просто приватный итератор для scoreForFrame и его подчиненных. Они все должны быть перенесены в другой обьект."

RCM: "А, ну да – твой любимый обьект Scorer. Ну в конце-концов, ты оказался прав. Давай сделаем."

RSK: (Забирает клаву и делает несколько маленьких итераций сопровождая их запуском тестов. В результате получает следующее:)

//Game.java----------------------------------
public class Game
{
 public int score()
 {
   return scoreForFrame(getCurrentFrame()-1);
 }

 public int getCurrentFrame()
 {
   return itsCurrentFrame;
 }

 public void add(int pins)
 {
   itsScorer.addThrow(pins);
   itsScore += pins;
   adjustCurrentFrame(pins);
 }

 private void adjustCurrentFrame(int pins)
 {
   if (firstThrowInFrame == true)
   {
     if( pins == 10 ) // strike
       itsCurrentFrame++;
     else
       firstThrowInFrame = false;
   }
   else
   {
     firstThrowInFrame=true;
     itsCurrentFrame++;
   }
   itsCurrentFrame = Math.min(11, itsCurrentFrame);
 }
 
  public int scoreForFrame(int theFrame)
 {
   return itsScorer.scoreForFrame(theFrame);
 }

 private int itsScore = 0;
 private int itsCurrentFrame = 1;
 private boolean firstThrowInFrame = true;
 private Scorer itsScorer = new Scorer();
}

//Scorer.java-----------------------------------
public class Scorer
{
 public void addThrow(int pins)
 {
   itsThrows[itsCurrentThrow++] = pins;
 }

 public int scoreForFrame(int theFrame)
 {
   ball = 0;
   int score=0;
   for (int currentFrame = 0;
         currentFrame < theFrame;
         currentFrame++)
   {
     if (strike())
       score += 10 + nextTwoBalls();
     else if (spare())
       score += 10 + nextBall();
     else
       score += twoBallsInFrame();
   }

   return score;
 }

 private boolean strike()
 {

   if (itsThrows[ball] == 10)
   {
     ball++;
     return true;
   }
   return false;
 }

 private boolean spare()
 {
   if ((itsThrows[ball] + itsThrows[ball+1]) == 10)
   {
     ball += 2;
     return true;
   }
   return false;
 }

 private int nextTwoBalls()
 {
   return itsThrows[ball] + itsThrows[ball+1];
 }

 private int nextBall()
 {
   return itsThrows[ball];
 }

 private int twoBallsInFrame()
 {
   return itsThrows[ball++] + itsThrows[ball++];
 }

 private int ball;
 private int[] itsThrows = new int[21];
 private int itsCurrentThrow = 0;

}

RSK: "Так-то получше.Теперь Game просто отслеживает фреймы, а Scorer просто считает счет. SRP-круто!"

RCM: "Все равно. Но так точно лучше. А ты заметил, что itsScore больше не используется?"

RSK: "Ха! Точно. Иди сюда..." (Злобно стирает все.)

 public void add(int pins)
 {
   itsScorer.addThrow(pins);
   adjustCurrentFrame(pins);
 }
 

RSK:"Неплохо. Теперь почистим adjustCurrentFrame?"

RCM:"OK, давай посмотрим."

 private void adjustCurrentFrame(int pins)
 {
   if (firstThrowInFrame == true)
   {
     if( pins == 10 ) // strike
       itsCurrentFrame++;
     else
       firstThrowInFrame = false;
   }
   else
   {
     firstThrowInFrame=true;
     itsCurrentFrame++;
   }
   itsCurrentFrame = Math.min(11, itsCurrentFrame);
 }
 

RCM:"OK, давай сначала выделим инкремент в отдельную функцию которая также ограничивает фрейм до 11. (Брр... Не нравится мне эта 11.)"

RSK:"Боб, 11 значит конец игры."

RCM:"Да. Брр...." (Хватает клаву и делает пару изменений сопровождаемых прогоном тестов.)

 private void adjustCurrentFrame(int pins)
 {
   if (firstThrowInFrame == true)
   {
     if( pins == 10 ) // strike
       advanceFrame();
     else
       firstThrowInFrame = false;
   }
   else
   {
     firstThrowInFrame=true;
     advanceFrame();
   }
 }
 private void advanceFrame()
 {
   itsCurrentFrame = Math.min(11, itsCurrentFrame + 1);
 }

RCM:"OK, так лучше. Теперь давай перенесем случай strike в отдельную функцию."(Делает несколько маленьких изменений запуская тест между ними.)

 private void adjustCurrentFrame(int pins)
 {
   if (firstThrowInFrame == true)
   {
     if (adjustFrameForStrike(pins) == false)
       firstThrowInFrame = false;
   } 
    else
   {
     firstThrowInFrame=true;
     advanceFrame();
   }
 }

 private boolean adjustFrameForStrike(int pins)
 {
   if (pins == 10)
   {
     advanceFrame();
     return true;
   }
   return false;
 }
 

RCM:"Вот так здорово. Теперь об 11."

RSK:"Эй, ты ее не любишь похоже."

RCM:"Да, посмотри на функцию score:"

 public int score()
 {
   return scoreForFrame(getCurrentFrame()-1);
 }
 

RCM:"Этот -1 воняет. Это единственное место где мы реально используем результат getCurrentFrame, и мы его должны еще и подправлять."

RSK:"Черт, точно. Сколько раз мы уже это меняли?"

RCM:"Слишком много.Но что делать. Код хочет чтобы itsCurrentFrame представляла фрейм последнего брошенного шара, а не тот в котором бросать будем."

RSK:"Ой, это развалит половину тестов."

RCM:"Вообще-то, я думаю надо убрать getCurrentFrame из всех тестов и убрать саму функцию getCurrentFrame. Никто ее на самом деле не использует."

RSK:"OK, Понимаю. Сейчас сделаю." (Берет клаву.)

//Game.java----------------------------------
 public int score()
 {
   return scoreForFrame(itsCurrentFrame);
 }

 private void advanceFrame()
 {
   itsCurrentFrame = Math.min(10, itsCurrentFrame + 1);
 }
 

RCM:"Сейчас заплачу. Зачем мы все это время носились туда-сюда, добавляли 11 к 10 и прибавляли -1. Тупизм."

RSK:"Да, дядя Боб, это не стоило тех усилий, котороые мы приложили."

RCM:"OK, кажись все готово. Давай еще раз просмотрим все – проста ли и понятна наша прогамма настолько насколько возможно." 

//Game.java----------------------------------
public class Game
{
 public int score()
 {
   return scoreForFrame(itsCurrentFrame);
 }

 public void add(int pins)
 {
   itsScorer.addThrow(pins);
   adjustCurrentFrame(pins);
 }

 private void adjustCurrentFrame(int pins)
 {
   if (firstThrowInFrame == true)
   {
     if (adjustFrameForStrike(pins) == false)
       firstThrowInFrame = false;
   }
   else
   {
     firstThrowInFrame=true;
     advanceFrame();
   }
 }

 private boolean adjustFrameForStrike(int pins)
 {
   if (pins == 10)
   {
     advanceFrame();
     return true;
   }
   return false;
 } 

 private void advanceFrame()
 {
   itsCurrentFrame = Math.min(10, itsCurrentFrame + 1);
 }

 public int scoreForFrame(int theFrame)
 {
   return itsScorer.scoreForFrame(theFrame);
 }

 private int itsCurrentFrame = 0;
 private boolean firstThrowInFrame = true;
 private Scorer itsScorer = new Scorer();
}

//Scorer.java-----------------------------------
public class Scorer
{
 public void addThrow(int pins)
 {
   itsThrows[itsCurrentThrow++] = pins;
 }

 public int scoreForFrame(int theFrame)
 {
   ball = 0;
   int score=0;
   for (int currentFrame = 0;
         currentFrame < theFrame;
         currentFrame++)
   {
     if (strike())

       score += 10 + nextTwoBalls();
     else if (spare())
       score += 10 + nextBall();
     else
       score += twoBallsInFrame();
   }

   return score;
 }

 private boolean strike()
 {
   if (itsThrows[ball] == 10)
   {
     ball++;
     return true;
   }
   return false;
 }

 private boolean spare()
 {
   if ((itsThrows[ball] + itsThrows[ball+1]) == 10)
   {
     ball += 2;
     return true;
   }
   return false;
 }

 private int nextTwoBalls()
 {
   return itsThrows[ball] + itsThrows[ball+1];
 }

 private int nextBall()
 {
   return itsThrows[ball];
 }

 private int twoBallsInFrame()
 {
   return itsThrows[ball++] + itsThrows[ball++];
 }

 private int ball;
 private int[] itsThrows = new int[21];
 private int itsCurrentThrow = 0;
}

RCM:"OK, выглядит неплохо.Я не знаю что еще можно добавить."

RSK:"Да, приятно. Давай на тесты посморим для порядка."

//TestGame.java------------------------------------------
import junit.framework.*;

public class TestGame extends TestCase
{
 public TestGame(String name)
 {
   super(name);
 }

 private Game g;

 public void setUp()
 {
   g = new Game();
 }

 public void testTwoThrowsNoMark()
 {
   g.add(5);
   g.add(4);
   assertEquals(9, g.score());
 }

 public void testFourThrowsNoMark()
 {
   g.add(5);
   g.add(4);
   g.add(7);
   g.add(2);
   assertEquals(18, g.score());
   assertEquals(9, g.scoreForFrame(1));
   assertEquals(18, g.scoreForFrame(2));
 }

 public void testSimpleSpare()
 {
   g.add(3);
   g.add(7);
   g.add(3);
   assertEquals(13, g.scoreForFrame(1));
 }

 public void testSimpleFrameAfterSpare()
 {
   g.add(3);
   g.add(7);
   g.add(3);
   g.add(2);
   assertEquals(13, g.scoreForFrame(1));
   assertEquals(18, g.scoreForFrame(2));
   assertEquals(18, g.score());
 }

 public void testSimpleStrike()
 {
   g.add(10);
   g.add(3);
   g.add(6);
   assertEquals(19, g.scoreForFrame(1));
   assertEquals(28, g.score());
 }

 public void testPerfectGame()
 {
   for (int i=0; i<12; i++)
   {
     g.add(10);
   }
   assertEquals(300, g.score());
 }

 public void testEndOfArray()
 {
   for (int i=0; i<9; i++)
   {
     g.add(0);
     g.add(0);
   }
   g.add(2);
   g.add(8); // 10th frame spare
   g.add(10); // Strike in last position of array.
   assertEquals(20, g.score());
  }

 public void testSampleGame()
 {
   g.add(1);
   g.add(4);
   g.add(4);
   g.add(5);
   g.add(6);
   g.add(4);
   g.add(5);
   g.add(5);
   g.add(10);

   g.add(0);
   g.add(1);
   g.add(7);
   g.add(3);
   g.add(6);
   g.add(4);
   g.add(10);
   g.add(2);
   g.add(8);
   g.add(6);
   assertEquals(133, g.score());
 }

 public void testHeartBreak()
 {
   for (int i=0; i<11; i++)
     g.add(10);
   g.add(9);
    assertEquals(299, g.score());
 }

 public void testTenthFrameSpare()
 {
   for (int i=0; i<9; i++)
     g.add(10);
   g.add(9);
   g.add(1);
   g.add(1);
    assertEquals(270, g.score());
 }
}

RSK:"Ну это в принципе все покрывает. Ты можешь придумать какие-нибудь более крутые тесты?"

RCM:"Да нет, кажется это все. И я не вижу никаких которые стоит удалить."

RSK:"Тогда мы закончили."

RCM:"Похоже на то.Спасибо за помощь."

RSK:"Без проблем – это было здорово."

Обсудить