每周四下午我們會花一個小時針對一個選定的用戶故事做代碼評審,這次選定的用戶故事是這樣的:
做為一個物流服務提供者,我想查看貨物從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的魅力所在吧。