TDD 学习笔记(三)

接上一篇。这里到了比较关键的时候了。
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空了,我们还可以添加,继续重构代码。

你可能感兴趣的:(学习笔记)