TDD(測(cè)試驅(qū)動(dòng)開(kāi)發(fā))示范姿勢(shì)(下)

寫(xiě)給想要上手試試 TDD诚亚,卻不知從何入手的同學(xué)。

(上)集在這里:http://www.reibang.com/p/a5766176c493

第三個(gè)大任務(wù)

歡迎回來(lái)纺念。在開(kāi)始第三個(gè)大任務(wù)“處理 2 個(gè)參數(shù)”之前苫耸,要注意疫鹊,我們還沒(méi)有對(duì)這個(gè)任務(wù)做拆解。是的伐蒂,記住煞躬,

一定要拆小了再做。

這個(gè)任務(wù)怎么拆呢逸邦?這里恩沛,參數(shù)個(gè)數(shù)已經(jīng)確定了(必須是兩個(gè)),那么還有哪些沒(méi)確定的東西缕减?那就是參數(shù)類(lèi)型雷客。兩個(gè)參數(shù),三種類(lèi)型桥狡,一共有六種排列方式搅裙。不好選皱卓。不過(guò)我們可以先分個(gè)類(lèi):從兩個(gè)參數(shù)的類(lèi)型的異同方面看。你希望首先處理兩個(gè)不同類(lèi)型的參數(shù)部逮,還是兩個(gè)相同類(lèi)型的參數(shù)娜汁?這個(gè)不用糾結(jié)吧,肯定是兩個(gè)相同類(lèi)型啊兄朋,因?yàn)檫@樣處理

難度更低掐禁。

然后,你會(huì)選擇兩個(gè)什么參數(shù)類(lèi)型颅和??jī)蓚€(gè)布爾穆桂??jī)蓚€(gè)整數(shù)?還是兩個(gè)字符串融虽?思考一下。直觀上感覺(jué)灼芭,兩個(gè)布爾可能是最簡(jiǎn)單的有额。但是我們的情況不是這樣,為什么彼绷?這就要從我們目前的實(shí)現(xiàn)代碼說(shuō)起了巍佑。我們目前的代碼,是連續(xù)從 commandLine 里面“吃”兩個(gè)部分寄悯,并將第一部分作為標(biāo)志萤衰,第二部分作為參數(shù)值。而布爾型參數(shù)是不需要傳值的猜旬,所以現(xiàn)有代碼邏輯會(huì)導(dǎo)致第二個(gè)參數(shù)的標(biāo)志脆栋,被當(dāng)做第一個(gè)參數(shù)的值,被“吃”掉洒擦。所以首先處理兩個(gè)需要傳值的參數(shù)類(lèi)型是更簡(jiǎn)單的椿争。就暫定兩個(gè)整數(shù)型吧,更新任務(wù)清單:

  • 處理 2 個(gè)參數(shù)
    • 處理 2 個(gè)整數(shù)型的參數(shù)

接下來(lái)呢熟嫩?有必要再拆出“處理 2 個(gè)字符串型的參數(shù)”嗎秦踪?沒(méi)有必要,因?yàn)閰?shù)類(lèi)型轉(zhuǎn)換的邏輯是已經(jīng)有了的掸茅。拆出這個(gè)來(lái)椅邓,不會(huì)驅(qū)動(dòng)我們的實(shí)現(xiàn)代碼。所以昧狮,接下來(lái)應(yīng)該處理布爾型參數(shù):

  • 處理 2 個(gè)參數(shù)
    • 處理 2 個(gè)整數(shù)型的參數(shù)
    • 處理 2 個(gè)布爾型的參數(shù)

相同類(lèi)型的都拆完了景馁,接下來(lái)是不同類(lèi)型的參數(shù)。該怎么拆呢陵且?暫時(shí)沒(méi)有什么頭緒裁僧。沒(méi)關(guān)系个束,我們可以把這兩個(gè)小任務(wù)做完了再看,還是延遲決定聊疲。

又可以開(kāi)始愉快的編碼了茬底。首先做什么不用再說(shuō)了吧,先寫(xiě)一個(gè)失敗的測(cè)試:

describe('處理 2 個(gè)參數(shù)', () => {

    it('處理 2 個(gè)整數(shù)型的參數(shù)', () => {
        let schemas = [IntegerSchema('p'), IntegerSchema('q')];
        let parser = new ArgumentParser(schemas);

        let result = parser.parse('-p 8080 -q 9527');

        expect(result.get('p')).toEqual(8080);
        expect(result.get('q')).toEqual(9527);
    });

});

保存获洲,不出意外的變紅了阱表。為啥?當(dāng)然是還沒(méi)有實(shí)現(xiàn)嘛贡珊。目前的代碼只能從 commandLine 里面取出第一個(gè)參數(shù)的標(biāo)志和值最爬,沒(méi)有處理后續(xù)參數(shù)。所以门岔,我們只需要把 if 改成 while 就可以了:

parse(commandLine) {
    let args = this.schemas.map(schema => this.getDefaultValue(schema));
    let tokens = commandLine.split(' ').filter(t => t.length);
    while (tokens.length) {
        let flag = tokens.shift().substring(1);
        let value = tokens.shift();
        let schema = this.schemas.find(s => s.flag === flag);
        let arg = args.find(a => a.flag === flag);
        arg.value = schema.type.convert(value);
    }
    return new Arguments(args);
}

保存爱致,綠了。需要重構(gòu)嗎寒随?可以考慮接下來(lái)要解析布爾型的任務(wù)糠悯。布爾型不能“吃”參數(shù)值,也就是說(shuō)目前抽取參數(shù)值的這部分代碼妻往,不能無(wú)條件執(zhí)行互艾,而是要根據(jù)當(dāng)前參數(shù)標(biāo)志所反映的規(guī)則類(lèi)型來(lái)確定。所以我們需要把找規(guī)則的代碼移動(dòng)到“吃”參數(shù)值的前面讯泣。

parse(commandLine) {
    let args = this.schemas.map(schema => this.getDefaultValue(schema));
    let tokens = commandLine.split(' ').filter(t => t.length);
    while (tokens.length) {
        let flag = tokens.shift().substring(1);
        let schema = this.schemas.find(s => s.flag === flag);
        let value = tokens.shift();
        let arg = args.find(a => a.flag === flag);
        arg.value = schema.type.convert(value);
    }
    return new Arguments(args);
}

保存纫普,仍然是綠的。好了好渠,開(kāi)始下一個(gè)任務(wù)昨稼,“處理 2 個(gè)布爾型的參數(shù)”。先來(lái)一個(gè)失敗的測(cè)試:

it('處理 2 個(gè)布爾型的參數(shù)', () => {
    let schemas = [BooleanSchema('d'), BooleanSchema('e')];
    let parser = new ArgumentParser(schemas);

    let result = parser.parse('-d -e');

    expect(result.get('d')).toEqual(true);
    expect(result.get('e')).toEqual(true);
});

保存晦墙,紅了悦昵。注意看出錯(cuò)提示,里面有告訴你是哪一行測(cè)試出的錯(cuò)晌畅〉福可以看到,是第二個(gè) expect 的條件沒(méi)有被滿足抗楔。因?yàn)樘幚淼谝粋€(gè)參數(shù)的時(shí)候棋凳,就把第二個(gè)參數(shù)的標(biāo)志當(dāng)做第一個(gè)參數(shù)的值,給“吃”掉了连躏。修正也很簡(jiǎn)單剩岳,加個(gè)判斷就好了,只需要改一行:

let value = schema.type === BooleanArgumentType ? undefined : tokens.shift();

保存入热,綠了拍棕。需要重構(gòu)嗎晓铆?剛剛這一行其實(shí)就有需要重構(gòu)的地方。這行是根據(jù)參數(shù)類(lèi)型绰播,決定是否“吃”規(guī)則參數(shù)值骄噪。而這實(shí)際上是參數(shù)類(lèi)型本身的邏輯,或者說(shuō)只和參數(shù)類(lèi)型有關(guān)蠢箩,而不應(yīng)該放在規(guī)則解析器里面链蕊。所以我們把它挪到 BooleanArgumentType 里面:

export class ArgumentType {

    static needValue() {
        return true;
    }

}

export class BooleanArgumentType extends ArgumentType {

    static default() {
        return false;
    }

    static convert() {
        return true;
    }

    static needValue() {
        return false;
    }

}

對(duì)應(yīng)的 parse() 方法:

parse(commandLine) {
    let args = this.schemas.map(schema => this.getDefaultValue(schema));
    let tokens = commandLine.split(' ').filter(t => t.length);
    while (tokens.length) {
        let flag = tokens.shift().substring(1);
        let schema = this.schemas.find(s => s.flag === flag);
        let value = schema.type.needValue() ? tokens.shift() : undefined;
        let arg = args.find(a => a.flag === flag);
        arg.value = schema.type.convert(value);
    }
    return new Arguments(args);
}

保存,綠的谬泌。這個(gè)方法越來(lái)越復(fù)雜了滔韵,早就看這個(gè)方法不順眼了,我們來(lái)重構(gòu)它掌实。不要去看實(shí)現(xiàn)陪蜻,直接從業(yè)務(wù)角度看,這個(gè)方法其實(shí)只干兩件事情:根據(jù)默認(rèn)值創(chuàng)建參數(shù)列表贱鼻;解析命令行用傳入值覆蓋默認(rèn)值囱皿。所以我們連續(xù)使用兩次抽取方法(記得每次抽取之后都要保存跑測(cè)試):

parse(commandLine) {
    let args = this.createDefaultArguments();
    this.parseCommandLine(commandLine, args);
    return new Arguments(args);
}

parseCommandLine(commandLine, args) {
    let tokens = commandLine.split(' ').filter(t => t.length);
    while (tokens.length) {
        let flag = tokens.shift().substring(1);
        let schema = this.schemas.find(s => s.flag === flag);
        let value = schema.type.needValue() ? tokens.shift() : undefined;
        let arg = args.find(a => a.flag === flag);
        arg.value = schema.type.convert(value);
    }
}

createDefaultArguments() {
    return this.schemas.map(schema => this.getDefaultValue(schema));
}

args 到處傳來(lái)傳去也很煩,可以把它作為我們解析器的一個(gè)內(nèi)部狀態(tài)(屬性):

parse(commandLine) {
    this.createDefaultArguments();
    this.parseCommandLine(commandLine);
    return new Arguments(this.args);
}

parseCommandLine(commandLine) {
    let tokens = commandLine.split(' ').filter(t => t.length);
    while (tokens.length) {
        let flag = tokens.shift().substring(1);
        let schema = this.schemas.find(s => s.flag === flag);
        let value = schema.type.needValue() ? tokens.shift() : undefined;
        let arg = this.args.find(a => a.flag === flag);
        arg.value = schema.type.convert(value);
    }
}

createDefaultArguments() {
    this.args = this.schemas.map(schema => this.getDefaultValue(schema));
}

保存忱嘹,綠的。再看 parseCommandLine() 方法耕渴,它干了兩件事:把 commandLine 拆分成 tokens拘悦,以及解析 tokens 中的內(nèi)容。所以我們把這兩個(gè)職責(zé)分開(kāi):

parse(commandLine) {
    this.createDefaultArguments();
    this.tokenizeCommandLine(commandLine);
    this.parseTokens();
    return new Arguments(this.args);
}

parseTokens() {
    while (this.tokens.length) {
        let flag = this.tokens.shift().substring(1);
        let schema = this.schemas.find(s => s.flag === flag);
        let value = schema.type.needValue() ? this.tokens.shift() : undefined;
        let arg = this.args.find(a => a.flag === flag);
        arg.value = schema.type.convert(value);
    }
}

tokenizeCommandLine(commandLine) {
    this.tokens = commandLine.split(' ').filter(t => t.length);
}

保存橱脸,綠的础米。嗯,現(xiàn)在 parse() 方法看起來(lái)很清爽了添诉。接下來(lái)我們關(guān)注另外兩個(gè)方法屁桑。可以看到一堆對(duì) tokens 的使用:拆開(kāi)栏赴,取出參數(shù)標(biāo)志蘑斧,取出參數(shù)值。既然它們都和 token 有關(guān)须眷,那么就應(yīng)該被放到一個(gè)與 token 有關(guān)的竖瘾,獨(dú)立的類(lèi)里面。先直接在該文件中創(chuàng)建一個(gè)新類(lèi):


class Tokenizer {

    constructor(commandLine) {
        this.tokens = commandLine.split(' ').filter(t => t.length);
    }

}

保存花颗,綠的捕传。為了實(shí)現(xiàn)平滑替換,我們需要看看扩劝,使用 tokens 的地方庸论,用到了它的哪些屬性和方法职辅。目前看來(lái),只有 parseTokens() 方法里面會(huì)用到聂示,分別是 length 屬性和 shift() 方法域携。我們先用最直接的方式實(shí)現(xiàn)它們(在 Tokenizer 類(lèi)里面):

get length() {
    return this.tokens.length;
}

shift() {
    return this.tokens.shift();
}

保存,還是綠的催什。然后修改 tokenizeCommandLine() 方法:

tokenizeCommandLine(commandLine) {
    this.tokens = new Tokenizer(commandLine);
}

保存涵亏,綠的。我們已經(jīng)把 tokens 由原生數(shù)組對(duì)象蒲凶,成功替換為我們的 Tokenizer 類(lèi)的對(duì)象了气筋。繼續(xù)重構(gòu)⌒玻看看 parseTokens() 里面的 while 語(yǔ)句宠默,判斷 tokens 的長(zhǎng)度,用于決定是否繼續(xù)循環(huán)灵巧。這個(gè)判斷不是很有描述性搀矫,我們?yōu)?Tokenizer 類(lèi)加一個(gè)新方法。既然是用于判斷是否還有更多的 token刻肄,那么就命名為 hasMore() 吧:

