430 likes | 619 Views
Тема 3. Программирование с помощью тестирования на примере. XP. eXtreme Programming. Задача – разработка класса для работы с различными валютами. Пусть есть некоторая информационная система для банков.
E N D
Тема 3 Программирование с помощью тестирования на примере XP eXtreme Programming
Задача – разработка класса для работы с различными валютами Пусть есть некоторая информационная система для банков. В качестве основной валюты для расчетов используется доллар, для работы с которым был реализован класс Dollar. В условиях кризиса требовалось перевести систему на мультивалютные расчеты. XP
Список задач для тестированияРешение задачи 2 $5+10 CHF =$10, если курс обмена 2:1 $5*2=$10 public void testMultiplication() { Dollar five=new Dollar(5); five.times(2); assertEquals(10.,five.amount); } public class Dollar { double amount; Dollar (double amount) {} void times(intmult) {} } XP
Варианты исправления • При объявлении переменной amount: double amount=5*2; • В методе times: void times(int mult){ amount=5*2;} • Изменения должны быть и в конструкторе, и в методе times: Dollar (double amount){this.amount=amount;} void times(int mult) {amount*=mult;} XP
Список задач $5+10 CHF =$10, если курс обмена 2:1 $5*2=$10 amount-закрытая переменная Побочные эффекты в классе Dollar – изменение объекта Округление денежных величин Тест, который не выполняется: public void testMultiplication() { Dollar five=new Dollar(5); five.times(2); assertEquals(10.,five.amount); five.times(3); assertEquals(15.,five.amount); } XP
Решение проблемы Изменение метода times: Dollar times(intmult) { return new Dollar(amount*mult); } Изменение теста: public void testMultiplication() { Dollar five=new Dollar(5); Dollar product=five.times(2);assertEquals(10.,product.amount); product=five.times(3); assertEquals(15.,product.amount); } XP
Равенство $5+10 CHF =$10, если курс обмена 2:1 $5*2=$10 amount-закрытая переменная Побочные эффекты в классе Dollar – изменение объекта Округление денежных величин equals() Тест на равенство: public void testEquality() { assertTrue(new Dollar(5).equals(new Dollar(5))); } Тест не работает – нужна заглушка метода equals. public boolean equals(Object obj){return true;} XP
Равенство Не работает другой тест: public void testEquality() { assertTrue(new Dollar(5).equals(new Dollar(5))); assertFalse(new Dollar(5).equals(new Dollar(6))); } Требуется изменить метод equals: public boolean equals(Object obj) { Dollar dollar=(Dollar)obj; return amount==dollar.amount; } XP
Список задач $5+10 CHF =$10, если курс обмена 2:1 $5*2=$10 amount-закрытая переменная Побочные эффекты в классе Dollar – изменение объекта Округление денежных величин equals() Равенство значению null Равенство объектов XP
Закрытые переменные Меняем тест – сравниваем на равенство объекты: public void testMultiplication() { Dollar five=new Dollar(5); assertEquals(new Dollar(10),five.times(2)); assertEquals(new Dollar(15),five.times(3)); } XP
Список задач $5+10 CHF =$10, если курс обмена 2:1 $5*2=$10 amount-закрытая переменная Побочные эффекты в классе Dollar – изменение объекта Округление денежных величин equals() Равенство значению null Равенство объектов 5 CHF*2=10 CHF XP
Добавляем класс для швейцарского франка • Добавляем новый класс Franc (код из класса Dollar); • Пишем тест: public void testFrancMultiplication() { Franc five=new Franc(5); assertEquals(new Franc(10),five.times(2)); assertEquals(new Franc(15),five.times(3)); } XP
Список задач $5+10 CHF =$10, если курс обмена 2:1 $5*2=$10 amount-закрытая переменная Побочные эффекты в классе Dollar – изменение объекта Округление денежных величин equals() Равенство значению null Равенство объектов 5 CHF*2=10 CHF Дублирование Dollar/Franc Общие операции equals() Общие операции times() XP
Решение проблемы – создание иерархии Создадим класс Money, от которого наследуют Dollar и Franc Перенесем в него переменную amount. Для выполнения всех тестов требуется сделать ее protected. XP
Снова проверка на равенство Меняем в классе Dollar метод equals – перенос на тип данных Money public boolean equals(Object obj) { Money money=(Money)obj; return amount==money.amount; } Переносим метод в класс Money и удаляем из классов Dollar и Franc. Тест: public void testEquality() { assertTrue(new Dollar(5).equals(new Dollar(5))); assertFalse(new Dollar(5).equals(new Dollar(6))); assertTrue(new Franc(5).equals(new Franc(5))); assertFalse(new Franc(5).equals(new Franc(6))); } XP
Список задач $5+10 CHF =$10, если курс обмена 2:1 $5*2=$10 amount-закрытая переменная Побочные эффекты в классе Dollar – изменение объекта Округление денежных величин equals() Равенство значению null Равенство объектов 5 CHF*2=10 CHF Дублирование Dollar/Franc Общие операции equals() Общие операции times() Сравнение долларов и франков XP
Сравнение долларов и франков Тест: public void testEquality() { assertTrue(new Dollar(5).equals(new Dollar(5))); assertFalse(new Dollar(5).equals(new Dollar(6))); assertTrue(new Franc(5).equals(new Franc(5))); assertFalse(new Franc(5).equals(new Franc(6))); assertFalse(new Franc(5).equals(new Dollar(5))); } Изменение метода: public boolean equals(Object obj) { Money money=(Money)obj; return amount==money.amount && getClass().equals(money.getClass()); } XP
Список задач $5+10 CHF =$10, если курс обмена 2:1 $5*2=$10 amount-закрытая переменная Побочные эффекты в классе Dollar – изменение объекта Округление денежных величин equals() Равенство значению null Равенство объектов 5 CHF*2=10 CHF Дублирование Dollar/Franc Общие операции equals() Общие операции times() Сравнение долларов и франков Валюта? XP
Устранение дублирования times требует другого способа создания объектов Методы times могут возвращать Money Money times(intmult) { return new Dollar(amount*mult); } В результате классы почти одинаковые -> хочется сделать только один класс XP
Фабричные методы создания объектов В класс Money добавим фабричный метод создания объекта Dollar: static Dollar dollar(double amount) { return new Dollar(amount);} Меняем принцип создания объекта в тесте: public void testMultiplication() { Money five=Money.dollar(5); assertEquals(Money.dollar(10),five.times(2)); assertEquals(Money.dollar(15),five.times(3)); } Осталась ошибка – метод times не определен в классе Money XP
Фабричные методы создания объектов Делаем класс Money абстрактным, добавляя в него абстрактный метод times. abstract class Money { protected double amount; … abstract Money times(int mult); } Меняем фабричный метод с точки зрения возвращаемого значения static Money dollar(double amount) { return new Dollar(amount); } XP
Фабричные методы создания объектов Делаем те же действия с классом Franc Меняем тесты public void testEquality() { assertTrue(Money.dollar(5).equals(Money.dollar(5))); assertFalse(Money.dollar(5).equals(Money.dollar(6))); assertTrue(Money.franc(5).equals(Money.franc(5))); assertFalse(Money.franc(5).equals(Money.franc(6))); assertFalse(Money.franc(5).equals(Money.dollar(5))); } XP
Список задач $5+10 CHF =$10, если курс обмена 2:1 $5*2=$10 amount-закрытая переменная Побочные эффекты в классе Dollar – изменение объекта Округление денежных величин equals() Равенство значению null Равенство объектов 5 CHF*2=10 CHF Дублирование Dollar/Franc Общие операции equals() Общие операции times() Сравнение долларов и франков Валюта? Нужен ли тест на умножения франков на число? XP
Пытаемся избавиться от производных классов Добавляем метод для получения названия валюты в классы Dollar и Franc и абстрактный метод в класс Money: abstract String currency(); String currency(){ return "USD";} String currency(){ return "CHF";} Тест public void testCurrency() { assertEquals("USD",Money.dollar(1).currency()); assertEquals("CHF",Money.franc(1).currency()); } XP
Пытаемся избавиться от производных классов Вводим символьную переменную для обозначения валюты в класс Money и переносим функцию currency полностью в класс Money abstract class Money { protected double amount; protected String currency; … String currency() { return currency; } } Конструкторы должны инициализировать эту строку, например, Franc (double amount) { this.amount=amount; this.currency="CHF"; } XP
Пытаемся избавиться от производных классов Конструкторы производных классов похожи -> можно инициализировать переменную currency внутри конструктора базового класса: public Money(double amount, String currency) { this.amount=amount; this.currency=currency; } Конструкторы производных классов выглядят, например, так: Franc (double amount) { super(amount,"CHF"); } Можно добавить второй параметр с обозначением валюты и передавать его конструктору базового класса XP
Список задач $5+10 CHF =$10, если курс обмена 2:1 $5*2=$10 amount-закрытая переменная Побочные эффекты в классе Dollar – изменение объекта Округление денежных величин equals() Равенство значению null Равенство объектов 5 CHF*2=10 CHF Дублирование Dollar/Franc Общие операции equals() Общие операции times() Сравнение долларов и франков Валюта? Нужен ли тест на умножения франков на число? XP
Общая операция times Функция times класса Dollar выглядит так: Money times(intmult) { return Money.dollar(amount*mult);} Вернемся к вызову конструктора: Money times(intmult) { return new Dollar(amount*mult,“USD");} Строку с названием валюты можно получить из поля currency-> переносим в класс Money (класс перестает быть абстрактным) Money times(intmult){ return new Money(amount*mult,currency);} Тест не срабатывает – сравнение имен классов (один объект класса Dollar, другой – класса Money). XP
Общая операция times Вернемся к методам times внутри производных классов, например, в Dollar Money times(int mult) { return new Dollar(amount*mult,currency);} Тесты сработали. Добавляем тест для сравнения объектов, созданных с помощью Money и Dollar public void testDifferentClasses() { assertTrue(new Money(10,"CHF").equals(new Franc(10,"CHF"))); } Тест не сработал. XP
Общая операция times Меняем equals – сравнение названий валют, а не имен классов. public boolean equals(Object obj) { Money money=(Money)obj; return amount==money.amount && currency.equals(money.currency); } Тесты сработали. Снова отказываемся от методов times производных классов. Тесты сработали. XP
Список задач $5+10 CHF =$10, если курс обмена 2:1 $5*2=$10 amount-закрытая переменная Побочные эффекты в классе Dollar – изменение объекта Округление денежных величин equals() Равенство значению null Равенство объектов 5 CHF*2=10 CHF Дублирование Dollar/Franc Общие операции equals() Общие операции times() Сравнение долларов и франков Валюта? Нужен ли тест на умножения франков на число? XP
Устраняем дублирование Производные классы имеют только конструкторы. Отказываемся от них. Методы-фабрики должны вызывать конструктор класса Money. static Money franc(double amount) { return new Money(amount,"CHF"); } Тест на равенство объектов разных классов и умножение франков можно удалить. XP
Список задач $5+10 CHF =$10, если курс обмена 2:1 $5*2=$10 amount-закрытая переменная Побочные эффекты в классе Dollar – изменение объекта Округление денежных величин equals() Равенство значению null Равенство объектов 5 CHF*2=10 CHF Дублирование Dollar/Franc Общие операции equals() Общие операции times() Сравнение долларов и франков Валюта? Нужен ли тест на умножения франков на число? $5+$5=$10 XP
Реализуем простое сложение Реализуем тест: public void testSimpleAddition() { Money sum=Money.dollar(5).plus(Money.dollar(5)); assertEquals(sum,Money.dollar(10)); } В класс Money добавляем функцию plus: Money plus(Money added) { return new Money(amount+added.amount,currency); } XP
Используем метафору – банк Обменный курсы должен знать банк -> создаем класс Bank (в нем метод reduce для перевода денег в конкретную валюту). XP
Используем метафору – банк и бумажник Тест: public void testSimpleAddition() { Money five=Money.dollar(5); Money sum=five.plus(five); Bank bank=new Bank(); Money reduced=bank.reduce(sum, "USD"); assertEquals(sum,Money.dollar(10)); } В классе Bank должна быть заглушка: public class Bank { Money reduce(Money source,String to) { return Money.dollar(10); } } Метод plus возвращает Money. XP
Пытаемся определить курсы валют Тесты: public void testReduceMoney() { Bank bank=new Bank(); bank.addRate("CHF","USD",2); Money result=bank.reduce(Money.franc(2), "USD"); assertEquals(Money.dollar(1),result); } public void testHashRates() { Bank bank=new Bank(); bank.addRate("CHF", "USD", 2); assertTrue(bank.rate("CHF", "USD")==2.); } XP
Реализуем хранение курсов валют В классе Bank создаем хэш-таблицу: private class Pair { private String from; private String to; public Pair(String from, String to) { this.from=from; this.to=to; } public boolean equals(Object obj) { Pair p=(Pair)obj; return from.equals(p.from) && to.equals(p.to); } public inthashCode(){ return 0; } } private Hashtable rates=new Hashtable(); XP
Добавление и получение курса void addRate(String from, String to, double rate) { rates.put(new Pair(from,to), new Double(rate)); } double rate(String from, String to) { Double rate=(Double)rates.get(new Pair(from,to)); return rate.doubleValue(); } XP
Проверка определения курса валюты Делаем метод-заглушку класса Bank: Money reduce(Money source,String to) { double rate=1; if (source.currency.equals("CHF") && to.equals("USD")) rate=2.; if (source.currency.equals("USD") && to.equals("CHF")) rate=1./2; return new Money(source.amount/rate,to); } XP
Проверка определения курса валюты Изменяем заглушку: Money reduce(Money source,String to) { double rate=1; Pair p=new Pair(source.currency,to); if (rates.containsKey(p)) rate=rate(source.currency,to); return new Money(source.amount/rate,to); } XP
Что, если курса валют нет? Изменяем тест public void testReduceMoney() { Bank bank=new Bank(); bank.addRate("CHF","USD",2); Money result=bank.reduce(Money.franc(2), "USD"); assertEquals(Money.dollar(1),result); result=bank.reduce(Money.dollar(2), "USD"); assertEquals(result,Money.dollar(2)); result=bank.reduce(Money.dollar(2), "CHF"); assertEquals(result,Money.franc(4)); } Тест не работает, так как если курса нет, то в хэш-таблице ничего не будет найдено. XP
Что, если курса валют нет? Добавлять надо два курса void addRate(String from, String to, double rate) { rates.put(new Pair(from,to), new Double(rate)); rates.put(new Pair(to,from), new Double(1/rate)); } XP