一次代碼評審引發的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的魅力所在吧。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容