hasMore() {
    return this.tokens.length > 0;
}

保存瓤球,綠的。然后修改 parseTokens() 里面的 while 的條件判斷:

while (this.tokens.hasMore()) {
    // ...
}

保存敏弃,綠的∝韵郏現(xiàn)在 Tokenizer.length 屬性已經(jīng)沒(méi)有地方用到了,IDE 也會(huì)自動(dòng)將這個(gè)屬性名標(biāo)記為灰色麦到。我們直接刪除掉這個(gè)屬性定義即可绿饵。保存,還是綠的瓶颠。接下來(lái)是兩處 shift() 調(diào)用拟赊,分別用于取出標(biāo)志和值。先處理取標(biāo)志粹淋。通常這種在循環(huán)里面吸祟,一個(gè)一個(gè)取的動(dòng)作,我們稱之為 next桃移。那么取出一個(gè)標(biāo)志欢搜,就命名為 nextFlag()。為 Tokenizer 類(lèi)加入該方法:

nextFlag() {
    return this.tokens.shift().substring(1);
}

保存谴轮,綠的炒瘟。修改第一處對(duì) tokens.shift() 的調(diào)用:

let flag = this.tokens.nextFlag();

保存,還是綠的第步。然后是用于取值的那個(gè) tokens.shift() 調(diào)用疮装,取出值就是 nextValue()缘琅。同樣加入 Tokenizer 類(lèi):

nextValue() {
    return this.tokens.shift();
}

保存,綠的廓推。修改第二處對(duì) tokens.shift() 的調(diào)用:

let value = schema.type.needValue() ? this.tokens.nextValue() : undefined;

保存刷袍,綠的》梗可以看到呻纹,Tokenizer.shift() 方法也被 IDE 標(biāo)記為灰色了,因?yàn)槲覀円矝](méi)有再用它了专缠。刪除該方法雷酪,保存,綠的涝婉。至此哥力,我們的 Tokenizer 初步完成了,跟 token 相關(guān)的邏輯都封裝到這個(gè)類(lèi)里面了墩弯。于是我們 F6吩跋,把它搬移到屬于它自己的文件里面吧,文件名是 main/tokenizer.js渔工。

再看 parseTokens() 方法:

parseTokens() {
    while (this.tokens.hasMore()) {
        let flag = this.tokens.nextFlag();
        let schema = this.schemas.find(s => s.flag === flag);
        let value = schema.type.needValue() ? this.tokens.nextValue() : undefined;
        let arg = this.args.find(a => a.flag === flag);
        arg.value = schema.type.convert(value);
    }
}

它還是干的兩件事情锌钮,一個(gè)是做循環(huán),一個(gè)是處理當(dāng)前取出來(lái)的參數(shù)引矩。于是轧粟,我們可以把循環(huán)的內(nèi)容單獨(dú)抽取出來(lái):

parseTokens() {
    while (this.tokens.hasMore()) this.parseToken();
}

parseToken() {
    let flag = this.tokens.nextFlag();
    let schema = this.schemas.find(s => s.flag === flag);
    let value = schema.type.needValue() ? this.tokens.nextValue() : undefined;
    let arg = this.args.find(a => a.flag === flag);
    arg.value = schema.type.convert(value);
}

保存,綠的脓魏。這下 parseTokens() 方法也很清爽了。接著我們關(guān)注 parseToken() 方法通惫∶瑁可以看到 let value = ... 這行有些長(zhǎng),而且里面包含邏輯履腋,我們把它抽取出來(lái)珊燎,就叫 nextValue() 吧:

parseToken() {
    let flag = this.tokens.nextFlag();
    let schema = this.schemas.find(s => s.flag === flag);
    let value = this.nextValue(schema);
    let arg = this.args.find(a => a.flag === flag);
    arg.value = schema.type.convert(value);
}

nextValue(schema) {
    return schema.type.needValue() ? this.tokens.nextValue() : undefined;
}

保存,綠的遵湖。再看 parseToken 的最后一行悔政,對(duì) value 做類(lèi)型轉(zhuǎn)換,也應(yīng)該是屬于 nextValue() 的一部分:

parseToken() {
    let flag = this.tokens.nextFlag();
    let schema = this.schemas.find(s => s.flag === flag);
    let value = this.nextValue(schema);
    let arg = this.args.find(a => a.flag === flag);
    arg.value = value;
}

nextValue(schema) {
    let value = schema.type.needValue() ? this.tokens.nextValue() : undefined;
    return schema.type.convert(value);
}

保存延旧,綠的谋国。可以看到 nextValue() 里面并沒(méi)有直接使用傳進(jìn)來(lái)的 schema迁沫,而是全程使用 schema.type芦瘾,既然如此捌蚊,直接傳 type 進(jìn)來(lái)就好了:

parseToken() {
    let flag = this.tokens.nextFlag();
    let schema = this.schemas.find(s => s.flag === flag);
    let value = this.nextValue(schema.type);
    let arg = this.args.find(a => a.flag === flag);
    arg.value = value;
}

nextValue(type) {
    let value = type.needValue() ? this.tokens.nextValue() : undefined;
    return type.convert(value);
}

保存,綠的近弟。這下 parseToken() 里面的 value 變量就沒(méi)有存在的必要了缅糟。選中 value 的定義,敲 Ctrl + Alt + N/Cmd + Alt + N 內(nèi)聯(lián)變量:

parseToken() {
    let flag = this.tokens.nextFlag();
    let schema = this.schemas.find(s => s.flag === flag);
    let arg = this.args.find(a => a.flag === flag);
    arg.value = this.nextValue(schema.type);
}

保存祷愉,還是綠的窗宦。繼續(xù)看 parseToken() 方法,兩個(gè) find() 調(diào)用顯得比較扎眼二鳄。其中赴涵,schemas.find() 括號(hào)里面的邏輯,應(yīng)該是規(guī)則列表的邏輯泥从,解析器本身不需要關(guān)注這個(gè)句占。同理,args.find() 括號(hào)里面的邏輯躯嫉,則應(yīng)該是參數(shù)列表的邏輯纱烘,解析器也不需要關(guān)心。這說(shuō)明我們需要一個(gè) Schemas 類(lèi),用于處理規(guī)則列表相關(guān)的邏輯皆看,和一個(gè) Arguments 類(lèi)遗锣,用于處理參數(shù)列表邏輯。而后者我們已經(jīng)有了哺壶,所以先處理這個(gè),對(duì)蜒谤,這樣難度更低山宾。

處理方式前面已經(jīng)介紹過(guò)了。先看看目前使用了 args 的哪些屬性和方法鳍徽。發(fā)現(xiàn)只用到了其 find() 方法资锰。那就先為 Arguments 類(lèi)新增一個(gè) find() 方法:

find(cb) {
    return this.items.find(cb);
}

保存,綠的阶祭。替換 ArgumentParser.createDefaultArguments() 方法中 args 的創(chuàng)建方式:

createDefaultArguments() {
    this.args = new Arguments(this.schemas.map(schema => this.getDefaultValue(schema)));
}

別忘了同時(shí)修改 parse() 方法中的返回值:

parse(commandLine) {
    this.createDefaultArguments();
    this.tokenizeCommandLine(commandLine);
    this.parseTokens();
    return this.args;
}

保存绷杜,綠的。于是我們可以修改 Arguments.find() 方法:

find(flag) {
    return this.items.find(item => item.flag === flag);
}

同時(shí)替換 ArgumentParser.parseToken() 中調(diào)用的那一行:

let arg = this.args.find(flag);

保存濒募,綠的鞭盟。接下來(lái)輪到規(guī)則列表,這個(gè)類(lèi)還沒(méi)有瑰剃,于是我們就在 ArgumentParser 類(lèi)所在的文件中創(chuàng)建這個(gè)新類(lèi):

class Schemas {

    constructor(items) {
        this.items = items;
    }

}

保存齿诉,綠的。老辦法,替換之前鹃两,看看目前用到了 schemas 的哪些屬性和方法遗座。可以看到俊扳,目前就用到 map()find() 兩個(gè)方法途蒋。先做直接傳遞,為 Schemas 類(lèi)添加兩個(gè)方法:

find(cb) {
    return this.items.find(cb);
}

map(cb) {
    return this.items.map(cb);
}

保存馋记,還是綠的号坡。修改 ArgumentParser 構(gòu)造函數(shù)的實(shí)現(xiàn):

constructor(schemas) {
    this.schemas = new Schemas(schemas);
}

保存,綠的梯醒。類(lèi)的替換完成了宽堆,可以開(kāi)始方法的替換了。其中 find() 方法比較簡(jiǎn)單茸习,和前面的 Arguments 一樣的畜隶。修改 Schemas.find() 方法:

find(flag) {
    return this.items.find(item => item.flag === flag);
}

同時(shí)替換 ArgumentParser.parseToken() 中調(diào)用的那一行:

let schema = this.schemas.find(flag);

保存,綠的号胚。接下來(lái)看看 createDefaultArguments() 方法籽慢。看起來(lái)猫胁,該方法對(duì) schemas 的使用箱亿,與解析器并沒(méi)有任何關(guān)系,我們應(yīng)該可以把相應(yīng)的實(shí)現(xiàn)都移動(dòng)到 Schemas 類(lèi)里面弃秆。不過(guò)這樣會(huì)導(dǎo)致另一個(gè)問(wèn)題届惋,那就是我們會(huì)在 Schemas 類(lèi)中創(chuàng)建 Argument 類(lèi)的對(duì)象。從設(shè)計(jì)角度看菠赚,參數(shù)信息和規(guī)則信息并沒(méi)有直接的關(guān)系脑豹,讓它們相互依賴是不合理的。于是協(xié)調(diào)兩者這種“粗重活”就落到了我們的解析器身上衡查。也就是說(shuō)這里的 schemas.map() 挪不走了瘩欺。不過(guò)為了簡(jiǎn)化調(diào)用,還是有一點(diǎn)改進(jìn)空間的峡捡。先修改 Schemas.map() 方法:

map(cb) {
    return this.items.map(item => cb(item));
}

然后修改 createDefaultArguments() 的實(shí)現(xiàn):

createDefaultArguments() {
    this.args = new Arguments(this.schemas.map(this.getDefaultValue));
}

保存,綠的筑悴。嗯们拙,比之前省了四分之一的長(zhǎng)度。這個(gè) getDefaultValue() 感覺(jué)不是很貼切了阁吝,它的作用其實(shí)就是創(chuàng)建對(duì)應(yīng)的 Argument砚婆,使用默認(rèn)值只是創(chuàng)建它的實(shí)現(xiàn)邏輯。所以我們用 Shift + F6 將其改名為 createArgument。保存装盯,還是綠的坷虑。

再看看現(xiàn)在的 parseToken()

parseToken() {
    let flag = this.tokens.nextFlag();
    let schema = this.schemas.find(flag);
    let arg = this.args.find(flag);
    arg.value = this.nextValue(schema.type);
}

后兩行,把 arg 找出來(lái)埂奈,再給它賦值迄损,其實(shí)可以一步完成的,沒(méi)有必要分成兩步账磺,何況 arg 后面也只用了這一次芹敌。為 Arguments 新增 set() 方法:

set(flag, value) {
    this.find(flag).value = value;
}

保存,綠的垮抗。修改 parseToken()

parseToken() {
    let flag = this.tokens.nextFlag();
    let schema = this.schemas.find(flag);
    this.args.set(flag, this.nextValue(schema.type));
}

保存氏捞,綠的。感覺(jué)仍然不是很爽冒版,nextValue() 的兩個(gè)參數(shù)一個(gè)是使用變量液茎,一個(gè)是調(diào)用方法,看起來(lái)不一致辞嗡。選中第二個(gè)參數(shù)捆等,敲 Ctrl + Alt + V/Cmd + Alt + V 抽取變量,命名為 value

parseToken() {
    let flag = this.tokens.nextFlag();
    let schema = this.schemas.find(flag);
    let value = this.nextValue(schema.type);
    this.args.set(flag, value);
}

保存欲间,綠的楚里。Ok,這個(gè)方法也清爽了猎贴。剛才修改 Arguments 類(lèi)的時(shí)候看到 get()find() 方法存在重復(fù)代碼:

find(flag) {
    return this.items.find(item => item.flag === flag);
}

get(flag) {
    return this.items.find(item => item.flag === flag).value;
}

很明顯班缎,讓 get() 直接使用 find() 就可以了:

get(flag) {
    return this.find(flag).value;
}

保存,綠的她渴。接下來(lái)达址,把 Schemas 類(lèi)移動(dòng)到新建的 main/schemas.js 文件里面,保存趁耗,綠的沉唠。

現(xiàn)在,我們的 ArgumentParser 就是這個(gè)樣子:

import { Arguments } from './arguments';
import { Argument } from './argument';
import { Tokenizer } from './tokenizer';
import { Schemas } from './schemas';

export class ArgumentParser {

    constructor(schemas) {
        this.schemas = new Schemas(schemas);
    }

    parse(commandLine) {
        this.createDefaultArguments();
        this.tokenizeCommandLine(commandLine);
        this.parseTokens();
        return this.args;
    }

    createDefaultArguments() {
        this.args = new Arguments(this.schemas.map(this.createArgument));
    }

    createArgument(schema) {
        return new Argument(schema.flag, schema.type.default());
    }

    tokenizeCommandLine(commandLine) {
        this.tokens = new Tokenizer(commandLine);
    }

    parseTokens() {
        while (this.tokens.hasMore()) this.parseToken();
    }

    parseToken() {
        let flag = this.tokens.nextFlag();
        let schema = this.schemas.find(flag);
        let value = this.nextValue(schema.type);
        this.args.set(flag, value);
    }

