一次代码评审引发的TDD

每周四下午我们会花一个小时针对一个选定的用户故事做代码评审,这次选定的用户故事是这样的:

做为一个物流服务提供者,我想查看货物从A点运到B点的运费报价是多少。举个例子,假如价目表如下,

起点 终点 运费
上海市 上海市 10元
上海市徐汇区 上海市虹口区 9元
徐汇区长桥街道 虹口区提篮桥街道 8元

当货物从上中路老沪闵路(徐汇区长桥街道)送到杨树浦路大连路(虹口区提篮桥街道)时,三条运费报价都满足条件,但是根据匹配最精确的报价原则,运费报价应该是8元。

匹配到三条运费报价的逻辑已经实现,这个迭代要实现的是从三条报价里选出最精确的那条报价。

简短地介绍完要做什么之后,负责这个用户故事的同事开始分享自己的实现代码和思路,当看到代码的主体部分是一棵有序二叉树时,我仿佛穿越到了大学时代的算法课上,等我回过神来的时候,我提出了我的困惑,“用二叉树实现的好处在哪里?如果另外一个人来维护这块代码,是否能hold住这棵树?” 后来因为时间的限制,再加上还没把怎么实现想得特别清楚,大家把精力放在了解二叉树相关的代码上,对于是否可以有更加简单和直观的方案并未做进一步的讨论。

Talk is cheap. Show me the code.

多说无益,更简单的代码才是最有说服力的,于是我决定用TDD(测试驱动开发)的方式来实现这个逻辑。
先从最简单的测试开始,假设只有一条匹配的报价(Tariff),那么最精确的也是这个报价。输入:

起点 终点 运费
上海市徐汇区 上海市虹口区 9元

输出:

起点 终点 运费
上海市徐汇区 上海市虹口区 9元

测试代码如下,

    @Test
    public void one_matched_tariff() {
        Tariff matchedTariff = new Tariff();
        List matchedTariffs = Arrays.asList(matchedTariff);
        List bestFitTariffs = Arrays.asList(matchedTariff);
        assertListEqual(bestFitTariffs, selectBestFit(matchedTariffs));
    }

然后添加最简单的实现代码让测试通过,

public class Tariff() {
}

public class BestFitTariffMatcher {
    public static List selectBestFit(List matchedTariffs) {
        return matchedTariffs;
    }
}

测试通过,接下来的测试是假设有两条匹配的报价,一条比另外一条更精确一些,那么应该返回更精确的那条报价。输入:

起点 终点 运费
上海市徐汇区 上海市虹口区 9元
徐汇区长桥街道 虹口区提篮桥街道 8元

输出:

起点 终点 运费
徐汇区长桥街道 虹口区提篮桥街道 8元

测试代码如下:

    @Test
    public void two_matched_tariffs_with_different_rank() {
        Tariff matchedTariff1 = new Tariff(1, 1);
        Tariff matchedTariff2 = new Tariff(2, 2);
        List matchedTariffs = Arrays.asList(matchedTariff1, matchedTariff2);
        List bestFitTariffs = Arrays.asList(matchedTariff1);
        assertListEqual(bestFitTariffs, selectBestFit(matchedTariffs));
    }

然后让测试通过,产品代码如下:

public class Tariff {
    //起点的级别,比如街道是1,区是2,市是3,级别越低位置越精确
    private int fromLevel;  
    //终点的级别
    private int toLevel;
    public Tariff(int fromLevel, int toLevel) {
        this.fromLevel = fromLevel;
        this.toLevel = toLevel;
    }
    public boolean fitThan(Tariff otherTariff) {
        return this.getFromLevel() <= otherTariff.getFromLevel()
            && this.getToLevel() <= otherTariff.getToLevel();
    }
}
public class BestFitTariffMatcher {
    public static List selectBestFit(List matchedTariffs) {
        if (matchedTariffs.size() <= 1) {
            return matchedTariffs;
        }

        Tariff bestFitTariff = matchedTariffs.get(0);

        for (int i = 1; i < matchedTariffs.size(); i++) {
            Tariff tariff = matchedTariffs.get(i);
            if (tariff.fitThan(bestFitTariff)) {
                bestFitTariff = tariff;
            }
        }
        return Arrays.asList(bestFitTariff);
    }
}

在快速让测试代码通过的过程中,我已经意识到最精确的报价可能不止一条,没有关系,让下面这个测试来完善这段代码,可以看到下面的测试中,两条报价的精确度是一样的,一条是区到街道,一个是街道到区。
输入:

起点 终点 运费
上海市徐汇区 虹口区提篮桥街道 8.5元
徐汇区长桥街道 上海市虹口区 8元

输出:

起点 终点 运费
上海市徐汇区 虹口区提篮桥街道 8.5元
徐汇区长桥街道 上海市虹口区 8元

测试代码如下,

    @Test
    public void two_matched_tariffs_with_same_rank() {
        Tariff matchedTariff1 = new Tariff(2, 1);
        Tariff matchedTariff2 = new Tariff(1, 2);
        List matchedTariffs = Arrays.asList(matchedTariff1, matchedTariff2);
        List bestFitTariffs = Arrays.asList(matchedTariff1, matchedTariff2);
        assertListEqual(bestFitTariffs, selectBestFit(matchedTariffs));
    }

修改产品代码,让测试通过,

public class BestFitTariffMatcher {
    public static List selectBestFit(List matchedTariffs) {
        if (matchedTariffs.size() <= 1) {
            return matchedTariffs;
        }

        List bestFitTariffs = Arrays.asList(matchedTariffs.get(0));
        for (int i = 1; i < matchedTariffs.size(); i++) {
            Tariff candidate = matchedTariffs.get(i);
            updateBestFitTariffs(candidate, bestFitTariffs);
        }
        return bestFitTariffs;
    }

    private void updateBestFitTariffs(Tariff candidate, List bestFits) {
        boolean acceptCandidate = true;
        List toBeRemoved = Lists.newArrayList();
        for (Tariff bestFit : bestFits) {
            if (tariff.fitThan(bestFit)) {
                toBeRemoved.add(bestFit);
                continue;
            }

            if (bestFit.fitThan(tariff)) {
                acceptCandidate = false;
                break;
            }
        }
        bestFits.removeAll(toBeRemoved);
        if (acceptCandidate) {
            bestFits.add(candidate);
        }
    }
}

测试通过后,感觉逻辑应该实现了,最后测试一个复杂点的,

    @Test
    public void six_tariffs() {
        Tariff matchedTariff1 = new Tariff(5, 6);
        Tariff matchedTariff2 = new Tariff(4, 5);
        Tariff matchedTariff3 = new Tariff(6, 1);
        Tariff matchedTariff4 = new Tariff(3, 3);
        Tariff matchedTariff5 = new Tariff(2, 4);
        Tariff matchedTariff6 = new Tariff(1, 6);
        List matchedTariffs = Arrays.asList(matchedTariff1, matchedTariff2, matchedTariff3, matchedTariff4, matchedTariff5, matchedTariff6);
        List bestFitTariffs = Arrays.asList(matchedTariff3, matchedTariff6, matchedTariff4, matchedTariff5);
        assertListEqual(bestFitTariffs, selectBestFit(matchedTariffs));
    }

测试也通过,证明实现代码是基本正确的,于是把这段代码分享给用二叉树实现的那位同事参考。

其实在用TDD实现这段代码之前,我想过应该怎么实现这个逻辑,但是最后TDD驱动出来的代码比我之前的想法更简单,这也许就是TDD的魅力所在吧。

你可能感兴趣的:(一次代码评审引发的TDD)