Предваряющее тестирование при экстремальной разработке
Предваряющее тестирование и рефакторинг (реорганизация) — основной способ разработки при экстремальном программировании.
Обычно рефакторингом называют внесение в код небольших изменений, сохраняющих функциональность и улучшающих структуру программы. Более широко рефакторинг определяют как технику разработки ПО через множество изменений кода, направленных на добавление функциональности и улучшение структуры.
Предваряющее (test-first) тестирование и рефакторинг — это способ создания и последующего улучшения ПО, при котором сначала пишутся тесты, а затем программируется код, который будет подвергаться этим тестам. Программист выбирает задачу, затем пишет тестовые варианты, которые приводят к отказу программы, так как программа еще не выполняет данную задачу. Далее он модифицирует программу так, чтобы тесты проходили и задача выполнялась. Программист продолжает писать новые тестовые варианты и модифицировать программу (для их выполнения) до тех пор, пока программа не будет исполнять все свои обязанности. После этого программист небольшими шагами улучшает ее структуру (проводит рефакторинг), после каждого из шагов запускает все тесты, чтобы убедиться, что программа по-прежнему работает.
Для демонстрации такого подхода рассмотрим пример конкретной разработки. Будем опираться на технику, описанную Робертом Мартином (с любезного разрешения автора)*.
* Robert C. Martin. RUP/XP Guidelines: Test-first Design and Refactoring. - Rational Software White Paper, 2000.
Создадим программу для регистрации посещений кафе-кондитерской. Каждый раз, когда лакомка посещает кафе, вводится количество купленных булочек, их стоимость и текущий вес любителя (любительницы) сладостей. Система отслеживает эти значения и выдает отчеты. Программу будем писать на языке Java.
Для экстремального тестирования удобно использовать среду Junit, авторами которой являются Кент Бек и Эрик Гамма (Kent Beck и Erich Gamma). Прежде всего создадим среду для хранения тестов модулей. Это очень важно для предваряющего тестирования: вначале пишется тестовый вариант, а только потом — код программы.
Необходимый код имеет следующий вид:
Листинг 16.1. ТестЛакомки. java
import junit.framework.*;
public class ТестЛакомки extends TestCase
{
public ТестЛакомки (String name)
{
super(name);
}
}
Видно, что при использовании среды Junit класс-контейнер тестовых вариантов должен быть наследником от класса TestCase. Кстати, условимся весь новый код выделять полужирным шрифтом.
Создадим первый тестовый вариант. Одна из целей создаваемой программы — запись количества посещений кафе. Поэтому должен существовать объект ПосещениеКафе, содержащий нужные данные. Следовательно, надо написать тест, создающий этот объект и опрашивающий его свойства. Тесты будем записывать как тестовые функции (их имена должны начинаться с префикса тест). Введем тестовую функцию тестСоздатьПосещениеКафе (листинг 16.2).
Листинг 16.2. ТестЛакомки.jауа
import junit.framework.*;
public class ТестЛакомки extends TestCase
{
public ТестЛакомки (String name)
{
super(name);
}
public void тестСоздатьПосещениеКафе()
{
ПосещениеКафе v = new-ПосещениеКафе();
}
}
Для компиляции этого фрагмента подключим класс ПосещениеКафе.
Листинг 16.3. ТестЛакомки.jаvа и ПосещениеКафе.jаvа
ТестЛакомки.jаvа
import junit. framework.*;
import ПосещениеКафе;
public class ТестЛакомки extends TestCase
{
public ТестЛакомки (String name)
{
super(name);
}
public void тестСоздатьПосещениеКафе()
{
ПосещениеКафе v = new ПосещениеКафе();
}
}
ПосещениеКафе.java
public class ПосещениеКафе
{
}
Этот код компилируется, тест проходит, и мы готовы добавить необходимую функциональность.
Листинг 16.4. ТестЛакомки.jауа и ПосещениеКафе.jауа
ТестЛакомки.java
import junit.framework.*;
import ПосещениеКафе;
import java.util.Date
public class ТестЛакомки extends TestCase
{
public TecтЛакомки(String name)
{
super(name):
}
public void тестСоздатьПосещениеКафе()
{
Date дата = new Date();
double булочки = 7.0; // 7 булочек
double стоимость = 12.5 * 7;
// цена 1 булочки - 12.5 руб.
double вес = 60.0; // взвешивание лакомки
double дельта = 0.0001; // точность
ПосещениеКафе v =
new ПосещениеКафе(дата, булочки, стоимость, вес);
assertEquals(дата, v.получитьДату( ));
assertEquals(12.5 * 7, v.получитьСтоииость(), дельта);
assertEquals(7.0, v.получитьБулочки(), дельта);
assertEquals(60.0, v.получитьВес(), дельта);
assertEquals(12.5, v.получитьЦену(). дельта);
}
}
ПосещениеКафе.java
import Java.uti1.Date;
public class ПосещениеКафе
{
private Date егоДата;
private double егоБулочки;
private double егоСтоимость;
private double eroBec;
public ПосещениеКафе(Date дата, double булочки,
double стоимость, double вес)
{
егоДата = дата;
егоБулочки = булочки;
егоСтоимость = стоимость;
егоВес = вес;
}
public Date получитьДату() {return егоДата;}
public double получитьБулочки() {return егоБулочки;}
public double получитьСтоимость() {return егоСтоимость;}
public double получитьЦену(){return егоСтоимость/егоБулочки;}
public double получитьВес() {return eroBec;}
}
На этом шаге мы добавили тесты в класс ТестЛакомки, а также добавили методы в класс ПосещениеКафе. Унаследованные методы assertEquals позволяют проводить сравнение ожидаемых и фактических результатов тестирования.
Очевидно, вы удивитесь этому подходу. Неужели нельзя вначале написать весь код класса ПосещениеКафе, а потом создать тесты? Ответ достаточно прост. Написание тестов перед написанием программного кода дает важное преимущество: мы знаем, что весь ранее созданный код компилируется и выполняется. Следовательно, любая ошибка вызывается текущими изменениями, а не более ранним кодом. И значимость этого преимущества усиливается по мере продвижения вперед.
Далее определимся с хранением объектов класса ПосещениеКафе. Очевидно, что свойство егоВес характеризует лакомку. Таким образом, объект ПосещениеКафе записывает часть состояния лакомки па момент посещения кафе. Следовательно, нужно создать объект Лакомка и содержать объекты класса ПосещениеКафе в нем.
Листинг 16.5. ТестЛакомки.java и Лакомка.java
ТестЛакомки.java
import junit.framework.*;
import ПосещениеКафе;
import java.util.Date
public class ТестЛакомки extends TestCase
{
public TecтЛакомки(String name)
{
super(name);
}
…
public void тестСоздатьЛакомку()
{
Лакомка g = new Лакомка();
assertEquals(0, д.получитьЧислоПосещений());
}
}
Лакомка.Java
public class Лакомка
{
public int получитьЧислоПосещений()
{
return 0;
}
}
Листинг 16. 5 показывает начальный шаг. Мы написали новую тестовую функцию тестСоздатьЛакомку. Эта функция создает объект класса Лакомка и затем убеждается, что хранимое количество посещений равно 0. Конечно, реализация метода получитьЧислоПосещений неверна, но она обеспечивает прохождение теста. Это позволит нам в будущем выполнить рефакторинг (для улучшения решения).
Введем в класс Лакомку объект-контейнер, хранящий данные о разных посещениях (как элементы списка в массиве изменяемого размера). Для его создания используем класс-контейнер Array List из библиотеки Java 2. В будущем нам потребуются три метода контейнера: add (добавить элемент в контейнер), get (получить элемент из контейнера), size (вернуть количество элементов в контейнере).
Листинг 16.6. ЛАKOMKА.java
import java.util.ArrayList;
public class Лакомка
{
private ArrayList егоПосещения = new ArrayList();
// создание объекта егоПосещения - контейнера посещений
public int получитьЧислоПосещений ()
{
return егоПосещения.size();
// возврат количества элементов в контейнере
// оно равно количеству посещений кафе
}
}
Отметим, что после каждого изменения мы прогоняем все тесты, а не только функцию тестСоздатьЛакомку. Это дает гарантию, что изменения не испортили уже работающий код.
На следующем шаге следует определить, как к Лакомке добавляется посещение кафе. Так будет выглядеть простейший тестовый вариант:
Листинг 16.7. TecтЛакомки.java
public void тестДобавитьПосещение()
{
double булочки = 7.0; // 7 булочек
double стоимость = 12.5 * 7; // цена 1 булочки = 12.5 руб.
double вес = 60.0; // взвешивание лакомки
double дельта = 0.0001; // точность
Лакомка g = new Лакомка();
g.добавитьПосещениеКафе(булочки, стоимость, вес);
assertEquals(1, g.получитьЧислоПосещений());
}
В этом тесте объект класса ПосещениеКафе не создается. Очевидно, что создавать объект и добавлять его в список должен метод добавитьПосещениеКафе объекта Лакомка.
Листинг 16.8. Лакомка.jауа
public void добавитьПосещениеКафе((double булочки, double стоимость, double вес)
{
ПосещениеКафе v =
new ПосещениеКафе(new Date(), булочки, стоимость, вес);
егоПосещения.add(v);
// добавление эл-та v в контейнер посещений
}
Опять прогоняются все тесты. Анализ программного кода в функциях тестДобавитьПосещение и тестСоздатьПосещениеКафе показывает, что он частично дублируется. Обе функции создают одинаковые локальные переменные и инициализируют их одинаковыми значениями. Чтобы избавиться от дублирования, проведем рефакторинг тестируемой программы и сделаем локальные переменные свойствами класса.
Листинг 16.9. ТестЛакомки.jауа
import junit.framework.*;
import ПосещениеКафе;
import java.util.Date;
public class ТестЛакомки extends TestCase
{
private double булочки - 7.0; // 7 булочек
private double стоимость = 12.5 * 7;
// цена 1 булочки = 12.5 p.
private double вес = 60.0; // взвешивание лакомки
private double дельта = 0.0001; // точность
public ТестЛакомки(String name)
{
super(name);
}
public void тестСоздатьПосещениеКафе()
{
Date дата = new Date();
ПосещениеКафе v = new ПосещениеКафе(дата. булочки.
стоимость, вес);
assertEquals(date, v.получитьДату());
assertEquals(12.5 * 7. v.получитьСтоимость(). дельта);
assertEquals(7.0. v.получитьБулочки(). дельта);
assertEquals(60.0. v.получитьВес(), дельта);
assertEquals(12.5. v.получитьЦену(). дельта):
}
public void тестСоздатьЛакомку()
{
Лакомка g = new Лакомка ();
assertEquals(0. g.получитьЧислоПосещений());
}
public void тестДобааитьПосещение()
{
Лакомка g = new Лакомка();
g.добавитьПосещениеКафе(булочки. стоимость, вес);
assertEquals(1. g.получитьЧислоПосещениРК));
}
}
Еще раз подчеркнем: наличие тестов позволяет определить, что этот рефакторинг ничего не разрушил в программе. Мы будем убеждаться в этом преимуществе постоянно, после очередного применения рефакторинга для реструктуризации программы. Каждый раз после внесения в код изменений запускаются тесты и проверяется работоспособность программы.
Очередная задача — после добавления к Лакомке объектов ПосещениеКафе у Лакомки можно запрашивать генерацию отчетов. Сначала напишем тесты, начнем с простейшего теста.
Листинг 16.10. TecтЛакомки.java
public void тестОтчетаОдногоПосещения()
{
Лакоика g = new Лакоика();
g.добавитьПосещениеКафе(булочки. стоимость, вес);
Отчет r = g.создатьОтчет();
assertEquals(0. r.получитьИзменениеВеса(), дельта);
assertEqualз(булочки, г.получитьПотреблениеБулочек(),
дельта);
assertEquals(0, r.получитьВесНаБулочку(), дельта);
assertEquals(стоимость. r.получитьСтоимостьБулочек(),
дельта);
}
При создании этого тестового варианта мы обдумали детали генерации отчета. Во-первых, Лакомка должна обладать методом создатьОтчет. Во-вторых, этот метод должен возвращать объект класса с именем Отчет. В-третьих, Отчет должен иметь несколько методов-селекторов.
Значения, возвращаемые методами-селекторами, следует проанализировать. Для вычисления изменения веса (или приращения веса на одну булочку) одного посещения кафе недостаточно. Чтобы вычислить эти значения, необходимы, как минимум, два посещения, С другой стороны, одного визита достаточно, чтобы сосчитать потребление и стоимость булочек.
Разумеется, тестовый вариант не компилируется. Поэтому необходимо добавить соответствующие методы и классы. Сначала добавим код, обеспечивающий компиляцию, но не обеспечивающий выполнение тестов.
Листинг 16.11. Лакомка.java, TecтЛакомки.java и Отчет.jаvа
Лакомка.java
public Отчет создатьОтчет()
{
return new Отчет();
}
ТестЛакомки.java
public void тестОтчетаОдногоПосещения()
{
Лакомка g = new Лакомка();
g.добавитьПосещениеКафе(булочки, стоимость, вес);
Отчет r = g.создатьОтчет();
assertEquals(0, r.получитьИзменениеВеса(), дельта);
assertEquals(булочки. r.получитьПотреблениеБулочек(),
дельта);
assertEquals(0. r.получитьВесНаБулочку(), дельта);
assertEquals(cтоимость,. r.получитьСтоимостьБулочек(),.
дельта);
}
Отчет.java
public class Отчет
{
public double получитьИзменениеВеса()
{return егоИзменениеВеса;}
public double получитьВесНаБулочку()
{return егоВесНаБулочку;}
public double получитьСтоииостьБулочек()
{return егоСтоимостьБулочек;}
public double получитьЛотреблениеБулочек()
{return егоПотреблениеБулочек;}
private double егоИзменениеВеса;
private double егоВесНаБулочку;
private double егоСтоимостьБулочек;
private double егоПотреблениеБулочек;
}
Код в листинге 16.11 компилируется и запускается, но его недостаточно для того, чтобы прошли тесты. Нужен рефакторинг кода. Для начала сделаем минимально возможные изменения.
Листинг 16.12. Лакомка.java и Отчет.java
Лакомка.java
public Отчет создатьОтчет()
{
Отчет r = new Отчет();
ПосещениеКафе v = (ПосещениеКафе) егоПосещения. Get(0);
// занести в v первый элемент из контейнера посещений
r.устВесНаБулочку(0);
r.устИзменениеВеса(0);
r.устСтоимостьБулочек(v.получитьСтоимость());
r.устПотреблениеБулочек(v.получитьБулочки()):
return r;
}
Отчет.java
public void устВесНаБулочку (double wpb)
{егоВесНаБулочку = wpb;}
public void устИзменениеВес(double kg)
{егоИзменениеВеса = kg;}
public void устСтоимостьБулочек(double ct)
(егоСтоимостьБулочек = ct;}
public void устПотреблениеБулочек (double b)
{егоПотреблениеБулочек = b;}
Предполагаем, что Лакомке разрешено только одно посещение. В этой версии метода создатьОтчет устанавливаются и возвращаются значения свойств Отчета.
Такой способ разработки метода создатьОтчет может показаться странным, ведь его реализация не завершена. Однако преимущество по-прежнему в том, что между каждой компиляцией и тестированием вносятся только контролируемые добавления.
Если что- то отказывает, можно просто вернуться к предыдущей версии и начать сначала, необходимость в сложной отладке отсутствует.
Для завершения кода продумаем тесты для Лакомки без посещений и с несколькими посещениями кафе. Начнем с теста и кода для варианта без посещений.
Листинг 16.13. TecтЛакомки.java и Лакомка.jауа
ТестЛакомки.java
public void тестОтчетаБезПосещений()
{
Лакомка g = new Лакомка();
Отчет r= g.создатьОтчет();
assertEquals(0, r.получитьИзменениеВеса(). дельта);
assertEquals(0, r.получитьПотреблениеБулочек(), дельта);
assertEquals(0, r.получитьВесНаБулочку()), дельта;
assertEquals(0, r.получитьСтоимостьБулочек(), дельта);
}
Лакомка.Java
public Отчет создатьОтчет()
{
Отчет r = new Отчет();
if (егоПосещения.size() = 0)
{
r.устВесНаБулочку(0);
r.устИзиенениеВеса(0);
r.устСтоимостьБулочек(0);
r.устПотреблениеБулочек(0);
}
else
{
ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(0);
// занести в v первый элемент из контейнера посещений
r.устВесНаБулочку(0);
r.устИзменениеВеса(0);
r.устСтоимостьБулочек(v.получитьСтоимость());
r. устПотреблениеБулочек (v.получитьБулочки ()):
}
return r;
}
Теперь начнем создавать тестовый вариант для нескольких посещений.
Листинг 16.14. ТестЛакомки.jауа
public void тестОтчетаНесколькихПосещений()
{
Лакомка g = new Лакомка();
g.добавить(ПосещениеКафе(7. 87.5, 60.7);
g.добавитьПосещениеКафе(14. 175, 62.1);
g.добавитьПосещениеКафе(28, 350. 64.9);
Отчет r= g.создатьОтчет();
assertEquals(4.2, r.получитьИзменениеВеса(), дельта);
assertEquals(49. r.получитьПотреблениеБулочек(), дельта);
assertEquals(0.086, r.получитьВесНаБулочку(), дельта);
assertEquals(612.5, r.получитьСтоииостьБулочек(), дельта);
}
Мы установили число посещений для Лакомки равным трем. Предполагается, что цена булочки составляет 12,5 руб., а изменение веса — 0,1 кг на одну булочку. Таким образом, за 175 руб. лакомка покупает и съедает 14 булочек, полнея на 1,4 кг.
Но здесь какая-то ошибка. Скорость изменения веса должна определяться коэффициентом 0,1 кг на одну булочку.
А если разделить 4,2 (изменение веса) на 49 (количество булочек), то получаем коэффициент 0,086. В чем причина несоответствия?
После размышлений становится понятно, что вес лакомки регистрируется на выходе из кафе. Поэтому приращение веса и потребление булочек во время первого посещения не учитывается. Изменим исходные данные теста.
Листинг 16.15. ТестЛакомки.jауа
public void тестОтчетаНесколькихПосещений()
{
Лакомка g = new Лакомка();
g.добавитьПосещениеКафе(7. 87.5. 60.7);
g.добавитьПосещениеКафе(14. 175. 62.1);
g.добавитьПосещениеКафе(28. 350. 64.9);
Отчет r - g.создатьОтчет();
assertEquals(4.2, r.получитьИзменениеВеса(), дельта);
assertEquals(42, r.получитьПотреблениеБулочек(), дельта);
assertEquals(0.1, r.получитьВесНаБулочку(), дельта);
assertEquals(612.5, r.получитьСтоимостьБулочек(), дельта);
}
Этот тест корректен. Никогда не известно, с чем встретишься при написании тестов. Можно быть уверенным лишь в том, что, определяя понятия дважды (при написании тестов и кода), вы найдете больше ошибок, чем при простом написании кода.
Теперь добавим код, обеспечивающий прохождение теста из листинга 16.15.
Листинг 16.16. Лакомка.java
public Отчет создатьОтчет()
{
Отчет r = new Отчет ();
if (егоПосещения.size() = 0)
{
r.устВесНаБулочку(0);
r.устИзменениеВеса(0);
r.устСтоимостьБулочек(0);
r.устПотреблениеБулочек(0);
}
else if (егоПосещения.size() = 1)
{
ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(0);
// занести в v первый элемент из контейнера посещений
r.устВесНаБулочку(0);
r.устИзменениеВеса(0);
r.устСтоимостьБулочек(v.получитьСтоимость());
r.устПотреблениеБулочек(v.получитьБулочки());
}
else
{
double первыйЗамер = 0;
double последнийЗамер = 0;
double общаяСтоиность = 0;
double потреблениеБулочек = 0;
for (int i = 0; i < егоПосещения.size(); i++)
// проход по всем элементам контейнера посещений
{
ПосещениеКафе v = (ПосещениеКафе)
егоПосещения.get(i);
// занести в v 1-й элемент из контейнера посещений
if (i = = 0)
{
первыйЗамер = v.получитьВес();
// занести в первыйЗамер вес при 1-м посещении
потреблениеБулочек -= v.получитьБулочки();
}
if (i= = егоПосещения.size()- 1) последнийЗамер =
v.получитьВес();
// занести в последнийЗамер вес при послед, посещении
общаяСтоимость += v.получитьСтоиность();
потреблениеБулочек += v.получитьБулочки();
}
double изменение = последнийЗамер - первыйЗамер;
r.устВесНаБулочкуСизменение/потреблениеБулочек);
r.устИзненениеВеса(иэненение);
r.устСтоиностьБулочек(общаяСтоиность):
r.устПотреблениеБулочек(потреблениеБулочек);
}
return r;
}
Данный код из-за множества специальных случаев выглядит неуклюже. Для устранения специальных случаев нужно провести рефакторинг. Поскольку третий специальный случай наиболее универсален, надо убрать первые два случая.
Когда мы это сделаем, запуск тестового варианта тестОтчетаОдногоПосещения завершится неудачей. Причина в том, что обработка одного посещения включала булочки, купленные только во время первого посещения. А как мы уже обнаружили, если число посещений равно единице, то потребление булочек должно быть равно нулю. Поэтому исправим тестовый вариант и код.
Листинг 16.17. Лакомка.java
public Отчет создатьОтчет()
{
Отчет r = new Отчет();
double первыйЗамер = 0;
double последнийЗамер = 0;
double общаяСтоимость = 0;
double потреблениеБулочек = 0;
for (int i= 0; i< егоПосещения.size(); i++)
// проход по всем элементам контейнера посещений
{
ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(i);
// занести в v i-й элемент из контейнера посещений
if (i = = 0)
{
первыйЗамер = v. ПолучитьВес();
//занести в первыйЗамер вес при 1-м посещении
потреблениеБулочек -= v.получитьБулочки();
}
if (i= = егоПосещения.size()- 1) последнийЗамер =
v. получитьВес();
// занести в последнийЗамер вес при послед. посещении
общаяСтоимость += v.получитьСтоимость();
потреблениеБулочек += v.получитьБулочки();
}
double изменение = последнийЗамер – первыйЗамер;
r.устВесНаБулочку(изменение/потреблениеБулочек);
r.устИзменениеВеса(изменение);
r.устСтоимостьБулочек(общаяСтоимость);
r.устПотреблениеБулочекСпотреблениеБулочек);
return r;
}
Теперь попытаемся сделать функцию короче и понятнее. Переместим фрагменты кода так, чтобы их можно было вынести в отдельные функции.
Листинг 16.18. Лакомка.java
public Отчет создатьОтчет()
{
Отчет г = new Отчет();
double изменение = 0;
double общаяСтоимость = 0;
double потреблениеБулочек =0;
double первыеБулочки = 0; double wpb =0;
if (егоПосещения.size() > 0)
{
ПосещениеКафе первоеПосещение =
(ПосещениеКафе) егоПосещения.get(0);
ПосещениеКафе последнееПосещение = (ПосещениеКафе)
егоПосещения.get(егоПосещения.size() - 1);
double первыйЗамер = первоеПосещение.получитьВес();
double последнийЗамер =
последнееПосещение. ПолучитьВес();
изменение = последнийЗамер – первыйЗамер;
первыеБулочки = первоеПосещение.получитьБулочки();
for (int i = 0; i < егоПосещения.size(); i++)
{
ПосещениеКафе v = (ПосещениеКафе)
егоПосещения.get(i);
общаяСтоимость += v.получитьСтоимость();
потреблениеБулочек += v.получитьБулочки();
}
потреблениеБулочек -= первыеБулочки;
if (потреблениеБулочек > 0)
wpb = изменение / потреблениеБулочек;
}
r.устВесНаБулочку(wpb );
r.устИзменениеВеса(изненение);
r.устСтоимостьБулочек(общаяСтоиность);
r.устПотреблениеБулочек(потреблениеБулочек);
return r;
}
Листинг 16.18 иллюстрирует промежуточный шаг в перемещении фрагментов кода. На пути к нему мы выполнили несколько более мелких шагов. Каждый из этих шагов тестировался. И вот теперь тесты стали завершаться успешно. Облегченно вздохнув, мы увидели, как можно улучшить код. Начнем с разбиения единственного цикла на два цикла.
Листинг 16.19. Лакомка.java
if (егоПосещения.size() > 0)
{
ПосещениеКафе первоеПосещение =
(ПосещениеКафе) егоПосещения.get(0);
ПосещениеКафе последнееПосещение = (ПосещениеКафе)
егоПосещения.get(егоПосещения.size() - 1):
double первыйЗамер = первоеПосещение.получитьВес();
double последнийЗамер = последнееПосещение.получитьВес();
изменение = последнийЗамер – первыйЗамер;
первыеБулочки = первоеПосещение.получитьБулочки();
for (int i =0; i < егоПосещения.size(); i++)
{
ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(i);
потреблениеБулочек += v.получитьБулочки();
}
for (int i =0; i < егоПосещения.size(); i++)
{
ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(i);
общаяСтоимость += v.получитьСтоимость();
}
потреблениеБулочек -= первыеБулочки;
if (потреблениеБулочек > 0)
wpb = изменение / потреблениеБулочек;
}
Выполним тестирование. На следующем шаге поместим каждый цикл в отдельный приватный метод.
Листинг 16.20. Лакомка.java
public Отчет создатьОтчет()
{
Отчет г = new Отчет();
double изменение = 0;
double общаяСтоимость = 0;
double потреблениеБулочек = 0;
double первыеБулочки =0;
double wpb = 0;
if (егоПосещения. Size() > О)
{
ПосещениеКафе первоеПосещение =
(ПосещениеКафе) егоПосещения.get(0);
ПосещениеКафе последнееЛосещение = (ПосещениеКафе)
егоПосещения.get(егоПосещения.size() - 1);
double первыйЗамер = первоеПосещение.получитьВес();
double последнийЗамер =
последнееПосещение.получитьВес();
изменение - последнийЗамер – первыйЗамер;
первыеБулочки = первоеПосещение.получитьБулочки();
потреблениеБулочек = вычПотреблениеБулочек();
общаяСтоимость = вычОбщуюСтоимость();
потреблениеБулочек -= первыеБулочки;
if (потреблениеБулочек > 0)
wpb = изменение / потреблениеБулочек;
}
r.устВесНаБулочку(wpb);
r.устИзменениеВеса(изменение);
r.устСтоимостьБулочек(общаяСтоимость);
r.устПотреблениеБулочек(потреблениеБулочек);
return r;
}
private double вычОбщуюСтоимость()
{
double общаяСтоииость = 0;
for (int i = 0; i < егоПосещения.size(); i++);
{
ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(i);
общаяСтоимость += v.получитьСтоимость();
}
return общаяСтоимость;
}
private double вычПотреблениеБулочек()
{
double потреблениеБулочек = 0;
for (int i - 0; i < егоПосещения.size(); i++)
{
ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(i);
потреблениеБулочек += v.получитьБулочки();
}
return потреблениеБулочек;
}
После соответствующего тестирования перенесем обработку вариантов потребления булочек в метод вычПотреблениеБулочек.
Листинг 16.21. Лакомка.java
public Отчет создатьОтчет()
{
…
if (егоПосещения.size() > 0)
{
ПосещениеКафе первоеПосещение =
(ПосещениеКафе) егоПосещения.get(0);
ПосещениеКафе последнееПосещение - (ПосещениеКафе)
егоПосещения.get(егоПосещения.size() - 1);
double первыйЗамер = первоеПосещение.получитьВес();
double последнийЗамер =
последнееПосещение.получитьВес();
изменение = последнийЗамер - первыйЗамер;
потреблениеБулочек = вычПотреблениеБулочек();
общаяСтоимость - вычОбщуюС тонкость ();
if (потреблениеБулочек > 0)
wpb = изменение / потреблениеБулочек;
}
…
return r;
}
private double вычОбщуюСтоимость()
{
double общаяСтоимость = 0;
for (int i= 0; i < егоПосещения.size(); i++);
{
ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(i);
общаяСтоимость += v.получитьСтоимость();
}
return общаяСтоимость;
}
private double вычПотреблениеБулочек()
{
double потреблениеБулочек = 0;
if (егоПосещения.size() > 0)
{
for (int i = 1; i < егоПосещения.size(); i++)
{
ПосещениеКафе v = (ПосещениеКафе)
егоПосещения.get(i);
потреблениеБулочек += v.получитьБулочки();
}
}
return потреблениеБулочек;
}
Заметим, что функция вычПотреблениеБулочек теперь суммирует потребление булочек, начиная со второго посещения. И опять выполняем тестирование. На следующем шаге выделим функцию для расчета изменения веса.
Листинг 16.22. Лакомка.java
public Отчет создатьОтчет()
{
Отчет r = new Отчет ();
double изменение = 0;
double общаяСтоимость = 0;
double потреблениеБулочек = 0;
double первыеБулочки = 0;
double wpb = 0;
if (егоПосещения.size() > 0)
{
изменение = вычИзменение();
потреблениеБулочек = вычПотреблениеБулочек();
общаяСтоимость = вычОбщуюСтоимость();
if (потреблениеБулочек > 0)
wpb = изменение / потреблениеБулочек;
}
r.устВесНаБулочку(wpb);
r.устИзменениеВеса(изменение);
r.устСтоимостьБулочек(общаяСтоимость);
r.устПотреблениеБулочек(потреблениеБулочек):
return r;
}
private double вычИзменение()
{
double изменение = 0;
if (егоПосещения.size() > 0)
{
ПосещениеКафе первоеПосещение =
(ПосещениеКафе) егоПосещения.get(0);
ПосещениеКафе последнееПосещение = (ПосещениеКафе)
егоПосещения.get;(егоПосещения.sizе() - 1);
double первыйЗамер = первоеПосещение.получитьВес();
double последнийЗамер =
последнееПосещение. получитьВес();
изменение = последнийЗамер - первыйЗамер;
}
return изменение;
}
После очередного запуска тестов переместим условия в главном методе создатьОтчет и подчистим лишние места.
Листинг 16.23. Лакомка.java
public Отчет создатьОтчет()
{
double изменение = вычИзменение();
double потреблениеБулочек = вычПотреблениеБулочек();
double общаяСтоимость = вычОбщуюСтоимость();
double wpb = 0;
if (потреблениеБулочек > 0)
wpb = изменение / потреблениеБулочек;
Отчет г = new Отчет ();
r.устВесНаБулочку(wpb);
r.устИзменениеВеса(изменение);
r.устСтоимостьБулочек(общаяСтоимость);
r.устПотреблениеБулочек(потреблениеБулочек);
return r;
}
private double вычИзменение()
{
double изменение = 0;
if (eroПосещения.size() > 1)
{
ПосещениеКафе первоеПосещение =
(ПосещениеКафе) егоПосещения.get(0);
ПосещениеКафе последнееПосещение = (ПосещениеКафе)
егоПосещения.get(егоПосещения.size() - 1);
double первыйЗамер = первоеПосещение.получитьВес(0;
double последнийЗамер =
последнееПосещение. получитьВес();
изменение = последнийЗамер – первыйЗамер;
}
return изменение;
}
private double вычОбщуюСтоимость()
{
double общаяСтоимость =0;
for (int i= 0; i < егоПосещения.size(); i++);
{
ПосещениеКафе v = (ПосещениеКафе) егоПосещения.get(i);
общаяСтоимость += v.получитьСтоимость();
}
return общаяСтоимость;
}
private double вычПотреблениеБулочек()
{
double потреблениеБулочек = 0;
if (егоПосещения.size() > 1)
{
for (int i = 1; i < егоПосещения.size(); i++)
{
ПосещениеКафе v = (ПосещениеКафе)
егоПосещения.get(i);
потреблениеБулочек += v.получитьБулочки();
}
}
return потреблениеБулочек;
}
После окончательного прогона тестов констатируем, что цель достигнута — код стал компактным и понятным, обязанности разнесены по отдельным функциям.
Таким образом, в рассмотренном подходе программа считается завершенной не тогда, когда она заработала, а когда она стала максимально простой и ясной.