    nextValue(type) {
        let value = type.needValue() ? this.tokens.nextValue() : undefined;
        return type.convert(value);
    }

}

好了苛败,拆出來(lái)的兩個(gè)小需求也做完了满葛,繼續(xù)拆吧。兩個(gè)參數(shù)類(lèi)型一致的情況已經(jīng)覆蓋到了罢屈,接下來(lái)自然是類(lèi)型不一致的情況嘀韧。那么,一個(gè)整型和一個(gè)字符串型缠捌,有必要嗎锄贷?沒(méi)有,在類(lèi)型轉(zhuǎn)換已經(jīng)覆蓋到了的情況下,一個(gè)整型和一個(gè)字符串型谊却,跟兩個(gè)整型柔昼,沒(méi)有區(qū)別。所以炎辨,最好是需要參數(shù)值和不需要參數(shù)值的組合捕透。整型加布爾型,以及布爾型加整型:

  • 處理 2 個(gè)參數(shù)
    • 處理 2 個(gè)整數(shù)型的參數(shù)
    • 處理 2 個(gè)布爾型的參數(shù)
    • 處理 1 個(gè)整型和 1 個(gè)布爾型的參數(shù)
    • 處理 1 個(gè)布爾型和 1 個(gè)整型的參數(shù)

好蹦魔,來(lái)個(gè)失敗的測(cè)試:

it('處理 1 個(gè)整型和 1 個(gè)布爾型的參數(shù)', () => {
    let schemas = [IntegerSchema('p'), BooleanSchema('d')];
    let parser = new ArgumentParser(schemas);

    let result = parser.parse('-p 8080 -d');

    expect(result.get('p')).toEqual(8080);
    expect(result.get('d')).toEqual(true);
});

保存激率,紅……咦?怎么沒(méi)變紅勿决,還是綠的乒躺?因?yàn)椤材愕退酰@個(gè)需求已經(jīng)完成了嘉冒。無(wú)論你信不信,雖然我們搞了很多假實(shí)現(xiàn)和傻實(shí)現(xiàn)咆繁,而且每一步都小得很娘炮讳推,不過(guò),我們確實(shí)已經(jīng)把參數(shù)解析的正常業(yè)務(wù)邏輯做完了玩般。

那么問(wèn)題來(lái)了银觅,這個(gè)測(cè)試還有沒(méi)有必要保留呢?當(dāng)然有了坏为,測(cè)試可以用來(lái)驅(qū)動(dòng)實(shí)現(xiàn)究驴,這是其重要作用之一,但不是全部匀伏。用來(lái)驗(yàn)證需求洒忧,充當(dāng)安全網(wǎng),才是其最根本的作用够颠。雖然這個(gè)需求所要求的實(shí)現(xiàn)代碼熙侍,已經(jīng)被前面的測(cè)試覆蓋到了。但是履磨,這是基于我們目前的實(shí)現(xiàn)方式蛉抓,指不定以后哪天,另一位同學(xué)接手這份代碼剃诅,來(lái)了一個(gè)大大的“重構(gòu)”巷送,恰好能通過(guò)前面的所有測(cè)試,但是無(wú)法通過(guò)這個(gè)測(cè)試呢综苔?那它就幫助我們避免了一個(gè) bug惩系。

有同學(xué)要問(wèn)了,如果按照這個(gè)邏輯如筛,那我們還可以再寫(xiě) 100 個(gè)測(cè)試堡牡,怎樣才算完呢?這是個(gè)好問(wèn)題杨刨,目前業(yè)界對(duì)此的答案是:“看信心”晤柄。也就是說(shuō),寫(xiě)到——你認(rèn)為覆蓋到了足夠多的情況妖胀,因而不會(huì)出錯(cuò)了——為止芥颈。哈哈,這聽(tīng)起來(lái)有點(diǎn)玄學(xué)的味道赚抡。不過(guò)也還是有跡可循的爬坑,通常,把主要業(yè)務(wù)邏輯涂臣、異常情況覆蓋到以后盾计,再加上一些邊界條件的測(cè)試,基本上就差不多了赁遗。這些都有了署辉,就可以自由發(fā)揮了,不過(guò)別發(fā)揮太多就好岩四。

所以哭尝,新加的這兩個(gè)小任務(wù),是屬于“自由發(fā)揮”的嗎剖煌?你猜呢材鹦?:)其實(shí)不是的,這是我們?cè)诟采w邊界條件末捣。雖然我們的實(shí)現(xiàn)邏輯是一次“吃”一個(gè) token侠姑,但不排除將來(lái)想要做重構(gòu)的同學(xué)修改這個(gè)實(shí)現(xiàn)方式。如果有人“聰明”的提前把拆出來(lái)的 token 兩兩分組箩做,以方便進(jìn)一步處理莽红,那就會(huì)在處理布爾型參數(shù)的時(shí)候遇到問(wèn)題。所以邦邦,把需要參數(shù)值的(整型安吁、字符串型)參數(shù),和不需要參數(shù)值的(布爾型)參數(shù)混排燃辖,就是用來(lái)覆蓋這個(gè)邊界條件的鬼店。

說(shuō)到這里,其實(shí)還有一個(gè)邊界條件我們沒(méi)有覆蓋的黔龟,那就是我們沒(méi)有針對(duì)整型參數(shù)的傳值是負(fù)數(shù)的情況進(jìn)行測(cè)試妇智。為什么這是一個(gè)邊界條件滥玷?首先,對(duì)于整數(shù)而言巍棱,正數(shù)惑畴、零、負(fù)數(shù)航徙,都是屬于邊界條件(用測(cè)試語(yǔ)言說(shuō)如贷,叫做等價(jià)類(lèi) [6])。其次到踏,負(fù)數(shù)的負(fù)號(hào)杠袱,和我們參數(shù)標(biāo)志前面的“杠”,是同一個(gè)字符窝稿。所以如果將來(lái)有同學(xué)做重構(gòu)楣富,把拆分命令行的現(xiàn)有代碼實(shí)現(xiàn),改為用正則表達(dá)式進(jìn)行匹配拆分伴榔,就有可能把負(fù)數(shù)前面的負(fù)號(hào)給吃掉菩彬。所以,負(fù)數(shù)也是需要覆蓋到的潮梯,我們后面找個(gè)用例順便覆蓋一下就好了骗灶。

那么,我們可以開(kāi)始下一個(gè)小任務(wù)了嗎秉馏?別急耙旦,注意看測(cè)試代碼,第二顆子彈已經(jīng)來(lái)了萝究。三個(gè)新的測(cè)試用例免都,一如既往的充斥著重復(fù)代碼,仍然需要抽取公共方法帆竹。手法都是前面介紹過(guò)的绕娘,這里就不展開(kāi)了,直接看重構(gòu)結(jié)果栽连。公共方法:

function testMultipleArguments(schemaTypes, flags, commandLine, expectedValues) {
    let schemas = schemaTypes.map((schemaType, i) => schemaType(flags[i]));
    let parser = new ArgumentParser(schemas);

    let result = parser.parse(commandLine);

    expectedValues.forEach(
        (expectedValue, i) => expect(result.get(flags[i])).toEqual(expectedValue),
    );
}

測(cè)試用例:

it('處理 2 個(gè)整數(shù)型的參數(shù)', () => {
    testMultipleArguments([IntegerSchema, IntegerSchema], ['p', 'q'],
        '-p 8080 -q 9527', [8080, 9527]);
});

it('處理 2 個(gè)布爾型的參數(shù)', () => {
    testMultipleArguments([BooleanSchema, BooleanSchema], ['d', 'e'],
        '-d -e', [true, true]);
});

it('處理 1 個(gè)整型和 1 個(gè)布爾型的參數(shù)', () => {
    testMultipleArguments([IntegerSchema, BooleanSchema], ['p', 'd'],
        '-p 8080 -d', [8080, true]);
});

保存险领,綠的。重復(fù)代碼是沒(méi)有了秒紧,不過(guò)測(cè)試用例里面的代碼看起來(lái)還是很詭異啊绢陌。傳給 testMultipleArguments() 的的參數(shù)一共四個(gè),其中第一第二和第四個(gè)都是數(shù)組熔恢,第三個(gè)是字符串脐湾。這個(gè)調(diào)用看起來(lái)很不直觀,而越不直觀的代碼叙淌,維護(hù)起來(lái)越困難秤掌,而且還容易出錯(cuò)愁铺。那怎么改呢?可以看到傳的三個(gè)數(shù)組其實(shí)是一一對(duì)應(yīng)的闻鉴,所以我們可以把三個(gè)數(shù)組合并為一個(gè)數(shù)組帜讲。引入一個(gè)簡(jiǎn)單對(duì)象作為數(shù)組的成員,這個(gè)對(duì)象包含三個(gè)屬性椒拗,分別代表之前的三個(gè)數(shù)組的含義:規(guī)則類(lèi)型、參數(shù)標(biāo)志以及期待值获黔。調(diào)整之后的公共方法:

function testMultipleArguments(commandLine, params) {
    let schemas = params.map(param => param.type(param.flag));
    let parser = new ArgumentParser(schemas);

    let result = parser.parse(commandLine);

    params.forEach((param) => {
        let { flag, value } = param;
        expect(result.get(flag)).toEqual(value);
    });
}

測(cè)試用例:

it('處理 2 個(gè)整數(shù)型的參數(shù)', () => {
    testMultipleArguments('-p 8080 -q 9527', [
        { type: IntegerSchema, flag: 'p', value: 8080 },
        { type: IntegerSchema, flag: 'q', value: 9527 },
    ]);
});

it('處理 2 個(gè)布爾型的參數(shù)', () => {
    testMultipleArguments('-d -e', [
        { type: BooleanSchema, flag: 'd', value: true },
        { type: BooleanSchema, flag: 'e', value: true },
    ]);
});

it('處理 1 個(gè)整型和 1 個(gè)布爾型的參數(shù)', () => {
    testMultipleArguments('-p 8080 -d', [
        { type: IntegerSchema, flag: 'p', value: 8080 },
        { type: BooleanSchema, flag: 'd', value: true },
    ]);
});

保存蚀苛,綠的。這下測(cè)試用例看起來(lái)清楚多了玷氏。接下來(lái)需要把三個(gè)公共函數(shù)也整理一下堵未。首先,testSingleArgument() 很顯然是屬于 testMultipleArguments() 的特殊情況盏触,直接調(diào)用即可省掉重復(fù)代碼:

function testSingleArgument(schemaType, flag, commandLine, expectedValue) {
    testMultipleArguments(commandLine, [
        { type: schemaType, flag, value: expectedValue },
    ]);
}

保存渗蟹,綠的。既然這個(gè)方法只是一個(gè)二傳手赞辩,不如直接把它干掉雌芽,還能省下不少代碼。光標(biāo)定位到 testSingleArgument() 的定義處辨嗽,敲 Ctrl + Alt + N/Cmd + Alt + N 內(nèi)聯(lián)函數(shù)世落。干掉一個(gè)了。接下來(lái)看看 testDefaultValue()糟需,同樣也只是個(gè)二傳手屉佳,照樣把它也給內(nèi)聯(lián)了吧。最后公共方法就只剩下了 testMultipleArguments() 這一個(gè)了洲押。在沒(méi)有另外兩個(gè)公共方法(的名字)的“襯托”下武花,這個(gè)名字就不是很貼切了。咱們 F6杈帐,給它改成 testParse() 吧体箕,于是測(cè)試用例部分的代碼就變成了:

describe('ArgumentParser', () => {

    describe('處理默認(rèn)參數(shù)', () => {

        it('處理布爾型參數(shù)的默認(rèn)值', () => {
            testParse('', [
                { type: BooleanSchema, flag: 'd', value: false },
            ]);
        });

        it('處理字符串型參數(shù)的默認(rèn)值', () => {
            testParse('', [
                { type: StringSchema, flag: 'l', value: '' },
            ]);
        });

        it('處理整數(shù)型參數(shù)的默認(rèn)值', () => {
            testParse('', [
                { type: IntegerSchema, flag: 'p', value: 0 },
            ]);
        });

    });

    describe('處理 1 個(gè)參數(shù)', () => {

        it('處理布爾型參數(shù)', () => {
            testParse('-d', [
                { type: BooleanSchema, flag: 'd', value: true },
            ]);
        });

        it('處理字符串型參數(shù)', () => {
            testParse('-l /usr/logs', [
                { type: StringSchema, flag: 'l', value: '/usr/logs' },
            ]);
        });

        it('處理整數(shù)型參數(shù)', () => {
            testParse('-p 8080', [
                { type: IntegerSchema, flag: 'p', value: 8080 },
            ]);
        });

    });

    describe('處理 2 個(gè)參數(shù)', () => {

        it('處理 2 個(gè)整數(shù)型的參數(shù)', () => {
            testParse('-p 8080 -q 9527', [
                { type: IntegerSchema, flag: 'p', value: 8080 },
                { type: IntegerSchema, flag: 'q', value: 9527 },
            ]);
        });

        it('處理 2 個(gè)布爾型的參數(shù)', () => {
            testParse('-d -e', [
                { type: BooleanSchema, flag: 'd', value: true },
                { type: BooleanSchema, flag: 'e', value: true },
            ]);
        });

        it('處理 1 個(gè)整型和 1 個(gè)布爾型的參數(shù)', () => {
            testParse('-p 8080 -d', [
                { type: IntegerSchema, flag: 'p', value: 8080 },
                { type: BooleanSchema, flag: 'd', value: true },
            ]);
        });

    });

});

