一个完整的TDD演练案例(三)
说明:本讲义是我在ThoughtWorks作为咨询师时,为客户开展TDD Code Kata而编写。案例为Guess Number,案例需求来自当时的同事王瑜珩。当时,我们共同在ThoughtWorks的Zynx交付团队,为培养团队TDD能力进行训练时,引入了本案例。讲义中给出的代码问题则来自客户方的受训学员,可谓“真实的代码坏味道”。个人认为TDD不只是开发方法,还应该是设计方法,因此讲义中包含了诸多设计原理、思想和原则。
目标收益
- 熟悉IDE快捷键;
- 掌握TDD基本知识;
- 识别代码坏味道,熟练运用重构手法;
- 熟悉JUnit与Mockito框架;
- 了解Google Guice框架;
我们对Guess Number分解的任务为:
- 随机生成答案
- 判断每次猜测的结果
- 检查输入是否合法
- 记录并显示历史猜测数据
- 判断游戏结果。判断猜测次数,如果满6次但是未猜对则判负;如果在6次内猜测的4个数字值与位置都正确,则判胜
开始第三个任务
之所以将“验证输入是否合法”放在第三个任务,是因为它不属于happy path的范畴。它属于辅助业务,重要性相对次之。
提示:对于第三个任务,可以采用Specification By Example的方式来考虑测试用例。
问题:参数 vs. 字段
学员在定义执行该任务的类时,一种可能性是将输入的答案作为类的构造函数参数。例如:
new InputValidator("1 2 3 5").validate();
存在两个错误:
- 错误地判断了输入值的生命周期。什么内容应该放在构造函数中作为参数?换言之,构造函数参数与对象之间的关系是什么?之所以要作为构造函数参数,就是意味着在某种场景下这些参数值应该在创建该对象时就存在。这些参数值与对象“生死与共”,它们的生命周期是保持一致的。如果不是,就不应该作为构造函数的参数。你觉得输入应该作为构造函数吗?如果我要验证另一条输入应该怎么办?再创建一个InputValidator对象吗?
- 违反了阅读直觉。validate()方法验证谁?验证空吗?显然这样的接口违反了主-谓-宾的语法。
问题:封装的Answer与输入
既然已经封装了Answer对象,为何validate()方法还是要接收字符串类型的输入?阅读需求,已可寻求到答案。
问题:引入InputValidator类型是否有必要?
多数人会认为这里的验证逻辑与Answer相关,根据前面提到的“信息专家模式”,似乎应该将验证逻辑放到Answer中。然而,这里的需求明确地表示了,如果输入不符合要求,就不允许创建该Answer,而是抛出异常。所以,这里的部分验证逻辑是在创建Answer之前就应该存在,当然就不应该由Answer承担了。
针对第三个任务,验证结果的逻辑不应该由boolean型或错误码来表现。对于表达一种错误规则来说,如果你将其看做是一种业务规则,最好的表达方式是采用自定义异常,除非这门语言允许返回两个值(例如Go语言支持返回多个字,但并不支持异常)。对此,在第二个任务中已有描述,这里不再赘述。
重构:Answer的验证逻辑
在开发第二个任务时,我们已经在Answer类中定义了validate()方法。现在,InputValidator类又提供了validate()方法,且其中部分逻辑是相同的。在实现时,应该如何重构现有代码?
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 框架源码调试实战之easypoi异常解决方案精讲
- MySQL8.0的错误日志
- Shiro学习笔记(一)
- Shiro学习笔记(二)
- Shiro学习笔记 三(认证授权)
- Shiro学习笔记四(Shiro集成WEB)
- Shiro学习笔记五(Shiro标签,及通配符)
- Shiro学习笔记六(自定义Reaml-使用数据库设置 user roles permissions)
- Luncene学习 第一天 《入门程序》
- Luncene学习二《搜索索引》
- JavaWeb--简单分页技术
- 使用Python制作第一个爬虫程序
- 使用BeautifulSoup 爬取一个页面上的所有的超链接
- 使用PlaceHolder,测试碰见的问题
- 隐藏MySQL InnoDB Cluster / ReplicaSet实例