`
airu
  • 浏览: 267455 次
  • 性别: Icon_minigender_1
  • 来自: 云南
社区版块
存档分类
最新评论

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又该有什么了呢?
既然有了两种货币,就该比较,并且混合计算了。
分享到:
评论

相关推荐

    Study__TDD:个人TDD学习资料库

    埃迪的TDD学习笔记 :police_car_light: 警告不要合并任何PR。 :memo: 内容所有摘要都放在“问题”选项卡中。 当前,列出了以下内容: NHN FE개발랩摘要-Finsihed 견고한JS소프트웨어만들기-进行中测试Vue.js应用程序...

    AppFuse学习笔记(J2EE入门级框架)

    采用TDD的开发方式,使用JUnit测试各层,甚至测试 jsp 输出的 w/o 错误。为了简化开发,预定义好了一套目录结构、基类、用来创建数据库、配置Tomcat、测试部署应用的 Ant 任务,帮助快速自动生成源程序和自动维护...

    appfuse 学习笔记

    采用TDD的开发方式,使用JUnit测试各层,甚至测试 jsp 输出的 w/o 错误。为了简化开发,预定义好了一套目录结构、基类、用来创建数据库、配置Tomcat、测试部署应用的 Ant 任务,帮助快速自动生成源程序和自动维护...

    tdd-rails-pluralsight:使用RSpec,Capybara和Cucumber以及Pluralsight课程学习Rails的TDD

    具有RSpec,Capybara和Cucumber的测试驱动Rails 我在TDD Rails上的Pluralsight 课程中的笔记。 常用命令命令描述bin/rails s 启动Rails服务器bin/rails c 启动Rails控制台bundle exec rake routes 列出所有路线bin/...

    ng-tdd:了解AngularJS测试驱动开发

    ng-tdd 通过本书学习 它是如何工作的? 安装依赖项 npm install bower install 使用业力运行规格测试 karma start karma.conf.js 使用量角器运行e2e测试 http-server -p 5555 protractor conf.js 我的笔记 第二...

    java银行笔试题-tdd-bank-account-java:TD-银行账户-java

    中要学习/练习的两个关键事项: 测试驱动开发 结对编程 指示 使用以下命令克隆存储库。 git clone https://github.com/xp-dojo/tdd-bank-account-java 如果您遇到 SSL 问题,可以尝试以下操作。 git clone -c ...

    云学习:我对云的个人笔记和笔记

    我的云学习这是一个回购记录,用于跟踪我在软件开发(尤其是云计算)方面学到的东西。目的我决定这样做是为了让自己有更新的地方,以防万一我忘记了自己学到的东西。 另外,我决定将它公开给其他人学习,使用和重用...

    AgileJavaNote:在学习敏捷Java时记录我的笔记

    AgileJavaNote ###简介: 自学敏捷开发的思想,采用TDD 的方式开发一个学生信息系统 ###成果: 运用了UML 进行类的设计;对JAVA 中大部分的概念和特性动手编程实践;使用JUnit 对每个模块编写测试用例

    kharioki:我的个人资料

    嘿,我是Tony Stark :speech_balloon: 我是一名软件工程师,数字Nomad者...机器学习: Numpy,Pandas,Jupyter笔记本,Matplotlib,Scikit学习,Tensorflow。 TDD: Jest,Mocha,Chai,Cypress,React测试库 敏捷的

    模块化C代码与UML对象模型之间的映射(一)

    日子一天天过去,业余时间不多,为了避免生活华而不实,碌碌无为,我计划抽空把工作和学习中散落在笔记本和书本某些角落的总结整理出来,放到博客上备忘,同时也希望有机会和技术圈的朋友交流,增长自己的见识,我的...

    spa-tuts:学习如何使用 Tuts Plus 从头开始​​构建 JavaScript SPA

    使用生成的目录 删除视图骨干路由器单元测试和 TDD 什么才是好的单元测试? 测试设置网络服务设置模型和集合本地存储RequireJS 调试技巧本地存储和收藏模板下划线模板[小胡子](#...

    jungle-rails:用Rails 4.2构建的一个小型电子商务应用程序,用于通过示例学习Rails的目的

    由于这个项目,我了解了有关Ruby on Rails,模型-视图-控制器范例,嵌入式Ruby和测试(TDD和BDD)的更多信息。笔记: 已知问题:来宾(未经身份验证)用户仍然可以下订单。 下一步,应禁用此选项。主页管理员类别...

    shadow:一个可常用PHP公共资源包,同时也是百度文库课程《如何写好一个PHP的类》的

    课程的主要内容可以在“课程笔记”中找到,里面有每一节的主要内容讲解。 课程介绍 首先,这门课面向PHP开发者,主要讲解一个类库的建立,开发和使用。的是,这里面讲到的都是偏实用的东西。这里面会涉及到一些经常...

    rspec-webservice_matchers:黑匣子网络应用程序测试

    安装$ gem install rspec-webservice_matchers你得到什么这些新的RSpec匹配器: 笔记be_up 寻找200,但最多可追踪4次重新导向be_fast 检查Google 分数是否大于WEBSERVICE_MATCHER_INSIGHTS_KEY 85。 WEBSERVICE_...

Global site tag (gtag.js) - Google Analytics