保存,綠的挑童。Neat干旁!測(cè)試代碼也很清楚吧。這份測(cè)試代碼好維護(hù)嗎炮沐?加入更多用例簡(jiǎn)單嗎争群?不言而喻的吧。所以大年,如果有人說(shuō)“由于 XXX 的原因换薄,測(cè)試代碼很難維護(hù)”(XXX 這個(gè)變量你隨便填)玉雾,你就知道了,這個(gè)同學(xué)不會(huì)做重構(gòu)轻要。

接下來(lái)可以加入下一個(gè)任務(wù)的測(cè)試了:

it('處理 1 個(gè)布爾型和 1 個(gè)整型的參數(shù)', () => {
    testParse('-d -p 8080', [
        { type: BooleanSchema, flag: 'd', value: true },
        { type: IntegerSchema, flag: 'p', value: 8080 },
    ]);
});

保存复旬,仍然是綠的,沒(méi)毛病冲泥,說(shuō)過(guò)嘛驹碍,已經(jīng)實(shí)現(xiàn)了的。第三個(gè)大任務(wù)搞定凡恍,休息一下吧志秃。

第四第五兩個(gè)大任務(wù)

歡迎回來(lái)。有了前面的鋪墊嚼酝,剩下的工作就簡(jiǎn)單很多了浮还。別誤會(huì),前面的工作也很簡(jiǎn)單闽巩,對(duì)吧钧舌,每個(gè)步驟都簡(jiǎn)單,是我們 TDD 的原則涎跨。只是由于大家應(yīng)該已經(jīng)熟悉 TDD 相關(guān)的流程了洼冻,所以,接下來(lái)隅很,操作方面會(huì)講得簡(jiǎn)單一些碘赖,而思考層面的內(nèi)容,還是會(huì)一如既往進(jìn)行詳細(xì)的闡述外构。

接下來(lái)是“處理 3 個(gè)參數(shù)”這個(gè)大任務(wù)普泡。這是屬于正常業(yè)務(wù)邏輯,如前所述审编,正常的業(yè)務(wù)邏輯撼班,我們都已經(jīng)實(shí)現(xiàn)完了。再加上異常邏輯是由下一個(gè)大任務(wù)所覆蓋的垒酬。所以砰嘁,現(xiàn)在,我們只需要覆蓋一些邊界條件勘究,加上一點(diǎn)點(diǎn)自由發(fā)揮矮湘,就可以了。

邊界條件嘛口糕,前面也講過(guò)缅阳,我們把布爾型的放在中間啦撮,外面分別用整型和字符串型包裹鞭执,就是一個(gè)用例了万哪。此外嗅绰,前面說(shuō)了,還需要覆蓋負(fù)數(shù)向族,那我們就用一個(gè)負(fù)數(shù)呵燕,再把布爾型放在末尾。再來(lái)一個(gè)有傳值和沒(méi)傳值混合的件相。嗯再扭,差不多了:

  • 處理 3 個(gè)參數(shù)
    • 處理 1 個(gè)整型、1 個(gè)布爾型和 1 個(gè)字符串型參數(shù)
    • 處理 1 個(gè)負(fù)數(shù)夜矗、1 個(gè)字符串型和 1 個(gè)布爾型參數(shù)
    • 處理 1 個(gè)布爾型泛范、1 個(gè)字符串型和 1 個(gè)未傳的整型參數(shù)

寫(xiě)測(cè)試:

describe('處理 3 個(gè)參數(shù)', () => {

    it('處理 1 個(gè)整型、1 個(gè)布爾型和 1 個(gè)字符串型參數(shù)', () => {
        testParse('-p 8080 -d -s /usr/logs', [
            { type: IntegerSchema, flag: 'p', value: 8080 },
            { type: BooleanSchema, flag: 'd', value: true },
            { type: StringSchema, flag: 's', value: '/usr/logs' },
        ]);
    });

});

保存侯养,綠的。下一個(gè)測(cè)試:

it('處理 1 個(gè)負(fù)數(shù)澄干、1 個(gè)字符串型和 1 個(gè)布爾型參數(shù)', () => {
    testParse('-q -9527 -s /usr/logs -d', [
        { type: IntegerSchema, flag: 'q', value: -9527 },
        { type: StringSchema, flag: 's', value: '/usr/logs' },
        { type: BooleanSchema, flag: 'd', value: true },
    ]);
});

保存逛揩,綠的。再下一個(gè)測(cè)試:

it('處理 1 個(gè)布爾型麸俘、1 個(gè)字符串型和 1 個(gè)未傳的整型參數(shù)', () => {
    testParse('-d -s /usr/logs', [
        { type: IntegerSchema, flag: 'p', value: 0 },
        { type: BooleanSchema, flag: 'd', value: true },
        { type: StringSchema, flag: 's', value: '/usr/logs' },
    ]);
});

保存辩稽,還是綠的,又完成一個(gè)大任務(wù)从媚〕研梗看看生產(chǎn)代碼有沒(méi)有需要重構(gòu)的,暫時(shí)沒(méi)有拜效。再看看測(cè)試代碼有沒(méi)有需要重構(gòu)的喷众,也沒(méi)有。

我們可以開(kāi)始下一個(gè)大任務(wù)了:“處理異常情況”紧憾。仍然是先拆任務(wù)到千。要處理異常情況,最好是先看看赴穗,異常情況是誰(shuí)引入的憔四。對(duì),先把人(角色)找到般眉,然后再?gòu)娜说慕嵌确治隽苏裕悬c(diǎn)類(lèi)似用戶畫(huà)像,這樣做比較不容易出現(xiàn)遺漏甸赃。對(duì)于我們這里的業(yè)務(wù)柿汛,引入異常的人可以分為兩類(lèi):我們的用戶,以及我們用戶的用戶埠对。當(dāng)然苛茂,還有我們自己已烤,不過(guò) TDD 的流程保證了,我們自己不會(huì)引入異常(否則測(cè)試無(wú)法通過(guò))妓羊,所以這里就不用考慮我們自己了胯究。我們是寫(xiě)解析器的人,我們的用戶躁绸,就是寫(xiě)應(yīng)用程序(比如前面提到的“網(wǎng)絡(luò)服務(wù)器程序”)的人裕循。而我們用戶的用戶,就是使用那些應(yīng)用程序的人净刮。

我們的用戶剥哑,可能引入哪些異常?首先淹父,最好的方案自然是讓他們沒(méi)有機(jī)會(huì)引入異常株婴。他們一旦使用上出現(xiàn)問(wèn)題,IDE 或者編譯器能告訴他們暑认,那么就沒(méi)有機(jī)會(huì)引入異常困介,這是最理想的。相應(yīng)的設(shè)計(jì)蘸际,我們?cè)谇懊鏈贤ㄐ枨蟮牡胤揭呀?jīng)討論過(guò)了座哩。當(dāng)然,現(xiàn)實(shí)情況通常都不那么理想粮彤,所以根穷,還是會(huì)異常需要處理的。我們的解析器類(lèi)导坟,一共有兩個(gè)輸入?yún)?shù)屿良,一個(gè)是規(guī)則定義,一個(gè)是命令行惫周。這兩個(gè)參數(shù)都有可能引入異常管引。

由于我們這里用的 JavaScript 是動(dòng)態(tài)類(lèi)型的語(yǔ)言,所以會(huì)有一個(gè)共性的問(wèn)題:類(lèi)型安全問(wèn)題闯两。我們的規(guī)則參數(shù)是接收一個(gè) Schema 的數(shù)組褥伴,要是人家傳過(guò)來(lái)的不是數(shù)組呢?要是數(shù)組里面的元素不是 Schema 的實(shí)例呢漾狼?如果創(chuàng)建規(guī)則的時(shí)候重慢,傳進(jìn)去的 flag 的長(zhǎng)度不是一個(gè)字符呢?如果傳進(jìn)來(lái)的命令行不是一個(gè)字符串呢(記得我們對(duì)它調(diào)用了 split() 方法吧)逊躁?

雖然這些問(wèn)題對(duì)應(yīng)靜態(tài)類(lèi)型的語(yǔ)言(比如 TypeScript似踱、Java)都不是問(wèn)題,不過(guò),嘿核芽,面對(duì)現(xiàn)實(shí)囚戚。如果你想寫(xiě)一個(gè)靠譜的第三方組件,那么這些情況是必須要考慮的轧简。插播一個(gè)暴露年齡的段子:屏幕上有一行提示:“請(qǐng)用鼠標(biāo)點(diǎn)擊這里開(kāi)始”驰坊,于是用戶抓起鼠標(biāo),輕輕的在了屏幕左下角敲了一下哮独。這個(gè)故事告訴我們拳芙,當(dāng)你考慮異常情況的時(shí)候,一定要假設(shè)你的用戶都是白癡皮璧!否則舟扎,今天挖的坑,將來(lái)都是要填的悴务,也許是你相親那天睹限,也許是某天凌晨?jī)牲c(diǎn)四十二分三十九秒,誰(shuí)知道呢讯檐。

所以羡疗,你知道了,如果哪天你發(fā)現(xiàn)一個(gè)特別好用的軟件裂垦,那么其實(shí)你已經(jīng)……被他們白癡了無(wú)數(shù)次了:)

由于類(lèi)型安全處理的實(shí)現(xiàn)非常簡(jiǎn)單顺囊,并且 Java 的同學(xué)也用不上肌索,考慮到篇幅問(wèn)題蕉拢,我們就不在本文中做展開(kāi)了。有興趣的同學(xué)诚亚,可以把這個(gè)當(dāng)做練習(xí)題晕换。

再看我們用戶的用戶,他們可能引入哪些異常站宗?謝天謝地闸准,他們只能影響我們的 commandLine 這個(gè)參數(shù)的內(nèi)容。想想他們的使用場(chǎng)景梢灭,他們只是在命令行里夷家,敲擊鍵盤(pán),啟動(dòng)由我們的用戶所制作的軟件敏释。沒(méi)有 IDE 的協(xié)助库快,敲錯(cuò)字母是很常見(jiàn)的情況吧。比如钥顽,本來(lái)要敲 -v 的义屏,手一抖,就敲成了 -b,而我們的用戶(應(yīng)用程序的開(kāi)發(fā)者)闽铐,壓根就沒(méi)有定義 b 這個(gè)規(guī)則蝶怔。于是,我們的第一個(gè)任務(wù)就有了:

  • 處理異常情況
    • 處理規(guī)則未定義的情況

既然標(biāo)志可能敲錯(cuò)兄墅,那么值也有可能敲錯(cuò)踢星。字符串型的值是不怕錯(cuò)的,布爾型本來(lái)就沒(méi)有值察迟,所以斩狱,最常見(jiàn)的是整型的值敲錯(cuò)了,比如一不小心混了個(gè)字母進(jìn)去扎瓶。這就是第二個(gè)任務(wù)了:

  • 處理異常情況
    • 處理規(guī)則未定義的情況
    • 處理整型參數(shù)的值不合法的情況

剛才說(shuō)“布爾型本來(lái)就沒(méi)有值”所踊,可別輕易放過(guò)了,這也有可能引入異常的哦概荷。對(duì)呀秕岛,不該傳值的時(shí)候,傳了值進(jìn)來(lái)误证,也是問(wèn)題:

  • 處理異常情況
    • 處理規(guī)則未定義的情況
    • 處理整型參數(shù)的值不合法的情況
    • 處理傳了多余的值的情況

既然有多傳值的情況继薛,就可能有少傳值的情況:

  • 處理異常情況
    • 處理規(guī)則未定義的情況
    • 處理整型參數(shù)的值不合法的情況
    • 處理傳了多余的值的情況
    • 處理字符串型參數(shù)沒(méi)有傳值的情況

嗯,任務(wù)拆得差不多了愈捅,可以開(kāi)始編碼了遏考。在做第一個(gè)小任務(wù)之前,還得先細(xì)化一下蓝谨,這個(gè)用例怎么寫(xiě)灌具。命令行里面?zhèn)魅胍粋€(gè) -b,而規(guī)則列表為空譬巫,就可以了咖楣,這樣最簡(jiǎn)單。一旦解析器發(fā)現(xiàn)這個(gè)問(wèn)題芦昔,就應(yīng)該拋出一個(gè)異常诱贿,以提醒我們的用戶。還有一個(gè)很重要的事情咕缎,就是異常信息(出錯(cuò)提示)的選擇珠十。時(shí)刻記住,用戶角度凭豪,你的這個(gè)信息焙蹭,必須對(duì)用戶修正問(wèn)題有足夠的幫助。要是你只給個(gè)“出錯(cuò)啦”一類(lèi)的信息墅诡,用戶會(huì)來(lái)薅你頭發(fā)壳嚎,你信么:)所以桐智,這里既然是參數(shù)未定義的問(wèn)題,就一定要清楚的告知用戶烟馅,某某參數(shù)是未定義的说庭。

先來(lái)一個(gè)失敗的測(cè)試:

describe('處理異常情況', () => {

    it('處理規(guī)則未定義的情況', () => {
        let schemas = [];
        let parser = new ArgumentParser(schemas);
        let commandLine = '-b';

        expect(() => parser.parse(commandLine)).toThrow('Unknown flag: -b');
    });

});

