TDD 学习笔记(二)

上一次讲到了私有化amount,并且,重写了equals方法,对于hashCode方法(假设返回0,这样退化到线性查找,但是暂且不影响我们的正确性),我们暂且放下。我们现在还是看看to-do lists


  • 5美元 * 2 = 10 美元
  • 5美元 等于 5美元
  • 5美元不等于6美元
  • 私有化amount
  • 5法郎 * 2= 10 法郎
  • 5法郎 等于 5法郎
  • 5法郎 不等于 5法郎
  • 5美元 不等于 5法郎

上面是引入新的货币,法郎,我们发现其实和美元很相像。于是,我们创建和美元一样的测试,然后把美元的代码copy过来,修改成法郎。
上面的动作,或者就是大家工作中的真实写照了吧。不过不要着急,我们的测试驱动开发正是要解决这种不可容忍的重复代码的。
测试:

@Test public void testTimes(){  
   Dollar five = new Dollar(5); 
   Franc ten = new Franc(10); 
   assertEquals(five.times(2),new Dollar(10));  
   assertEquals(five.times(3),new Dollar(15));  
   assertequals(ten.times(2),new Franc(20));
   assertequals(ten.times(3),new Franc(30));
}

@Test public void testEqualitiy(){
    assertTrue((new Dollar(5)).equals((new Dollar(5))));
    assertFalse((new Dollar(5)).equals((new Dollar(7))));
    assertTrue((new Franc(5)).equals((new Franc(5))));
    assertFalse((new Franc(5)).equals((new Franc(7))));
    assertFalse((new Dollar(5)).equals((new Franc(5))));
} 

注意,测试中,新加入一个测试,那就是 5美元不等于7美元。
下面是新的Franc类,是不是和美元类一个模子。
public class Franc{
private amount;
public Franc(int amount){
this.amount=amount;
}
public Franc times(int multipler){
   return new Franc(amount*multipler);
}
/*
 *I recommand you add @Override if you can't
 *remember the method clearly 
 */
@Override
public boolean equals(Object obj){
 return this.amount==((Franc)obj).amount;
}
}


面对这个问题,不知道你怎么想。但是这里,我们开始OO编程了。OO的目的不就是消除一些不必要的重复吗?既然这两个类那么相同,那么他们是否应该又一个父类呢。
于是我们引入了Money类。
Money类的引入,并不能马上消除重复。这里需要想办法做些改变,只有两个一摸一样的方法 ,我们才可以把他们放到父类中。
这里,显而易见的便是amount。

public class Money{
protected int amount;
}

然后Dollar和Franc都继承Money。我们接下来看到,equals方法很是最相像的。于是我们做些小变化。
对于Dollar类
public boolean equals(Object obj){
 Money money = (Money)obj;
 return amount == money.amount;
}

这里我又多走了几步。首先,把 (Dollar)obj变为 (Money)obj 这一步,运用Liskov替换原则,子类可以转换为父类。当然,声明也可以用父类了。这样,对Franc类的操作一样,我们就可以看到,两个equals方法完全一样,然后上移到Money中。
public class Money{
protected int amount;
public boolean equals(Object obj){
 Money money = (Money)obj;
 return amount == money.amount;
}
}

一切似乎很顺利,我们运行测试。啊,竟然没有通过,5美元竟然等于5法郎了。
原来我们的equals函数还有问题。既然是美元和法郎,就应该有不同,都转换成Money了,那么就没有区别了。这里,我们一眼看不出来应该怎么办,那么就使用triangulation吧。
public class Money{
protected int amount;
public boolean equals(Object obj){
 Money money = (Money)obj;
 return amount == money.amount &&
        this.getClass() == obj.getClass();
}
}

书上说,这种比较很不好,因为不能依赖于java语言中的对象比较,而是要真正弄清法郎和美元到底是什么不同,从业务上说明。但是这就是一个stub吧。我们还要继续测试。
测试通过了。
这时,测试清单上的测试完了。但是我们还需要进行重构,消除重复。
Franc和Dollar中的times方法进入我们的视线。他们太像了。
首先可以一样看出来的,就是,我们可以让他们都返回Money对象。
Dollar 类
public Money times(int multiplier){
      return new Dollar(amount*multiplier);
}


Franc 类
public Money times(int multiplier){
      return new Franc(amount*multiplier);
}


似乎就到这里,我们很难在改动了。难道就这样了?
但是这两个类实在是让我们觉得很不爽,因为他们两个的逻辑太一样了。
这里我也很迷茫,kent大叔说,可以引入一个工厂模式,既然我们不好区分这两个类,那么使用工厂模式来区分,这样,就可以看到他们需要如何区别自己了。

有了这个想法,测试程序可以改动了。
@Test public void testTimes(){  
   Money five = Money.dollar(5); 
   Franc ten = new Franc(10); 
   assertEquals(five.times(2),Money.dollar(10));  
   assertEquals(five.times(3),Money.dollar(15));  
   assertequals(ten.times(2),Money.franc(20));
   assertequals(ten.times(3),Money.franc(30));
}

然后改动Money类。这里我们不知道Money的times方法啊,别急,那就把Money改为abstract类吧。
public abstract class Money{
protected int amount;
public boolean equals(Object obj){
 Money money = (Money)obj;
 return amount == money.amount &&
        this.getClass() == obj.getClass();
}
public static dollar(int amount){
    return new Dollar(amount);
}

public static franc(int amount){
    return new Franc(amount);
}
public abstract Money times(int multipler);
}

