TDD和重构练习-FizzBuzz Sprint 1

作者: CodingDetails | 来源:发表于2019-05-14 18:16 被阅读104次

    FizzBuzz

    For

    Kobe Bryant details how Kevin Durant can get even better | 'Detail' Excerpt | ESPN

    Sprint 1

    • Product backlog
      1. 从1至100依次报数,如第1位报“1”,第2位报“2”
      2. 如果碰到被3整除的数则报“Fizz”
      3. 如果碰到被5整除的数则报“Buzz”
      4. 如果同时被3和5整除则报“FizzBuzz”

    操作分解

    下面以3分钟为时限来组织操作,每一组完成一个明确的目标。

    第1组操作:创建工程和配置

    • 创建工程

      • 开发环境:IntelliJ IDEA Community Edition 2019.1.1 x64
      • 编程语言:Java8
      • 构建系统:Gradle
    • 配置单元测试

      • Junit5

    Junit5不知道如何配置?

    步骤 动作 结果 用时
    1 通过搜索引擎搜索Junit5 找到官方主页https://junit.org/junit5/ 10s
    2 浏览主页,寻找文档链接 找到User Guide 10s
    3 浏览文档目录,寻找Gradle的配置 找到4.2.1节:Running Test -> Build Support -> Gradle 10.1.2节:JUnit Jupiter 30s
    4 照文档修改build.gradle 修改结果如下 60s
    test {
        useJUnitPlatform()
    }
    
    dependencies {
        testCompile("org.junit.jupiter:junit-jupiter-api:5.4.2")
        testCompile("org.junit.jupiter:junit-jupiter-params:5.4.2")
        testRuntime("org.junit.jupiter:junit-jupiter-engine:5.4.2")
    }
    
    • Refresh Gradle project

    一般会自动刷新,也可手动刷新:在BuildGradle窗口的左上角都有该刷新按钮。

    第2组操作:Hello, Junit

    创建测试类/文件

    步骤 动作 结果 用时
    1 Alt+1 跳到Project窗口 1s
    2 ↓→↑← 定位到src/test/java目录 3s
    3 Alt+Ins 弹出Generate或New菜单 1s
    4 选择Java Class回车 弹出Create New Class对话框 1s
    5 输入类名FizzBuzzTest回车 跳到新建的类编辑窗口 5s

    验证单元测试可工作

    步骤 动作 结果 用时
    1 Alt+Ins 弹出Generate菜单 1s
    2 选择Test Method回车 新建一个测试方法,并选择方法名 1s
    3 输入方法名should_work_ok回车 进入函数体 3s
    4 开始写第一行代码 输入代码如下 10s
    5 Ctrl+Shift+F10运行该测试方法 Run窗口显示Test Result 5s
    assertEquals(1, 1);
    

    第3组操作:实现第1条PBI

    为该PBI创建测试方法

    借助IDE的代码生成功能,快速完成类和函数的创建。

    步骤 动作 结果 用时
    1 Alt+Ins 弹出Generate菜单 1s
    2 选择Test Method回车 新建一个测试方法,并选择方法名 1s
    3 输入方法名should_input_1_return_1回车 进入函数体 3s
    4 开始写第一行代码 输入代码如下 10s
    5 光标定位在FizzBuzz,按Alt+Enter 弹出Generate对话框 3s
    6 选择Create Class FizzBuzz回车 弹出对话框配置包名和路径 1s
    7 包名留空,路径选择main,回车 进入新建的类FizzBuzz编辑区 3s
    8 Ctrl+Tab 切换回测试类编辑区 1s
    9 光标定位到FizzBuzz构造函数,按Alt+Enter 弹出Generate对话框 3s
    10 选择Create constructor回车 跳到类FizzBuzz新建的构造函数 1s
    11 Shift+F10运行上次运行过的测试方法 Run窗口显示Test Result 5s
    12 结果测试失败,红色提示
    @Test
    void should_input_1_return_1() {
        FizzBuzz item = new FizzBuzz(1);
        assertEquals("1", item.toString());
    }
    

    快速让该测试通过

    步骤 动作 结果 用时
    1 在FizzBuzz类编辑区,按Ctrl+O 弹出选择Override方法菜单 1s
    2 选择toString方法,回车 跳到toString函数体内 3s
    3 修改代码,让测试通过 代码结果如下 10s
    4 Shift+F10运行上次运行过的测试方法 Run窗口显示Test Result 5s
    5 结果测试成功,绿色提示
    @Override
    public String toString() {
        return "1";
    }
    

    重构代码

    步骤 动作 结果 用时
    1 修改toString实现 代码结果如下toString部分 10s
    2 光标定位在value,按Alt+Enter 弹出Generate对话框 3s
    3 选择Create Field,回车 跳到新建的成员变量处 1s
    4 在构造函数内为该成员变量赋值 代码结果如下构造函数部分 10s
    4 Shift+F10运行测试 仍是绿色,说明重构没出错 5s
    @Override
    public String toString() {
        return String.valueOf(this.value);
    }
    
    public FizzBuzz(int i) {
        this.value = i;
    }
    

    第4组操作:实现第2条PBI

    为PBI2创建测试方法

    步骤 动作 结果 用时
    1 Ctrl+Tab 切回到测试代码编辑区 1s
    2 选中已有的一个测试方法,按Ctrl+D 复制一份新的测试方法 3s
    3 为新复制的方法改名 代码如下:仅改函数名 3s
    4 修改新测试代码以实现PBI2 代码如下:函数内容 10s
    5 光标定位到测试方法外,测试类里面,Ctrl+Shift+F10运行测试 这样可以测试全部方法,结果是红色 5s
    @Test
    void should_input_3_return_Fizz() {
        FizzBuzz item = new FizzBuzz(3);
        assertEquals("Fizz", item.toString());
    }
    

    快速让测试should_input_3_return_Fizz通过

    从此处开始省略Baby Steps描述。

    修改toString实现,然后运行测试,绿色。

    @Override
    public String toString() {
        if (this.value % 3 == 0) return "Fizz";
        return String.valueOf(this.value);
    }
    

    重构测试代码

    使用Junit5的参数化测试方法,可以消除重复的测试方法,然后运行测试,绿色。

    @ParameterizedTest(name = "should return {1} given {0}")
    @CsvSource({
            "1, 1",
            "3, Fizz",
    })
    void should_test_sprint_1(int input, String expected) {
        FizzBuzz item = new FizzBuzz(input);
        assertEquals(expected, item.toString());
    }
    

    第5组操作:实现第3条PBI

    • 为该PBI添加测试数据,然后运行测试,红色

      测试方法的CsvSource内增加一条数据:"5, Buzz"

    • 快速让测试通过

      FizzBuzztoString方法内增加新逻辑,代码如下,然后运行测试,绿色

    @Override
    public String toString() {
        if (this.value % 3 == 0) return "Fizz";
        if (this.value % 5 == 0) return "Buzz";
        return String.valueOf(this.value);
    }
    
    • 重构:提取函数

    toString内有2个if表达式重复了,可以提取出到1个函数里面,代码如下。

    从此处开始省略运行测试的提示,每次代码改动结束后,都应该运行测试。

    @Override
    public String toString() {
        if (isDivBy(3)) return "Fizz";
        if (isDivBy(5)) return "Buzz";
        return String.valueOf(this.value);
    }
    
    private boolean isDivBy(int i) {
        return this.value % i == 0;
    }
    

    第6组操作:实现第4条PBI

    • 为该PBI添加测试数据

      测试方法的CsvSource内增加一条数据:"15, FizzBuzz"

    • 快速让测试通过

      FizzBuzztoString方法内增加新逻辑
      将光标移到到isDivBy(5)这一行,Ctrl+C选中该行,Ctrl+D复制选中内容到其之后,然后修改以适合PBI4。

    @Override
    public String toString() {
        if (isDivBy(3)) return "Fizz";
        if (isDivBy(5)) return "Buzz";
        if (isDivBy(15)) return "FizzBuzz";
        return String.valueOf(this.value);
    }
    
    • 修复错误

      上述修改没有一次性使测试通过,输入15时,期望得到FizzBuzz,结果却是Fizz,说明出现了Bug。
      经查代码,可以快速发现该错误是第一条条件语句if (isDivBy(3)) return "Fizz";造成,要想正确处理,需要调整这几条条件语句的顺序,修改如下:

    @Override
    public String toString() {
        if (isDivBy(15)) return "FizzBuzz";
        if (isDivBy(3)) return "Fizz";
        if (isDivBy(5)) return "Buzz";
        return String.valueOf(this.value);
    }
    
    • 重构:消灭代码坏味道

      toString方法里已经包含了4个条件判断的逻辑,可以预见后续迭代中,修改都会集中到该方法中,形成Long Method过长函数。
      该重构过程需要步骤较多,故单独形成一组操作。

    第7组操作:重构过长函数

    • 观察到4个条件语句具有相同的结构
      输入一个数字,输出一个字符串,只是判断的表达式不一样
    • 明确该方法的职责
      该方法最终要返回一个字符串,说明这是其本职,判断逻辑可以委托出去
    • 第一条判断逻辑看起来像是后俩条逻辑的组合,有点复杂,所以先动第二条逻辑,尝试委托出去
    @Override
    public String toString() {
        if (isDivBy(15)) return "FizzBuzz";
    
        String result1 = ruleFizzResult();
        if (!result1.isEmpty()) return result1;
    
        if (isDivBy(5)) return "Buzz";
    
        return String.valueOf(this.value);
    }
    
    private String ruleFizzResult() {
        if (isDivBy(3)) return "Fizz";
        return "";
    }
    
    • 尝试委托第三条逻辑
    @Override
    public String toString() {
        if (isDivBy(15)) return "FizzBuzz";
    
        String result1 = ruleFizzResult();
        if (!result1.isEmpty()) return result1;
    
        String result2 = ruleBuzzResult();
        if (!result2.isEmpty()) return result2;
    
        return String.valueOf(this.value);
    }
    
    private String ruleFizzResult() {
        if (isDivBy(3)) return "Fizz";
        return "";
    }
    
    private String ruleBuzzResult() {
        if (isDivBy(5)) return "Buzz";
        return "";
    }
    
    • 尝试组合实现第一条逻辑
    @Override
    public String toString() {
        String result1 = ruleFizzResult();
        String result2 = ruleBuzzResult();
    
        if (!result1.isEmpty() && !result2.isEmpty()) {
            return result1 + result2;
        }
    
        if (!result1.isEmpty()) return result1;
    
        if (!result2.isEmpty()) return result2;
    
        return String.valueOf(this.value);
    }
    
    • 消除重复的对resultx的判断
    @Override
    public String toString() {
        String result1 = ruleFizzResult();
        String result2 = ruleBuzzResult();
    
        String result = result1 + result2;
    
        if (!result.isEmpty()) return result;
    
        return String.valueOf(this.value);
    }
    
    • 消除对rule规则结果的重复获取
    @Override
    public String toString() {
    
        String[] results = getAllRuleResult();
    
        String result = String.join("", results);
    
        if (!result.isEmpty()) return result;
    
        return String.valueOf(this.value);
    }
    
    private String[] getAllRuleResult() {
        String result1 = ruleFizzResult();
        String result2 = ruleBuzzResult();
    
        return new String[] {result1, result2};
    }
    
    • 继续理清toString的职责
      首先获取了所有原子规则的结果,
      接着应用组合规则,生成组合的结果,该结果也兼容了原子规则
      最后是默认规则,但所有明确的规则都失效后才使用。
      对结果的处理可以提取函数为一个独立职责。
    @Override
    public String toString() {
        String[] results = getAtomicRuleResult();
        return getComponentRuleResult(results);
    }
    
    private String getComponentRuleResult(String[] results) {
        String result = String.join("", results);
    
        if (!result.isEmpty()) return result;
    
        return String.valueOf(this.value);
    }
    
    // `重命名函数`为原则规则的结果
    private String[] getAtomicRuleResult() {
        String result1 = ruleFizzResult();
        String result2 = ruleBuzzResult();
    
        return new String[] {result1, result2};
    }
    
    • 优化组合规则结果的操作逻辑

      使用Collection Pipelines,代替使用原始的逻辑判断if

    private String getComponentRuleResult(String[] results) {
        return Arrays.stream(results)
            .filter(v -> !v.isEmpty())
            .reduce(String::concat)
            .orElse(String.valueOf(this.value));
    }
    
    • 优化原子规则获取结果操作

      使用Inline Temp将临时变量内联化。

    private String[] getAtomicRuleResult() {
        return new String[] {ruleFizzResult(), ruleBuzzResult()};
    }
    
    • 截止目前的重构结果
    @Override
    public String toString() {
        String[] results = getAtomicRuleResult();
        return getComponentRuleResult(results);
    }
    
    private String[] getAtomicRuleResult() {
        return new String[] {ruleFizzResult(), ruleBuzzResult()};
    }
    
    private String getComponentRuleResult(String[] results) {
        return Arrays.stream(results)
            .filter(v -> !v.isEmpty())
            .reduce(String::concat)
            .orElse(String.valueOf(this.value));
    }
    
    private String ruleFizzResult() {
        if (isDivBy(3)) return "Fizz";
        return "";
    }
    
    private String ruleBuzzResult() {
        if (isDivBy(5)) return "Buzz";
        return "";
    }
    
    private boolean isDivBy(int i) {
        return this.value % i == 0;
    }
    

    第8组操作:分离原子规则职责

    使用Extract Class提炼类

    • getAtomicRuleResult中添加规则工厂的调用
    private String[] getAtomicRuleResult() {
        List<Executable> rules = Rules.all();
        return new String[] {ruleFizzResult(), ruleBuzzResult()};
    }
    
    • 使用Alt+Enter快速生成Executable接口、Rules类和all静态方法
    • 使用新的实现替换旧的实现
    private String[] getAtomicRuleResult() {
        List<Executable> rules = Rules.all();
        return rules.stream()
                .map(rule -> rule.exec(this.value))
                .toArray(String[]::new);
    }
    
    • 使用Alt+Enter快速生成Executable接口的exec方法
    public interface Executable {
        String exec(int i);
    }
    
    • 实现Rules.all,添加创建规则的工厂类和方法调用
    public class Rules {
    
        public static List<Executable> all() {
            return Arrays.asList(
                    DivRule.create(3, "Fizz"),
                    DivRule.create(5, "Buzz")
            );
        }
    }
    
    • 使用Alt+Enter快速生成DivRule类和create方法
    • 实现create方法和重载的exec方法,代码如下
    public class DivRule implements Executable {
        private int input;
        private final String output;
    
        public DivRule(int in, String out) {
            this.input = in;
            this.output = out;
        }
    
        public static Executable create(int in, String out) {
            return new DivRule(in, out);
        }
    
        @Override
        public String exec(int i) {
            if (i % this.input == 0) return this.output;
            return "";
        }
    }
    
    • 删除FizzBuzz中不需要的代码,剩余代码如下
    @Override
    public String toString() {
        String[] results = getAtomicRuleResult();
        return getComponentRuleResult(results);
    }
    
    private String[] getAtomicRuleResult() {
        List<Executable> rules = Rules.all();
        return rules.stream()
                .map(rule -> rule.exec(this.value))
                .toArray(String[]::new);
    }
    
    private String getComponentRuleResult(String[] results) {
        return Arrays.stream(results)
            .filter(v -> !v.isEmpty())
            .reduce(String::concat)
            .orElse(String.valueOf(this.value));
    }
    
    • 重构toString使用Inline Method将函数内联化,消除额外的2个函数
    @Override
    public String toString() {
        return Rules.all()
            .stream()
            .map(rule -> rule.exec(this.value))
            .filter(v -> !v.isEmpty())
            .reduce(String::concat)
            .orElse(String.valueOf(this.value));
    }
    

    Over

    相关文章

      网友评论

        本文标题:TDD和重构练习-FizzBuzz Sprint 1

        本文链接:https://www.haomeiwen.com/subject/wcueaqtx.html