保存,紅的郑趁。注意刊驴,這里出現(xiàn)一個(gè)新用法:expect(fn).toThrow(error)。就是執(zhí)行 fn 這個(gè)函數(shù)的時(shí)候寡润,必須出現(xiàn)包含 error 信息的異常捆憎。如果沒(méi)有出現(xiàn)異常,或者出現(xiàn)的異常中不包含 error 所指明的信息梭纹,測(cè)試就會(huì)不通過(guò)躲惰。注意,只能傳函數(shù)的名字進(jìn)去变抽,隨后 expect() 會(huì)幫我們執(zhí)行這個(gè)函數(shù)础拨,并自動(dòng)截獲相應(yīng)的異常。所以我們這里使用了一個(gè)匿名函數(shù)绍载,因?yàn)槟銢](méi)法直接給這個(gè)函數(shù)傳遞參數(shù)诡宗。如果你寫(xiě) expect(parser.parse(commandLine)),那就是把函數(shù)調(diào)用的結(jié)果(Arguments击儡,而非函數(shù)本身)傳給 expect() 了塔沃。而你的 parse() 一旦出現(xiàn)異常,expect() 就沒(méi)有機(jī)會(huì)執(zhí)行了(因?yàn)閳?zhí)行一個(gè)函數(shù)之前阳谍,得先把它需要的參數(shù)全部準(zhǔn)備好蛀柴,如果在準(zhǔn)備參數(shù)的過(guò)程中出現(xiàn)異常,那么這個(gè)函數(shù)是沒(méi)有機(jī)會(huì)執(zhí)行的)边坤,它也就沒(méi)有機(jī)會(huì)幫你捕獲這個(gè)異常了名扛。

此外谅年,前面不是說(shuō)出錯(cuò)提示是“未定義”嗎茧痒,怎么這里用的“Unknown”(未知)呢?主要是考慮到融蹂,用戶有可能直接把這個(gè)出錯(cuò)提示展示給他們的用戶旺订。那么“未知”這個(gè)說(shuō)法,對(duì)用戶的用戶會(huì)更友好一些超燃。還記得吧区拳,用戶視角。

好意乓,我們盡快讓它通過(guò)樱调。邏輯很簡(jiǎn)單,通過(guò)標(biāo)志去找規(guī)則的時(shí)候,如果找不到就拋出異常笆凌。修改 Schemas.find() 方法圣猎,選中 return 之后的內(nèi)容,Ctrl + Alt + V/Cmd + Alt + V抽取變量乞而,命名為 found送悔,然后中間加入一行判斷即可:

find(flag) {
    let found = this.items.find(item => item.flag === flag);
    if (!found) throw new Error(`Unknown flag: -b`);
    return found;
}

保存,綠了爪模。重構(gòu)欠啤,很明顯,為了快速通過(guò)測(cè)試屋灌,我們的出錯(cuò)提示是寫(xiě)死的洁段,現(xiàn)在把它寫(xiě)活:

if (!found) throw new Error(`Unknown flag: -${flag}`);

保存,綠的共郭。下一個(gè):“處理整型參數(shù)的值不合法的情況”眉撵。先假裝不會(huì)有其他問(wèn)題,主要還是關(guān)注出錯(cuò)提示的合理性落塑。失敗的測(cè)試:

it('處理整型參數(shù)的值不合法的情況', () => {
    let schemas = [IntegerSchema('p')];
    let parser = new ArgumentParser(schemas);
    let commandLine = '-p 123a';

    expect(() => parser.parse(commandLine)).toThrow('Invalid integer: 123a');
});

保存纽疟,紅了。不過(guò)憾赁,注意看出錯(cuò)提示污朽,紅歸紅,不是我們期待的出錯(cuò)信息不正確的紅龙考,而是“Received function did not throw”蟆肆,即指定方法并未拋出異常。這里就要了解我們的實(shí)現(xiàn)了晦款,我們用了 parseInt() 對(duì)字符串進(jìn)行解析炎功。而這個(gè)工具函數(shù),有一定的包容性缓溅,你傳 '123a456' 給它蛇损,它能給你吐出 123 來(lái);如果給它 'a456'坛怪,則會(huì)返回給你 NaN淤齐。所以,數(shù)字合法性得我們自己來(lái)進(jìn)行驗(yàn)證袜匿,方法也很簡(jiǎn)單更啄,上個(gè)正則就搞定了。那么判斷放在哪里呢居灯?自然是放在對(duì)應(yīng)的 IntegerArgumentType.convert() 里面了祭务,它負(fù)責(zé)類(lèi)型轉(zhuǎn)換嘛内狗。還記得吧,要快速通過(guò)义锥,所以我們依葫蘆畫(huà)瓢其屏,提示信息先繼續(xù)寫(xiě)死:

static convert(value) {
    if (!value.match(/^[-]?\d+$/)) throw new Error(`Invalid integer: 123a`);
    return parseInt(value, 10);
}

保存,綠了缨该。重構(gòu)出錯(cuò)提示:

if (!value.match(/^[-]?\d+$/)) throw new Error(`Invalid integer: ${value}`);

保存偎行,仍然是綠的。應(yīng)該能記住這個(gè)節(jié)奏了吧贰拿,一定要快速變綠蛤袒,然后重構(gòu)。現(xiàn)在夠好了沒(méi)膨更?不妙真,還不夠。我們?cè)倏纯催@個(gè)出錯(cuò)提示:“Invalid integer: 123a”荚守,設(shè)想一下珍德,如果你是我們用戶的用戶。你現(xiàn)在要啟動(dòng)一個(gè)程序矗漾,傳了一堆參數(shù)锈候,其中某一個(gè)參數(shù)的值出錯(cuò)了,如果手上只有這個(gè)信息敞贡,能不能幫助你快速定位到問(wèn)題并修正泵琳?能幫你找到信息,但是誊役,如果要快获列,那么應(yīng)該有更豐富的信息。很顯然蛔垢,參數(shù)值和參數(shù)標(biāo)志是成對(duì)出現(xiàn)的击孩,如果不僅有參數(shù)值,而且還有參數(shù)標(biāo)志鹏漆,可以幫助我們用戶的用戶更加方便的找到出問(wèn)題的地方巩梢,并進(jìn)行修正。

按照這個(gè)思路甫男,我們修改一下測(cè)試:

expect(() => parser.parse(commandLine)).toThrow('Invalid integer of flag -p: 123a');

保存且改,紅了验烧。好板驳,讓它快速通過(guò):

if (!value.match(/^[-]?\d+$/)) throw new Error(`Invalid integer of flag -p: ${value}`);

保存,綠了碍拆。接下來(lái)重構(gòu)它若治。我們看到慨蓝,需要的這個(gè) -p,我們的 convert() 方法是沒(méi)有的端幼,需要從外面?zhèn)鬟M(jìn)來(lái)礼烈。喲,要改接口婆跑,這得要花一袋煙的功夫吧此熬。為了改個(gè)出錯(cuò)提示,這么折騰滑进,值得嗎犀忱?值得!為了讓用戶方便扶关,我們自己麻煩一點(diǎn)點(diǎn)阴汇,是值得的,讓用戶方便正是我們存在的價(jià)值嘛节槐。何況我們有測(cè)試保駕護(hù)航搀庶,怕個(gè)毛線啊,隨便改:)先修改 ArgumentParser铜异,把 flag 一路傳進(jìn) convert()

parseToken() {
    let flag = this.tokens.nextFlag();
    let schema = this.schemas.find(flag);
    let value = this.nextValue(schema.type, flag);
    this.args.set(flag, value);
}

nextValue(type, flag) {
    let value = type.needValue() ? this.tokens.nextValue() : undefined;
    return type.convert(value, flag);
}

保存哥倔,還是綠的。接著修改 IntegerArgumentType.convert()

static convert(value, flag) {
    if (!value.match(/^[-]?\d+$/)) throw new Error(`Invalid integer of flag -${flag}: ${value}`);
    return parseInt(value, 10);
}

保存揍庄,綠的未斑。下一個(gè):“處理傳了多余的值的情況”。來(lái)個(gè)失敗的測(cè)試:

it('處理傳了多余的值的情況', () => {
    let schemas = [BooleanSchema('d')];
    let parser = new ArgumentParser(schemas);
    let commandLine = '-d hello';

    expect(() => parser.parse(commandLine)).toThrow('Unexpected value: hello');
});

保存币绩,紅了蜡秽。接下來(lái)讓它通過(guò)。接下來(lái)需要稍微動(dòng)點(diǎn)腦子缆镣,否則就算你想寫(xiě)假實(shí)現(xiàn)芽突,都不知道該往哪里加。所謂“多余的值”董瞻,實(shí)際上就是寞蚌,該“吃”的值吃完了,還剩下一個(gè)值在我們的 Tokenizer 里面钠糊。而值“吃”完了挟秤,接下來(lái)該吃什么?該“吃”標(biāo)志了抄伍。所以艘刚,這個(gè)邏輯的處理,應(yīng)該是在取標(biāo)志的時(shí)候進(jìn)行截珍。而多余的值攀甚,和一個(gè)正常的標(biāo)志疗认,兩者之間的區(qū)別菩帝,就是它是否以 '-' 開(kāi)頭。所以,我們修改 Tokenizer.nextFlag() 方法二蓝,選中 this.tokens.shift()观堂,抽取變量惩猫,命名為 token霉颠。再在中間加入一行判斷:

nextFlag() {
    let token = this.tokens.shift();
    if (!token.startsWith('-')) throw new Error(`Unexpected value: hello`);
    return token.substring(1);
}

保存,綠了事期。重構(gòu)拐格,把出錯(cuò)提示寫(xiě)活:

if (!token.startsWith('-')) throw new Error(`Unexpected value: ${token}`);

保存,還是綠的刑赶。接下來(lái)還有什么需要重構(gòu)的捏浊?注意,這是我們連續(xù)做的第三個(gè)小任務(wù)撞叨,所以金踪,出現(xiàn)“第二顆子彈”的幾率會(huì)比較大。先看出錯(cuò)信息牵敷,我們?nèi)齻€(gè)任務(wù)胡岔,用到三次 throw new Error(),而且竟然分布在三個(gè)不同的文件里面枷餐。同樣是出錯(cuò)信息靶瘸,統(tǒng)一管理起來(lái)肯定是有好處的,比如要做多語(yǔ)言毛肋,要寫(xiě)文檔等等怨咪。所以,我們需要把這些錯(cuò)誤信息歸置歸置润匙。那么诗眨,只需要抽取三個(gè)字符串常量出來(lái)就可以了嗎?當(dāng)然是可以的孕讳,不過(guò)匠楚,為了讓使用的地方更簡(jiǎn)潔一些,我們可以把整個(gè)異常一起抽取出來(lái)厂财。

來(lái)到 Schemas.find() 方法芋簿,選中從 throw 開(kāi)始一直到行尾,抽取一個(gè)全局的方法璃饱,命名為 unknownFlagError

function unknownFlagError(flag) {
    throw new Error(`Unknown flag: -${flag}`);
}

find() 中調(diào)用那一行則變?yōu)榱耍?/p>

if (!found) unknownFlagError(flag);

保存与斤,綠的。這樣看起來(lái)更清楚一些。接著我們 F6 把這個(gè)新抽取出來(lái)的方法幽告,移動(dòng)到新建的 main/errors.js 文件中梅鹦,保存裆甩,綠的冗锁。

同樣的手法,我們從 IntegerArgumentType.convert() 中抽取出 invalidIntegerError()嗤栓,并將其移入剛才創(chuàng)建的 errors.js 文件中冻河。記得保存并確保仍然是綠的。最后是從 Tokenizer.nextFlag() 中抽取出 unexpectedValueError()茉帅,并同樣移入 errors.js 文件叨叙。保存,仍然是綠的】芭欤現(xiàn)在 errors.js 文件長(zhǎng)這個(gè)樣子:

export function unknownFlagError(flag) {
    throw new Error(`Unknown flag: -${flag}`);
}

export function invalidIntegerError(flag, value) {
    throw new Error(`Invalid integer of flag -${flag}: ${value}`);
}

export function unexpectedValueError(token) {
    throw new Error(`Unexpected value: ${token}`);
}

把它們歸集到一起擂错,其實(shí)還有一個(gè)好處。現(xiàn)在三個(gè)函數(shù)都是直接拋出的 Error 類(lèi)的實(shí)例樱蛤,它們之間的區(qū)別僅僅在于消息字符串的不同钮呀。仍然考慮用戶視角,這其實(shí)是不友好的昨凡。為什么爽醋?想象一下,如果我們的用戶便脊,需要根據(jù)不同的錯(cuò)誤蚂四,做一些不同的邏輯處理,他們應(yīng)該如何判斷當(dāng)前出現(xiàn)的是哪種錯(cuò)誤呢哪痰?只能根據(jù)錯(cuò)誤中的消息字符串進(jìn)行判斷遂赠。而消息字符串是不穩(wěn)定的,畢竟是描述性的信息嘛晌杰,哪天產(chǎn)品經(jīng)理一句話解愤,就改了;或者做了多語(yǔ)言乎莉,翻譯成別的語(yǔ)言了送讲。因此,依賴于消息字符串惋啃,是不可靠的哼鬓。為了給用戶提供這方面的方便,應(yīng)該怎么做呢边灭?

至少有三種辦法:

  1. 為錯(cuò)誤對(duì)象新增一個(gè)整數(shù)型的錯(cuò)誤編碼屬性(這個(gè)整型編碼是穩(wěn)定的)
  2. 每個(gè)錯(cuò)誤使用不同的類(lèi)(并且通常它們會(huì)有共同的父類(lèi)异希,以便于用戶統(tǒng)一截獲,然后分別處理)
  3. 可以結(jié)合 1 和 2 兩種方式(不僅有不同的類(lèi)绒瘦,而且每個(gè)類(lèi)上還也有錯(cuò)誤編碼屬性)

所以称簿,把他們歸置在一起扣癣,要做這些修改,就會(huì)方便很多憨降。不過(guò)父虑,出于篇幅限制,這個(gè)話題我們也不展開(kāi)了授药。同樣的士嚎,如果有興趣,可以把這個(gè)當(dāng)做練習(xí)題悔叽。

