接上一篇。这里到了比较关键的时候了。
Kent跨大步了,但是如何找到合适的步伐,还是需要不断从小步尝试。
现在看看目前的to-do lists吧。
- 5美元 + 5美元 = 10 美元
- 5美元 + 10法郎 = 10 美元 假设美元对法兰的汇率是 1:2
这里要注意的是两件事,首先,加法的引入,然后,汇率实现。
我们需要快速在脑子里浮现出一个场景。我们去银行兑换货币,银行提供汇率……
这里,先实现一个简单的加法测试
@Test public void testSimpleAddition(){
Money sum = Money.dollar(5).plus(Money.dollar(5));
assertEquals(Money.dollar(10),sum);
}
很简单,现在来实现已经是轻车熟路了吧。
Money
public Money plus(Money addend){
new Money(amount + addend.amount,currency);
}
这里算是一眼就看出来的,所以没必要返回一个 Money.dollar(10)了。测试,OK。
接下来要考虑的,就是如何比较不同的货币,也就是货币转换。首先要有银行,然后呢不同的货币如何表示在一个类中?这里需要在抽象一层。
作者在书中使用了表达式, 5美元+10法郎 这就是一个表达式。我们通过银行,来把这个表达式兑换成美元,或者法郎(根据汇率)。
@Test public void testSimpleAddition(){
Moeny five = Money.dollar(5);
Expression sum = five.plus(five);
Bank bank = new Bank();
Moeny result = bank.reduce(sum,"USD");
assertEquals(Money.dollar(10),sum);
}
这就是我们脑子中浮现的一个样子。好了,接下来就一个一个实现。
首先修改plus方法。
Money 类
public Expression plus(Money addend){
new Money(amount+addend.amount,currency);
}
Expression 接口
public interface Expression{
}
Bank 类
public class Bank{
public Money reduce(Expression expression,String to){
return new Money.dollar(10);//just fake implementation
}
}
测试一下,OK。没什么问题。但是还有fake implementation。我们仔细分析下,发现其实Expression接口与Money还是有区别的,所以,在plus的时候,应该返回一个真正的实现了Expression 的类。这里就叫做Sum吧。这样,Sum应该又有一个加数和一个被加数。
测试上:
@Test public void testSum(){
Moeny five = Money.dollar(5);
Expression result = five.plus(five);
Sum sum = (Sum)result;
assertEquals(sum.augend,five);
assertEquals(sum.addend,five);
}
Sum
public class Sum implements Expression{
private Money augend;
private Money addend;
public Sum(Money augend, Money addend){
this.augend = augend;
this.addend = addend;
}
}
Money 类中的plus可以返回一个真正的Expression类了。
public Expression plus(Money addend){
return new Sum(this,addend);
}
这是,我们的问题终于回到这个fake implementation,让我们再写个测试,来暴露问题。
@Test public void testReduceSum(){
Expression sum = Money.dollar(3).plus(Money.dollar(4));
Bank bank = new Bank();
Moeny result = bank.reduce(sum,"USD");
assertEquals(Money.dollar(7),sum);
}
public class Bank{
public Money reduce(Expression expression,String to){
Sum sum = (Sum)expression;
return new Money(sum.augend.amount+sum.addend.amount,to);
}
}
这下是通过测试了,但是我们看到了又臭又长的一串串 sum.augend.amount 之类的。
这样的东西出现,预示着我们应该使用Move method重构。于是把reduce方法移到Sum中。
public class Sum implements Expression{
private Money augend;
private Money addend;
public Sum(Money augend, Money addend){
this.augend = augend;
this.addend = addend;
}
public Money reduce(String to){
return new Money(augend.amount + addend.amount,to);
}
}
于是Bank的reduce可以这样写
public class Bank{
public Money reduce(Expression expression,String to){
Sum sum = (Sum)expression;
return sum.reduce(to);
}
}
这下简洁多了。既然多了个函数,我们为此写个测试吧。
@Test public void testReduceMoney(){
Money money = Money.dollar(1);
Bank bank = new Bank();
Money m = bank.reduce(money,"USD");
assertEquals(Money.dollar(1),m);
}
这个测试很有意思,既然Money是一种表达式,那么就直接放到bank.reduce里检验。
结果问题出来了。造型错误。我们还得修改bank.reduce
public class Bank{
public Money reduce(Expression expression,String to){
if(expression instanceof Money) return (Money)expression;
Sum sum = (Sum)expression;
return sum.reduce(to);
}
}
这下代码越来越难看了。这完全背离了我们的意图。不过通过这里,我们很快就能发现,Money和Sum如果都有一致的方法,就很好了。于是Expression接口多了reduce方法
public interface Expression{
public Money reduce(String to);
}
Money需要添加此方法。(Money类实现了Expression接口)。
public Money reduce(String to){
return this;
}
现在,Bank的reduce方法终于可以这样写了
public Money reduce(Expression expression, String to){
return expression.reduce(to);
}
清晰很多了。测试通过,接下来是to-do lists的第二项未实现的。那就是;
5美元 + 10法郎 = 10 美元。
在写测试之前,我们一般要考虑一下如何实现,这里,我们的银行应该提供一个汇率。
这样,我们就可以转换不同币种了。
我们可以这样写测试:
@Test public void testRecuceMoneyDifferentCurrency(){
Bank bank = new Bank();
bank.addRate("CHF","USD",2)
Money result = bank.reduce(Money.franc(10),"USD");
assertEquals(Money.dollar(5),result);
}
这是对一个基本的汇率转换的测试。我们暂时还没有一个清晰的方法来实现汇率。于是就使用triangulation吧。先做一个stub实现。具体分析代码,最后实现转换是在Money中的reduce方法。
Money 类
public Money reduce(String to){
int rate = currency.equals("CHF")&&to.equals("USD")?2:1;
return new Money(amount/rate,to);
}
显然这是为了通过测试。
新增一个测试 :
@Test public void testBankRate(){
Bank bank = new Bank();
bank.addRate("USD","CHF",2); // reverse the rate
Money result = bank.reduce(Money.dollar(10),"CHF");
assertEquals(Money.franc(5),result);
}
显然,我们的银行没有起到作用。我们可以应该修改接口Expression中的reduce方法
public interface Expression{
public Money reduce(Bank bank, String to);
}
于是接着改Money
public Money reduce(Bank bank, String to){
int rate = (currency.equals("CHF"))&&(to.equals("USD"))?2:1;
return new Money(amount/rate,to);
}
当然Sum的也要改。这就省略。
rate应该由Bank提供。
所以Extract Method到Bank类。
Bank
public int rate(){
return (currency.equals("CHF"))&&(to.equals("USD"))?2:1;
}
Money
public Money reduce(Bank bank, String to){
int rate = bank.rate();
return new Money(amount/rate, to);
}
测试,还是不能通过。因为那个判断其实是个fake implementation,所以需要真正考虑如何实现了。这里,脑海里第一浮现的就是Collection类。作者使用Hashtable来存放一个key/value的键值对。这样每个转换都对应一个汇率。
修改Bank类的rate方法,添加参数。
Bank类。
private Hashtable rates;
public void addRate(String from ,String to, int rate){
rates.put(new Pair(from,to),new Ineger(rate));
}
public int rate(String from ,String to){
Pair key = new Pair(from,to);
return ((Integer)rates.get(key)).intValue();
}
这里有引入一个新类,作为key的封装。我们有需要测试了。
@Test public void testkeyEquality(){
assertEquals(new Pair("USD","CHF"),new Pair("USD","CHF"));
assertFalse((new Pair("USD","CHF")).equals(null));
}
如果你还记得前面的货币比较的话,这里应该容易理解,我们马上知道该如何比较Pair
public class Pair{
private String from;
private String to;
public Pair(Stirng from, String to){
this.from = from;
this.to = to;
}
@Override
public boolean equals(Object obj){
if(obj == null) return false;
if(obj == this) return true;
if(obj instanceof Pair){
Pair newpair = (Pair)obj;
return (from.equals(obj.from) && (to.equals(obj.to));
}else return false;
}
@Override
public int hashCode(){
return 0;
}
这里的euqals方法其实做过了。因为我们的测试没有放映到我需要这么做。
对于hashCode方法呢,暂时返回0,使之退化成线性查找,我们这里但求通过测试。
经过这番折腾,测试终于通过了。我们这里又想到一个测试:
@Test public void testIdentityRate(){
assertEquals(1,(new Bank()).rate("USD","USD"));
}
这类测试属于边界测试吧。往往起到意想不到的作用。果然 ,我们又一次倒下了。
对于这类一般性问题,我们在rate里面没有考虑全面。
public int rate(String from ,String to){
if(from.equals(to)) return 1;
Pair key = new Pair(from,to);
return ((Integer)rates.get(key)).intValue();
}
加上这个,就算完成rate的测试了吧。因为我们马上就要实现 5美元+10法郎=10美元的测试了。
@Test public void testMixedAddition(){
Money five = Money.dollar(5);
Expression sum = five.plus(Money.franc(10));
Bank bank = new Bank();
bank.addRate("CHF","USD",2);
Money result = bank.reduce(sum,"USD");
assertEquals(Money.dollar(10),result);
}
这个测试失败了。我们应该得到10美元,但是,我们得到的是15美元。问题出在Sum类。
public Money reduce(String to){
return new Money(augend.amount + addend.amount,to);
}
一下就明白了,不同的表达式需要经过reduce,才能相加。
public Money reduce(Bank bank,String to){
return new Money(augend.reduce(bank,to)+ addend.reduce(bank,to),to);
}
好了,现在工作了。但是我们看到,Sum中的加数与被加数同时也应该是表达式。于是
可以再测试通过的情况下,重构,把Sum变为:
public class Sum implements Expression{
private Expression augend;
private Expression addend;
public Sum(Expression augend, Expression addend){
this.augend = augend;
this.addend = addend;
}
public Money reduce(Bank bank,String to){
int amount = augend.reduce(bank,to).amount +
addend.reduce(bank,to).amount;
return new Money(amount,to);
}
}
既然都是面向接口了,注意Money中的plus也要改改。times也该改改
public Expression plus(Expression addend){
return new Sum(this,addend);
}
public Expression times(int multiplier){
return new Money(amount*multiplier,currency);
}
然后相应的测试代码也改改:
@Test public void testMixedAddition(){
Expression five = Money.dollar(5);
Expression sum = five.plus(Money.franc(10));
Bank bank = new Bank();
bank.addRate("CHF","USD",2);
Money result = bank.reduce(sum,"USD");
assertEquals(Money.dollar(10),result);
}
改完以后,测试发现,Expression接口么有plus方法,这就很明显了,Expression应该有plus和times接口。加上。
Money的实现未变。
Sum的plus实现呢?暂且未知。
public Expression plus(Expression addend){
return null;
}
于是测试通过了。我们需要一个新的测试Sum的plus,来发现我们的伪实现。
@Test public void testSumPlus(){
Expression five = Money.dollar(5);
Expression sum = five.plus(Money.franc(10));
Bank bank = new Bank();
bank.addRate("CHF","USD",2);
Money result = bank.reduce(sum.plus(Money.franc(20),"USD");
assertEquals(Money.dollar(20),result);
}
现在实现Sum的plus方法,其实很简单。
public Expression plus(Expression expression){
return new Sum(this,expression);
}
测试通过。就这么简单。现在我们可能会在to-do lists 中加入,
(5美元+10法郎)*3 = 30 美元
测试代码:
@Test public void testSumPlus(){
Expression fiveBuck = Money.dollar(5);
Expression tenFranc= Money.franc(10);
Bank bank = new Bank();
bank.addRate("CHF","USD",2);
Money result = bank.reduce(new Sum(fiveBuck,tenFranc).times(2),"USD");
assertEquals(Money.dollar(20),result);
}
我们的times方法,同样需要对Expression方法有效。
Sum中的实现
public Expression times(int multiplier){
return new Sum(augend.times(multiplier),addend.times(multiplier));
}
Ok ,测试通过。写到这里,基本就告一个段落了,但是这里远远没有完。
只是我们的to-do lists空了,我们还可以添加,继续重构代码。