测试通过了。这时,我们看Dollar和Franc其实就是两个的名字不一样。这里,我差点忘记了,我们的to-do lists里应该再加上
  • 消除Dollar与Franc类

这样,我们可以引入一个区分货币的东西了。越简洁约好,我们可能使用一个常量,或者一个字符串。作者使用了一个字符串来区分。美元就是”USD”,法郎就是"CHF"。这比较符合常理,我们都说,人民币100,或者美元100,区分就在于前面这个字符串。于是可以写测试了。

@Test public void testCurrency(){
    assertEquals("USD",Money.dollar(1).currency());
    assertequals("CHF",Money.franc(1).currency());
}


为此,我们可以看出,只要添加一个成员函数,currency()即可。
Money
public abstract String currency();


Dollar
public String currency(){return "USD";}


Franc
public String currency(){return "CHF";}


测试通过。我们开始消除重复,这里Dollar和Franc的currency方法可否统一呢?
引入 currency变量
Money
protected String currency;


Dollar
public Dollar(int amount){
  this.amount = amount;
  currency = "USD";
}
public String currency(return currency;}


Franc
public Franc(int amount){
  this.amount = amount;
  currency = "CHF";
}
public String currency(return currency;}


这下一样了,于是我们把这个方法放入Money中
这时,我们也看到,Dollar和Franc的构造函数很像了。采用同样的方法,我们改造构造函数。

Dollar
public Dollar(int amount, String currency){
    this.amount = amount;
    this.currency = currency;
}


Franc也一样,我们把他们提到Money中,这时,我们会发现,Dollar类,和Franc类基本一摸一样了。除了times方法。
Money
public Money(int amount, String currency){
   this.amount = amount;
   this.currency = currency;
}


Dollar
public Dollar(int amount, String currency){
    super(amount,currency);
}


Franc
public Franc(int amount,String currency(){
    super(amount,currency);
}


好了,又减少了一个方法。但是我们还是不能完全消除Dollar类和Franc类。因为times方法。
现在就来看一看times方法。

Dollar 类
public Money times(int multiplier){
      return new Dollar(amount*multiplier,null);
}


Franc 类
public Money times(int multiplier){
      return new Franc(amount*multiplier,null);
}


这里的方法,我们还需要一个参数,暂时使用null替代。

这下我们看到了new,然后考虑是否使用工厂模式来创建呢?

Dollar 类
public Money times(int multiplier){
      return Money.dollar(amount*multiplier);
}


Franc 类
public Money times(int multiplier){
      return Money.franc(amount*multiplier);
}


这里还是无济于事。那么在打回,并且实现,而不是使用null替代。
Dollar 类
public Money times(int multiplier){
      return new Dollar(amount*multiplier,“USD”);
}


Franc 类
public Money times(int multiplier){
      return new Franc(amount*multiplier,“CHF”);
}

我们使用了常量来做,这个已经和Money中的工厂方法很像了。一个Money,也有自己的currency,那么如果我们返回的是Money呢?
是否可以这样
Dollar 类
public Money times(int multiplier){
      return new Money(amount*multiplier,"USD");
}


Franc 类
public Money times(int multiplier){
      return new Money(amount*multiplier,"CHF");
}

看起来快要成功了。我们忘了还有个currency,这就是这个Money的币类了。
换上
Dollar 类
public Money times(int multiplier){
      return new Money(amount*multiplier,currency);
}


Franc 类
public Money times(int multiplier){
      return new Money(amount*multiplier,currency);
}

这下完全一样了。说实话,这一步,真的很不容易。因为我们一样看不出来,并且也很难想到吧。但是我们确实可以删除Dollar和Franc了。因为他们已经没有方法了。
一切顺利,但是还有错误,你不能返回一个抽象类的实例吧。那就把Money改为非abstract吧。同时Money的times也变成一个实际方法了。

测试,失败了。这里我们疑惑了,为什么测试不过呢?看起来很诡异。
我觉得测试如果失败,并且一时看不出来,可能是类的信息,那么我们就需要使用以下toString了
重写toString方法,打印我们看得懂的

Money 类
public String toString(){
    return amount + " " + currency;
}

好了,这下看懂了。10 USD 期望的是 10 USD
这不一样嘛?
其实再看,就发现,原来是类不一致。这就是equals方法潜伏的问题。
我们这下可以按照业务需求去实现比较了

public boolean equals(Object obj){
    Money money = (Money)obj;
    return amount == money.amount &&
           currency.equals(money.currency());
}

这下心里好受了。但是这里还没完,我们应该用不到Dollar和Franc类了啊。
原来是工厂方法还在用,马山改掉。
修改Money的工厂方法

Money 类
public static Money dollar(int amount){
    return new Money(amount,"USD");
}
public static Money franc(int amount){
    return new Money(amount,"CHF");
}

这下测试,通过。我们删掉Dollar和Franc类,在看看有没有什么地方引用到这两个类。
没有。很好。终于完成了这一步。接下来,我们的to-do lists又该有什么了呢?
既然有了两种货币,就该比较,并且混合计算了。

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