重構(gòu)完了嗎莱衩?生產(chǎn)代碼暫時(shí)是重構(gòu)完了。別忘了同樣重要的測(cè)試代碼娇澎,新增的三個(gè)用例明顯是有重復(fù)代碼的笨蚁。抽取公共方法,因?yàn)槎际菚?huì)拋出錯(cuò)誤的趟庄,所以我們就命名為 testParsingError 吧括细。抽取手法前面已經(jīng)詳細(xì)介紹過(guò)了,這里就不重復(fù)了岔激。抽取出來(lái)的公共方法:

function testParsingError(commandLine, schemas, error) {
    let parser = new ArgumentParser(schemas);

    expect(() => parser.parse(commandLine)).toThrow(error);
}

三個(gè)調(diào)整后的測(cè)試用例:

it('處理規(guī)則未定義的情況', () => {
    testParsingError('-b', [
    ], 'Unknown flag: -b');
});

it('處理整型參數(shù)的值不合法的情況', () => {
    testParsingError('-p 123a', [
        IntegerSchema('p'),
    ], 'Invalid integer of flag -p: 123a');
});

it('處理傳了多余的值的情況', () => {
    testParsingError('-d hello', [
        BooleanSchema('d'),
    ], 'Unexpected value: hello');
});

保存勒极,綠的。重構(gòu)完了嗎虑鼎?掃一眼測(cè)試代碼辱匿,你覺(jué)得呢?要想保持代碼不會(huì)腐化炫彩,必須隨時(shí)保持警覺(jué)匾七。有了新的公共方法 testParsingError() 之后,有沒(méi)有覺(jué)得之前的公共方法 testParse() 這個(gè)名字已經(jīng)不是很貼切了江兢?既然新的方法是測(cè)試解析會(huì)出錯(cuò)的昨忆,那么之前的方法就應(yīng)該稱之為測(cè)試解析會(huì)成功的——testParsingSuccess()。用 Shift + F6 改一下名字杉允,保存邑贴,還是綠的。

這下算是重構(gòu)完了叔磷。下一個(gè):“處理字符串型參數(shù)沒(méi)有傳值的情況”拢驾。老規(guī)矩,先上失敗的測(cè)試:

it('處理字符串型參數(shù)沒(méi)有傳值的情況', () => {
    testParsingError('-s', [
        StringSchema('s'),
    ], 'Value not specified of flag -s');
});

保存改基,紅了繁疤。接下來(lái)讓它通過(guò)。直接修改 Tokenizer.nextValue() 還是很簡(jiǎn)單的,套路前面也都用過(guò)稠腊。不過(guò)這里也有一個(gè)小問(wèn)題躁染,為了準(zhǔn)確的報(bào)告錯(cuò)誤,我們需要把標(biāo)志的信息傳進(jìn)來(lái)架忌。所以吞彤,先修改 ArgumentParser.nextValue()

nextValue(type, flag) {
    let value = type.needValue() ? this.tokens.nextValue(flag) : undefined;
    return type.convert(value, flag);
}

然后是 Tokenizer.nextValue()

nextValue(flag) {
    if (!this.tokens.length) throw new Error(`Value not specified of flag -s`);
    return this.tokens.shift();
}

保存,綠了鳖昌。重構(gòu)字符串和報(bào)錯(cuò)方法备畦,提取出 valueNotSpecifiedError()

function valueNotSpecifiedError(flag) {
    throw new Error(`Value not specified of flag -${flag}`);
}

同樣將其移動(dòng)到 errors.js 文件里面低飒,Tokenizer.nextValue() 就變成了:

nextValue(flag) {
    if (!this.tokens.length) valueNotSpecifiedError(flag);
    return this.tokens.shift();
}

保存许昨,還是綠的。好了褥赊,這個(gè)大任務(wù)也完成了糕档。看看有什么需要重構(gòu)的嗎拌喉?暫時(shí)沒(méi)有找到速那。休息一下吧。

最后一個(gè)大任務(wù)

最后一個(gè)大任務(wù)尿背,附加題:“處理列表型參數(shù)”端仰。仍然是先做任務(wù)拆分。需求本身還是很清楚的田藐,布爾型不需要參數(shù)荔烧,也就是說(shuō)只剩下字符串列表和整數(shù)列表兩種情況。先做哪種情況呢汽久?原則還記得吧鹤竭,先做簡(jiǎn)單的。哪種更簡(jiǎn)單呢景醇?也介紹過(guò)了臀稚,字符串的更簡(jiǎn)單。所以三痰,初始的任務(wù)列表就有了:

  • 處理列表型參數(shù)
    • 處理字符串型列表參數(shù)
    • 處理整型列表參數(shù)

完了嗎吧寺?任務(wù)拆分也是介紹過(guò)的,除了正常的業(yè)務(wù)邏輯散劫,還有哪些稚机?異常情況、邊界條件舷丹,再加上自由發(fā)揮抒钱。一個(gè)一個(gè)來(lái)。列表型參數(shù)可能引入什么異常?一個(gè)是整數(shù)列表的解析同樣會(huì)有數(shù)字合法性問(wèn)題谋币。還有別的嗎仗扬?可能會(huì)有同學(xué)想到分隔符不合法。仔細(xì)想一下蕾额,我們肯定會(huì)按合法的分隔符做拆分早芭,所以,不合法的分隔符會(huì)作為某一個(gè)值的一部分诅蝶。而字符串可以接受任意值退个,即使里面混入了非法分隔符,那就屬于語(yǔ)義層面的問(wèn)題了调炬,我們的解析器是無(wú)法分辨的语盈,從而也就無(wú)法處理。而如果非法分隔符進(jìn)入了整數(shù)值缰泡,那么同樣會(huì)被我們的數(shù)字合法性檢查排查出來(lái)刀荒。此外,還可以參考已有的異常情況棘钞,可以發(fā)現(xiàn)其他幾種異常缠借,和列表參數(shù)沒(méi)有什么關(guān)系。于是我們加入這個(gè)異常處理任務(wù):

  • 處理列表型參數(shù)
    • 處理字符串型列表參數(shù)
    • 處理整型列表參數(shù)
    • 處理整型列表參數(shù)數(shù)字不合法的問(wèn)題

這個(gè)異常處理任務(wù)放在這個(gè)列表里宜猜,主要是行文方便泼返。實(shí)際開(kāi)發(fā)的時(shí)候,我們會(huì)把它放到前一個(gè)大任務(wù)“處理異常情況”里面姨拥,這樣分類(lèi)會(huì)更清楚绅喉。接下來(lái)是邊界條件。命令行為空垫毙,能算一個(gè)吧霹疫,這其實(shí)也就是默認(rèn)參數(shù)的情形。無(wú)論是整型列表综芥,還是字符串型列表丽蝎,默認(rèn)值都應(yīng)該是空數(shù)組。加入這兩個(gè)任務(wù):

  • 處理列表型參數(shù)
    • 處理字符串型列表參數(shù)
    • 處理整型列表參數(shù)
    • 處理整型列表參數(shù)數(shù)字不合法的問(wèn)題
    • 處理字符串型列表參數(shù)的默認(rèn)值
    • 處理整數(shù)型列表參數(shù)的默認(rèn)值

同樣的膀藐,在測(cè)試用例里面屠阻,我們會(huì)把這兩個(gè)默認(rèn)值相關(guān)的任務(wù),加入第一個(gè)大任務(wù)“處理默認(rèn)參數(shù)”里面额各。任務(wù)列表的拆分就差不多了国觉,再審視一遍,順序還可以再調(diào)整一下虾啦。還記得吧麻诀,先做實(shí)現(xiàn)難度最小的任務(wù)痕寓。哪個(gè)難度最低呢?自然是默認(rèn)值相關(guān)的蝇闭,對(duì)吧呻率。我們更新一下:

  • 處理列表型參數(shù)
    • 處理字符串型列表參數(shù)的默認(rèn)值
    • 處理整數(shù)型列表參數(shù)的默認(rèn)值
    • 處理字符串型列表參數(shù)
    • 處理整型列表參數(shù)
    • 處理整型列表參數(shù)數(shù)字不合法的問(wèn)題

考慮到這是最后一個(gè)大任務(wù)了,我們把整個(gè)任務(wù)列表放出來(lái)呻引,給大家一個(gè)直觀的感受(加粗的是我們剛剛新增的和列表相關(guān)的任務(wù)):

  • 處理參數(shù)默認(rèn)值
    • 處理布爾型參數(shù)的默認(rèn)值
    • 處理字符串型參數(shù)的默認(rèn)值
    • 處理整數(shù)型參數(shù)的默認(rèn)值
    • 處理字符串型列表參數(shù)的默認(rèn)值
    • 處理整數(shù)型列表參數(shù)的默認(rèn)值
  • 處理 1 個(gè)參數(shù)
    • 處理布爾型參數(shù)
    • 處理字符串型參數(shù)
    • 處理整數(shù)型參數(shù)
  • 處理 2 個(gè)參數(shù)
  • 處理 2 個(gè)參數(shù)
    • 處理 2 個(gè)整數(shù)型的參數(shù)
    • 處理 2 個(gè)布爾型的參數(shù)
    • 處理 1 個(gè)整型和 1 個(gè)布爾型的參數(shù)
    • 處理 1 個(gè)布爾型和 1 個(gè)整型的參數(shù)
  • 處理 3 個(gè)參數(shù)
    • 處理 1 個(gè)整型礼仗、1 個(gè)布爾型和 1 個(gè)字符串型參數(shù)
    • 處理 1 個(gè)負(fù)數(shù)、1 個(gè)字符串型和 1 個(gè)布爾型參數(shù)
    • 處理 1 個(gè)布爾型逻悠、1 個(gè)字符串型和 1 個(gè)未傳的整型參數(shù)
  • 處理異常情況
    • 處理規(guī)則未定義的情況
    • 處理整型參數(shù)的值不合法的情況
    • 處理傳了多余的值的情況
    • 處理字符串型參數(shù)沒(méi)有傳值的情況
    • 處理整型列表參數(shù)數(shù)字不合法的問(wèn)題
  • 處理列表型參數(shù)
    • 處理字符串型列表參數(shù)
    • 處理整型列表參數(shù)

好元践,開(kāi)始我們的第一個(gè)任務(wù):“處理字符串型列表參數(shù)的默認(rèn)值”。失敗的測(cè)試走起:

it('處理字符串型列表參數(shù)的默認(rèn)值', () => {
    testParsingSuccess('', [
        { type: StringListSchema, flag: 's', value: [] },
    ]);
});

保存童谒,紅了单旁。提示 StringListSchema 未定義。創(chuàng)建這個(gè)函數(shù)惠啄,并將其移動(dòng)到 main/schema.js 文件中:

export function StringListSchema(flag) {
    return new Schema(flag, StringListArgumentType);
}

這下變成了 StringListArgumentType 未定義慎恒。創(chuàng)建這個(gè)類(lèi)任内,并將其移動(dòng)到 main/argument-type.js 文件中:

export class StringListArgumentType extends ArgumentType {
}

接下來(lái)為 StringListArgumentType 類(lèi)加入默認(rèn)值處理方法:

static default() {
    return [];
}

保存撵渡,綠了,任務(wù)完成死嗦。然后是“處理整數(shù)型列表參數(shù)的默認(rèn)值”趋距。失敗的測(cè)試:

it('處理整數(shù)型列表參數(shù)的默認(rèn)值', () => {
    testParsingSuccess('', [
        { type: IntegerListSchema, flag: 'i', value: [] },
    ]);
});

保存,紅了越除。同樣的手法节腐,為其加入 IntegerListSchema() 方法的實(shí)現(xiàn)

export function IntegerListSchema(flag) {
    return new Schema(flag, IntegerListArgumentType);
}

以及 IntegerListArgumentType 類(lèi)的定義:

export class IntegerListArgumentType extends ArgumentType {
}

然后就是為 IntegerListArgumentType 類(lèi)加入默認(rèn)值方法:

static default() {
    return [];
}

保存,綠了摘盆。接下來(lái)是“處理字符串型列表參數(shù)”翼雀,失敗的測(cè)試:

describe('處理列表型參數(shù)', () => {

    it('處理字符串型列表參數(shù)', () => {
        testParsingSuccess('-s how,are,u', [
            { type: StringListSchema, flag: 's', value: ['how', 'are', 'u'] },
        ]);
    });

});

保存,紅了孩擂。提示 "type.convert is not a function"狼渊。于是我們?yōu)?StringListArgumentType 類(lèi)加入 convert() 方法:

static convert(value) {
    return ['how', 'are', 'u'];
}

保存,綠了类垦。為了讓測(cè)試快速通過(guò)狈邑,這里是寫(xiě)死的。接下來(lái)蚤认,把它寫(xiě)活米苹。實(shí)現(xiàn)也很簡(jiǎn)單,就是按分隔符進(jìn)行拆分就行了:

static convert(value) {
    return value.split(',');
}

保存砰琢,還是綠的蘸嘶。有同學(xué)這里會(huì)有疑問(wèn)了:“明明一行真代碼就能搞定的事情良瞧,為啥非要寫(xiě)成一行假代碼,然后再替換它训唱?這不是脫了褲子放屁嗎莺褒?”你說(shuō)的沒(méi)錯(cuò),對(duì)于這個(gè)簡(jiǎn)單的場(chǎng)景雪情,是的遵岩。不過(guò)別忘了,我們現(xiàn)在是在做練習(xí)巡通,最重要的不是解決某個(gè)具體問(wèn)題尘执,而是養(yǎng)成一個(gè)正確的習(xí)慣。實(shí)際工作中宴凉,你不一定能遇到可以一行代碼解決的問(wèn)題誊锭,一旦你直接上手寫(xiě)真實(shí)現(xiàn),就有可能因?yàn)楦鞣N問(wèn)題導(dǎo)致測(cè)試無(wú)法通過(guò)弥锄。而在紅色狀態(tài)停留超過(guò)一定的時(shí)間丧靡,你就會(huì)陷入焦慮,并且狀態(tài)下降籽暇。為了提高效率温治,避免焦慮,你應(yīng)該熟練掌握這種方法戒悠,并養(yǎng)成習(xí)慣熬荆。

