寫(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)該怎么做呢边灭?
至少有三種辦法:
- 為錯(cuò)誤對(duì)象新增一個(gè)整數(shù)型的錯(cuò)誤編碼屬性(這個(gè)整型編碼是穩(wěn)定的)
- 每個(gè)錯(cuò)誤使用不同的類(lèi)(并且通常它們會(huì)有共同的父類(lèi)异希,以便于用戶統(tǒng)一截獲,然后分別處理)
- 可以結(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):StringListArgumentType
和 IntegerListArgumentType
镇眷,它們之間有重復(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ā)流程:
- 溝通確認(rèn)需求(確定輸入和輸出)
- 拆分任務(wù)(越小、實(shí)現(xiàn)越簡(jiǎn)單越好)
- 編寫(xiě)代碼(紅叫确、綠跳芳、重構(gòu))
編碼流程:
- 失敗的測(cè)試(紅)
- 快速通過(guò)(綠)
- 重構(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)劃分