一次代码评审引发的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<Tariff> matchedTariffs = Arrays.asList(matchedTariff);
        List<Tariff> bestFitTariffs = Arrays.asList(matchedTariff);
        assertListEqual(bestFitTariffs, selectBestFit(matchedTariffs));
    }

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

public class Tariff() {
}

public class BestFitTariffMatcher {
    public static List<Tariff> selectBestFit(List<Tariff> 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<Tariff> matchedTariffs = Arrays.asList(matchedTariff1, matchedTariff2);
        List<Tariff> 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<Tariff> selectBestFit(List<Tariff> 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<Tariff> matchedTariffs = Arrays.asList(matchedTariff1, matchedTariff2);
        List<Tariff> bestFitTariffs = Arrays.asList(matchedTariff1, matchedTariff2);
        assertListEqual(bestFitTariffs, selectBestFit(matchedTariffs));
    }

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

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

        List<Tariff> 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<Tariff> bestFits) {
        boolean acceptCandidate = true;
        List<Tariff> 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<Tariff> matchedTariffs = Arrays.asList(matchedTariff1, matchedTariff2, matchedTariff3, matchedTariff4, matchedTariff5, matchedTariff6);
        List<Tariff> bestFitTariffs = Arrays.asList(matchedTariff3, matchedTariff6, matchedTariff4, matchedTariff5);
        assertListEqual(bestFitTariffs, selectBestFit(matchedTariffs));
    }

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容