現(xiàn)在需要重構(gòu)嗎?暫時(shí)不用绸狐,把下一個(gè)任務(wù)做了再說(shuō)卤恳。開(kāi)始“處理整型列表參數(shù)”,失敗的測(cè)試:

it('處理整型列表參數(shù)', () => {
    testParsingSuccess('-i 1,-3,2', [
        { type: IntegerListSchema, flag: 'i', value: [1, -3, 2] },
    ]);
});

保存寒矿,紅了突琳。可以看到符相,這里我們用了一個(gè)負(fù)數(shù)拆融,算是順手覆蓋一個(gè)邊界條件。隨時(shí)保持對(duì)邊界條件的警覺(jué)主巍,對(duì)于提升代碼穩(wěn)定性是有好處的冠息,建議有意識(shí)的培養(yǎng)自己的這個(gè)習(xí)慣。接下來(lái)盡快讓它通過(guò)孕索,為 IntegerListArgumentType 類(lèi)新增 convert() 方法:

static convert(value) {
    return [1, -3, 2];
}

保存逛艰,綠了。重構(gòu):

static convert(value) {
    return value.split(',').map(v => parseInt(v, 10));
}

保存搞旭,還是綠的∩⒉溃現(xiàn)在可以考慮重構(gòu)的事情了菇绵。看看新增的兩個(gè) ArgumentType 的子類(lèi):StringListArgumentTypeIntegerListArgumentType镇眷,它們之間有重復(fù)代碼嗎咬最?嗯,default() 方法里面的重復(fù)代碼是很容易看出來(lái)的欠动。很顯然永乌,任何列表型參數(shù)的默認(rèn)值,都應(yīng)該是空列表具伍。所以翅雏,這兩個(gè)類(lèi)應(yīng)該有一個(gè)公共的父類(lèi),專(zhuān)門(mén)用來(lái)處理列表型參數(shù)里面那些共通的邏輯人芽。于是我們創(chuàng)建這個(gè)新類(lèi):

export class ListArgumentType extends ArgumentType {

    static default() {
        return [];
    }

}

export class StringListArgumentType extends ListArgumentType {

    static convert(value) {
        return value.split(',');
    }

}

export class IntegerListArgumentType extends ListArgumentType {

    static convert(value) {
        return value.split(',').map(v => parseInt(v, 10));
    }

}

保存望几,綠的。接下來(lái)還有需要重構(gòu)的么萤厅?就在這段代碼里面就有的橄抹,能看出來(lái)嗎?嗯惕味,兩個(gè) value.split(',') 很明顯是重復(fù)的楼誓。那么把它們抽取出來(lái)就搞定了嗎?其實(shí)單獨(dú)抽取 split() 是治標(biāo)不治本的赦拘。能找出表面上不重復(fù)慌随,但實(shí)際邏輯是重復(fù)的代碼,是一項(xiàng)重要技能躺同,需要多練。給個(gè)提示丸逸,我們先稍微改寫(xiě)一下 StringListArgumentType.convert() 方法:

static convert(value) {
    return value.split(',').map(v => v);
}

保存蹋艺,還是綠的。然后我們把兩個(gè)類(lèi)的 convert() 里面的代碼放在一起來(lái)觀察:

return value.split(',').map(v => v);
return value.split(',').map(v => parseInt(v, 10));

能看到什么黄刚?map() 里面的代碼有沒(méi)有似曾相識(shí)的感覺(jué)捎谨?還記得前面提到的“復(fù)讀機(jī)”不?是的憔维,這 map() 里面的涛救,其實(shí)就是每個(gè)具體參數(shù)類(lèi)型的 convert() 邏輯。對(duì)吧业扒。這從業(yè)務(wù)上也是能說(shuō)通的:作用于整數(shù)型參數(shù)的所有邏輯检吆,同樣應(yīng)該作用于整型列表參數(shù)(中的每個(gè)數(shù)字)。也就是說(shuō)程储,這里除了 split() 是重復(fù)代碼以外蹭沛,map() 里面的臂寝,也是重復(fù)代碼。所以摊灭,這里的重構(gòu)咆贬,不僅僅是代碼層面的抽取,而且是業(yè)務(wù)邏輯的梳理帚呼。

那么具體應(yīng)該如何入手呢掏缎?這里比較復(fù)雜,我們一步一步來(lái)煤杀。首先將 StringListArgumentType.convert() 中的 map() 里面的代碼抽取出來(lái):

static convert(value) {
    return value.split(',').map(v => this.convertItem(v));
}

static convertItem(v) {
    return v;
}

保存御毅,綠的。同樣手法怜珍,抽取 IntegerListArgumentType 里面的代碼:

static convert(value) {
    return value.split(',').map(v => this.convertItem(v));
}

static convertItem(v) {
    return parseInt(v, 10);
}

保存端蛆,綠的。現(xiàn)在可以看到酥泛,兩個(gè) convert() 的代碼完全一致了今豆,把它們移動(dòng)到父類(lèi)里面(這里其實(shí)是【Pull Members Up】重構(gòu)手法,不過(guò) IDE 支持不夠好柔袁,所以我們手工來(lái))呆躲,ListArgumentType.convert()

static convert(value) {
    return value.split(',').map(v => this.convertItem(v));
}

StringListArgumentType.convert()IntegerListArgumentType.convert() 刪除掉即可。保存捶索,還是綠的插掂。

接著修改 StringListArgumentType.convertItem(),讓它直接使用 StringArgumentType 的已有邏輯:

static convertItem(v) {
    return StringArgumentType.convert(v);
}

保存腥例,綠的辅甥。同樣處理 IntegerListArgumentType.convertItem()

static convertItem(v) {
    return IntegerArgumentType.convert(v);
}

保存,綠的×鞘現(xiàn)在選中 StringListArgumentType 里面的 StringArgumentType璃弄,抽取方法,命名為 itemClass

static convertItem(v) {
    return this.itemClass().convert(v);
}

static itemClass() {
    return StringArgumentType;
}

保存,綠的。同樣手法處理 IntegerListArgumentType

static convertItem(v) {
    return this.itemClass().convert(v);
}

static itemClass() {
    return IntegerArgumentType;
}

保存畦徘,綠的。現(xiàn)在兩個(gè) convertItem() 又是完全一致了脐供,用剛才介紹的手法,把這個(gè)方法也上拉到父類(lèi) ListArgumentType 里面:

static convertItem(v) {
    return this.itemClass().convert(v);
}

保存借跪,綠的政己。這里的 v 這個(gè)名字不太好,Shift + F6 改名為 value垦梆,保存匹颤,綠的〗龊ⅲ現(xiàn)在相關(guān)的三個(gè)類(lèi)就是這樣了:

export class ListArgumentType extends ArgumentType {

    static default() {
        return [];
    }

    static convert(value) {
        return value.split(',').map(v => this.convertItem(v));
    }

    static convertItem(value) {
        return this.itemClass().convert(value);
    }

}

export class StringListArgumentType extends ListArgumentType {

    static itemClass() {
        return StringArgumentType;
    }

}

export class IntegerListArgumentType extends ListArgumentType {

    static itemClass() {
        return IntegerArgumentType;
    }

}

重復(fù)代碼都沒(méi)有了吧。接下來(lái)是最后一個(gè)任務(wù)了:“處理整型列表參數(shù)數(shù)字不合法的問(wèn)題”印蓖。失敗的測(cè)試:

it('處理整型列表參數(shù)數(shù)字不合法的問(wèn)題', () => {
    testParsingError('-i 3,123a,7', [
        IntegerListSchema('i'),
    ], 'Invalid integer of flag -i: 123a');
});

保存辽慕,紅了∩馑啵可以看到溅蛉,問(wèn)題是出錯(cuò)提示里面沒(méi)有帶上參數(shù)標(biāo)志。由于標(biāo)志信息我們已經(jīng)傳入給 ArgumentType.convert 了他宛,所以這里改起來(lái)也很簡(jiǎn)單船侧,接收這個(gè)參數(shù),并傳下去就好了厅各。修改 ListArgumentType 類(lèi):

static convert(value, flag) {
    return value.split(',').map(v => this.convertItem(v, flag));
}

static convertItem(value, flag) {
    return this.itemClass().convert(value, flag);
}

保存镜撩,綠了。接下來(lái)的一個(gè)小調(diào)整是個(gè)人喜好队塘,你可以自己選擇是否采納:

static convert(value, flag) {
    return value.split(',').map(this.convertItem(flag));
}

static convertItem(flag) {
    return value => this.itemClass().convert(value, flag);
}

保存袁梗,還是綠的。恭喜憔古,需求我們都做完了遮怜。再看看有沒(méi)有需要重構(gòu)的地方。目前 argument-type.js 這個(gè)文件貌似有些臃腫了鸿市,里面包含 7 個(gè)類(lèi)锯梁,而且各個(gè)類(lèi)都還有自己的邏輯。所以焰情,我們把這個(gè)文件拆解一下陌凳。將 BooleanArgumentType 類(lèi),移動(dòng)到 main/types/boolean-argument-type.js 文件烙样,保存冯遂,綠的。同樣的手法谒获,將除了 ArgumentType 以外的類(lèi),都移動(dòng)到各自在 main/types/ 下的文件中壁却。最后批狱,直接在 argument-type.js 文件上用 F6,將其移動(dòng)到 main/types/ 文件夾中展东。確保測(cè)試仍然是綠的赔硫。看看我們現(xiàn)在的文件夾結(jié)構(gòu):

  • main/
    • types/
      • argument-type.js
      • boolean-argument-type.js
      • integer-argument-type.js
      • integer-list-argument-type.js
      • list-argument-type.js
      • string-argument-type.js
      • string-list-argument-type.js
    • argument.js
    • argument-parser.js
    • arguments.js
    • errors.js
    • schema.js
    • schemas.js
    • tokenizer.js
  • test/
    • argument-parser.test.js

以后要是有加入新類(lèi)型支持的需求盐肃,我們只需要在 main/types/ 文件夾中加入一個(gè)對(duì)應(yīng)的類(lèi)定義文件爪膊,然后在 main/schema.js 文件中加入一個(gè)規(guī)則函數(shù)(用于更方便的創(chuàng)建規(guī)則)就可以了权悟。當(dāng)然,如果我們完全不要這些規(guī)則函數(shù)推盛,讓用戶自己創(chuàng)建 Schema 的對(duì)象峦阁,我們自己就能更方便——新增類(lèi)型只需要在 main/types/ 中增加一個(gè)文件即可,不涉及其他任何文件的修改耘成。不過(guò)榔昔,對(duì)于我們來(lái)說(shuō),這些規(guī)則函數(shù)的維護(hù)是非常簡(jiǎn)單的瘪菌,以如此小的開(kāi)銷(xiāo)撒会,為我們的用戶帶來(lái)使用上的便利,是很值得的师妙。

好了诵肛,以上就是這個(gè)習(xí)題的所有內(nèi)容∧ǎ考慮到篇幅的問(wèn)題怔檩,我們?cè)谶@里只貼出測(cè)試代碼,以及 ArgumentParser 這個(gè)核心類(lèi)的代碼壁顶。整個(gè)項(xiàng)目的完整代碼珠洗,我把它放在了世界的盡頭,不若专,放在了這里:https://github.com/mophy/kata-args-v4许蓖。

test/argument-parser.test.js

import { ArgumentParser } from '../main/argument-parser';
import { BooleanSchema, IntegerListSchema, IntegerSchema, StringListSchema, StringSchema } from '../main/schema';

function testParsingSuccess(commandLine, params) {
    let schemas = params.map(param => param.type(param.flag));
    let parser = new ArgumentParser(schemas);

    let result = parser.parse(commandLine);

    params.forEach((param) => {
        let { flag, value } = param;
        expect(result.get(flag)).toEqual(value);
    });
}

function testParsingError(commandLine, schemas, error) {
    let parser = new ArgumentParser(schemas);

    expect(() => parser.parse(commandLine)).toThrow(error);
}

describe('ArgumentParser', () => {

    describe('處理默認(rèn)參數(shù)', () => {

        it('處理布爾型參數(shù)的默認(rèn)值', () => {
            testParsingSuccess('', [
                { type: BooleanSchema, flag: 'd', value: false },
            ]);
        });

        it('處理字符串型參數(shù)的默認(rèn)值', () => {
            testParsingSuccess('', [
                { type: StringSchema, flag: 'l', value: '' },
            ]);
        });

        it('處理整數(shù)型參數(shù)的默認(rèn)值', () => {
            testParsingSuccess('', [
                { type: IntegerSchema, flag: 'p', value: 0 },
            ]);
        });

        it('處理字符串型列表參數(shù)的默認(rèn)值', () => {
            testParsingSuccess('', [
                { type: StringListSchema, flag: 's', value: [] },
            ]);
        });

        it('處理整數(shù)型列表參數(shù)的默認(rèn)值', () => {
            testParsingSuccess('', [
                { type: IntegerListSchema, flag: 'i', value: [] },
            ]);
        });

    });

    describe('處理 1 個(gè)參數(shù)', () => {

        it('處理布爾型參數(shù)', () => {
            testParsingSuccess('-d', [
                { type: BooleanSchema, flag: 'd', value: true },
            ]);
        });

        it('處理字符串型參數(shù)', () => {
            testParsingSuccess('-l /usr/logs', [
                { type: StringSchema, flag: 'l', value: '/usr/logs' },
            ]);
        });

        it('處理整數(shù)型參數(shù)', () => {
            testParsingSuccess('-p 8080', [
                { type: IntegerSchema, flag: 'p', value: 8080 },
            ]);
        });

    });

    describe('處理 2 個(gè)參數(shù)', () => {

        it('處理 2 個(gè)整數(shù)型的參數(shù)', () => {
            testParsingSuccess('-p 8080 -q 9527', [
                { type: IntegerSchema, flag: 'p', value: 8080 },
                { type: IntegerSchema, flag: 'q', value: 9527 },
            ]);
        });

        it('處理 2 個(gè)布爾型的參數(shù)', () => {
            testParsingSuccess('-d -e', [
                { type: BooleanSchema, flag: 'd', value: true },
                { type: BooleanSchema, flag: 'e', value: true },
            ]);
        });

        it('處理 1 個(gè)整型和 1 個(gè)布爾型的參數(shù)', () => {
            testParsingSuccess('-p 8080 -d', [
                { type: IntegerSchema, flag: 'p', value: 8080 },
                { type: BooleanSchema, flag: 'd', value: true },
            ]);
        });

        it('處理 1 個(gè)布爾型和 1 個(gè)整型的參數(shù)', () => {
            testParsingSuccess('-d -p 8080', [
                { type: BooleanSchema, flag: 'd', value: true },
                { type: IntegerSchema, flag: 'p', value: 8080 },
            ]);
        });

    });

    describe('處理 3 個(gè)參數(shù)', () => {

        it('處理 1 個(gè)整型、1 個(gè)布爾型和 1 個(gè)字符串型參數(shù)', () => {
            testParsingSuccess('-p 8080 -d -s /usr/logs', [
                { type: IntegerSchema, flag: 'p', value: 8080 },
                { type: BooleanSchema, flag: 'd', value: true },
                { type: StringSchema, flag: 's', value: '/usr/logs' },
            ]);
        });

        it('處理 1 個(gè)負(fù)數(shù)调衰、1 個(gè)字符串型和 1 個(gè)布爾型參數(shù)', () => {
            testParsingSuccess('-q -9527 -s /usr/logs -d', [
                { type: IntegerSchema, flag: 'q', value: -9527 },
                { type: StringSchema, flag: 's', value: '/usr/logs' },
                { type: BooleanSchema, flag: 'd', value: true },
            ]);
        });

        it('處理 1 個(gè)布爾型膊爪、1 個(gè)字符串型和 1 個(gè)未傳的整型參數(shù)', () => {
            testParsingSuccess('-d -s /usr/logs', [
                { type: IntegerSchema, flag: 'p', value: 0 },
                { type: BooleanSchema, flag: 'd', value: true },
                { type: StringSchema, flag: 's', value: '/usr/logs' },
            ]);
        });

    });

    describe('處理異常情況', () => {

        it('處理規(guī)則未定義的情況', () => {
            testParsingError('-b', [
            ], 'Unknown flag: -b');
        });

        it('處理整型參數(shù)的值不合法的情況', () => {
            testParsingError('-p 123a', [
                IntegerSchema('p'),
            ], 'Invalid integer of flag -p: 123a');
        });

        it('處理傳了多余的值的情況', () => {
            testParsingError('-d hello', [
                BooleanSchema('d'),
            ], 'Unexpected value: hello');
        });

        it('處理字符串型參數(shù)沒(méi)有傳值的情況', () => {
            testParsingError('-s', [
                StringSchema('s'),
            ], 'Value not specified of flag -s');
        });

        it('處理整型列表參數(shù)數(shù)字不合法的問(wèn)題', () => {
            testParsingError('-i 3,123a,7', [
                IntegerListSchema('i'),
            ], 'Invalid integer of flag -i: 123a');
        });

    });

    describe('處理列表型參數(shù)', () => {

        it('處理字符串型列表參數(shù)', () => {
            testParsingSuccess('-s how,are,u', [
                { type: StringListSchema, flag: 's', value: ['how', 'are', 'u'] },
            ]);
        });

        it('處理整型列表參數(shù)', () => {
            testParsingSuccess('-i 1,-3,2', [
                { type: IntegerListSchema, flag: 'i', value: [1, -3, 2] },
            ]);
        });

    });

});

main/argument-parser.js

import { Arguments } from './arguments';
import { Argument } from './argument';
import { Tokenizer } from './tokenizer';
import { Schemas } from './schemas';

export class ArgumentParser {

    constructor(schemas) {
        this.schemas = new Schemas(schemas);
    }

    parse(commandLine) {
        this.createDefaultArguments();
        this.tokenizeCommandLine(commandLine);
        this.parseTokens();
        return this.args;
    }

    createDefaultArguments() {
        this.args = new Arguments(this.schemas.map(this.createArgument));
    }

    createArgument(schema) {
        return new Argument(schema.flag, schema.type.default());
    }

    tokenizeCommandLine(commandLine) {
        this.tokens = new Tokenizer(commandLine);
    }

    parseTokens() {
        while (this.tokens.hasMore()) this.parseToken();
    }

    parseToken() {
        let flag = this.tokens.nextFlag();
        let schema = this.schemas.find(flag);
        let value = this.nextValue(schema.type, flag);
        this.args.set(flag, value);
    }

    nextValue(type, flag) {
        let value = type.needValue() ? this.tokens.nextValue(flag) : undefined;
        return type.convert(value, flag);
    }

}

我們對(duì)實(shí)現(xiàn)代碼做一個(gè)簡(jiǎn)單的統(tǒng)計(jì):

  • 文件數(shù):14 個(gè)
  • 文件行數(shù)
    • 最小:7 行
    • 最大:47 行
    • 平均:18.2 行
  • 方法大小
    • 最泻坷颉:1 行
    • 最大:4 行
    • 平均:1.3 行

總結(jié)

如果你是一路跟著文章練到現(xiàn)在米酬,相信已經(jīng)通過(guò)親身體會(huì)的方式,掌握了 TDD 的相關(guān)方法趋箩。我們來(lái)簡(jiǎn)單復(fù)習(xí)一下赃额。

開(kāi)發(fā)流程:

  1. 溝通確認(rèn)需求(確定輸入和輸出)
  2. 拆分任務(wù)(越小、實(shí)現(xiàn)越簡(jiǎn)單越好)
  3. 編寫(xiě)代碼(紅叫确、綠跳芳、重構(gòu))

編碼流程:

  1. 失敗的測(cè)試(紅)
  2. 快速通過(guò)(綠)
  3. 重構(gòu)

現(xiàn)在我們?cè)贁[出 TDD 的好處,看看你有沒(méi)有在本次閱讀和操作過(guò)程中親身體會(huì)到:

  • 讓你倍有面子——?jiǎng)e人不會(huì)竹勉,你會(huì)(開(kāi)個(gè)玩笑)
  • 產(chǎn)品質(zhì)量更高——沒(méi)機(jī)會(huì)寫(xiě) bug飞盆,領(lǐng)導(dǎo)喜歡,績(jī)效好
  • 開(kāi)發(fā)速度更快——不寫(xiě) bug,不改 bug吓歇,早早回家睡覺(jué)
  • 開(kāi)發(fā)難度更低——能解決別人解決不了的問(wèn)題孽水,或者,參考前兩條
  • 返工成為歷史——不再被產(chǎn)品經(jīng)理怒懟
  • 維護(hù)風(fēng)險(xiǎn)更低——改需求不再“按下葫蘆浮起瓢”

瀏覽代碼城看,可以看到女气,最終的代碼是非常清楚和簡(jiǎn)潔的。你也明白了析命,這樣的代碼不是一蹴而就的主卫,而是通過(guò)無(wú)數(shù)個(gè)微小的步驟,逐漸打磨出來(lái)的鹃愤。如果只計(jì)方法體中的代碼簇搅,本文中的每次代碼修改,基本都在 2 行以內(nèi)软吐。這樣的小步子瘩将,是你能獲得 TDD 相關(guān)好處的關(guān)鍵因素。同時(shí)也是衡量一個(gè)人 TDD 做得好不好的關(guān)鍵指標(biāo)凹耙。

實(shí)際工作中姿现,你是可以按需調(diào)整步幅的。當(dāng)你感覺(jué)放心的時(shí)候肖抱,可以步子大一點(diǎn)备典;當(dāng)你覺(jué)得沒(méi)把握的時(shí)候,可以步子小一點(diǎn)意述。但是提佣,當(dāng)你的極限是一次 10 步的時(shí)候,即使是處理棘手的情況荤崇,你也沒(méi)法做到每次 2 步拌屏。這就是我們反復(fù)練習(xí),提高自己極限的原因术荤。當(dāng)你能夠達(dá)到每次 1 步倚喂,那么你會(huì)有足夠的信心和能力,處理任何復(fù)雜問(wèn)題瓣戚。而這個(gè)能力端圈,看文章、看書(shū)子库、聽(tīng)講座枫笛,都得不到,只有靠多練刚照。好消息是,習(xí)題多的是:http://codingdojo.org/kata/喧兄。

此外无畔,以下幾點(diǎn)也希望你能記得:

  • 測(cè)試代碼和生產(chǎn)代碼同等重要
  • 測(cè)試代碼也可以寫(xiě)得很漂亮啊楚、很容易維護(hù)
  • 用戶視角,讓用戶用起來(lái)簡(jiǎn)單浑彰、不容易出錯(cuò)

好恭理,終極問(wèn)題來(lái)了,“你說(shuō)這些東西郭变,我們工作里面用不上啊颜价,我們前端/后端情況比這個(gè)復(fù)雜,需要考慮的東西很多”诉濒。是的周伦,這也是 TDD 沒(méi)有被推行起來(lái)的重要原因之一。不過(guò)不用擔(dān)心未荒,無(wú)論是前端還是后端专挪,都可以做 TDD 的,只是需要一些技巧片排。具體做法寨腔,請(qǐng)期待接下來(lái)的文章,再會(huì):)

參考鏈接

[1]: http://www.reibang.com/p/62f16cd4fef3 深度解讀 - TDD(測(cè)試驅(qū)動(dòng)開(kāi)發(fā))
[2]: https://jestjs.io/docs/en/expect.html Jest Expect
[3]: https://github.com/unclebob/javaargs/tree/master The Java version of the Args Program
[4]: http://www.reibang.com/p/38493eb4ffbd 工廠設(shè)計(jì)模式(三種)詳解
[5]: https://en.wikipedia.org/wiki/Code_refactoring Code refactoring
[6]: https://baike.baidu.com/item/%E7%AD%89%E4%BB%B7%E7%B1%BB%E5%88%92%E5%88%86/4219313 等價(jià)類(lèi)劃分

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末率寡,一起剝皮案震驚了整個(gè)濱河市迫卢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌冶共,老刑警劉巖乾蛤,帶你破解...
    沈念sama閱讀 221,548評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異比默,居然都是意外死亡幻捏,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)命咐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)篡九,“玉大人,你說(shuō)我怎么就攤上這事醋奠¢痪剩” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,990評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵窜司,是天一觀的道長(zhǎng)沛善。 經(jīng)常有香客問(wèn)我,道長(zhǎng)塞祈,這世上最難降的妖魔是什么金刁? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,618評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上尤蛮,老公的妹妹穿的比我還像新娘媳友。我一直安慰自己,他們只是感情好产捞,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布醇锚。 她就那樣靜靜地躺著,像睡著了一般坯临。 火紅的嫁衣襯著肌膚如雪焊唬。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,246評(píng)論 1 308
  • 那天看靠,我揣著相機(jī)與錄音赶促,去河邊找鬼。 笑死衷笋,一個(gè)胖子當(dāng)著我的面吹牛芳杏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播辟宗,決...
    沈念sama閱讀 40,819評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼爵赵,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了泊脐?” 一聲冷哼從身側(cè)響起空幻,我...
    開(kāi)封第一講書(shū)人閱讀 39,725評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎容客,沒(méi)想到半個(gè)月后秕铛,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡缩挑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評(píng)論 3 340
  • 正文 我和宋清朗相戀三年但两,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片供置。...
    茶點(diǎn)故事閱讀 40,488評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谨湘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出芥丧,到底是詐尸還是另有隱情紧阔,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評(píng)論 5 350
  • 正文 年R本政府宣布续担,位于F島的核電站擅耽,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏物遇。R本人自食惡果不足惜乖仇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評(píng)論 3 333
  • 文/蒙蒙 一憾儒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧这敬,春花似錦航夺、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,331評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)始衅。三九已至冷蚂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間汛闸,已是汗流浹背蝙茶。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,445評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留诸老,地道東北人隆夯。 一個(gè)月前我還...
    沈念sama閱讀 48,897評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像别伏,于是被迫代替她去往敵國(guó)和親蹄衷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類(lèi)型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,111評(píng)論 1 32
  • 這是16年5月份編輯的一份比較雜亂適合自己觀看的學(xué)習(xí)記錄文檔厘肮,今天18年5月份再次想寫(xiě)文章愧口,發(fā)現(xiàn)簡(jiǎn)書(shū)還為我保存起的...
    Jenaral閱讀 2,767評(píng)論 2 9
  • ¥開(kāi)啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開(kāi)一個(gè)線程,因...
    小菜c閱讀 6,444評(píng)論 0 17
  • 1 青蛙類(lèi): 我目前最想改善的地方类茂,最大的青蛙是什么耍属? 想改善調(diào)整自己的睡眠狀態(tài),雖然每天10點(diǎn)半睡巩检,早上6點(diǎn)半起...
    會(huì)飛的番茄_fc4b閱讀 225評(píng)論 1 1
  • 每日推薦: 每日一歌――蘇打綠《你在煩惱什么》 每周一影――朱延平《旋風(fēng)小子》 每日一詩(shī)――陶淵明《雜詩(shī)》 人...
    薩拉芯雪閱讀 257評(píng)論 0 0