重构遗留代码(1):金牌大师
?
http://blog.jobbole.com/78635/
舊代碼,丑陋的代碼,復(fù)雜的代碼,意大利面條似的代碼,鬼話廢話……就是四個(gè)字:遺留代碼。這是一個(gè)系列文章,將有助于你處理并解決它。
在理想的世界中,你只會(huì)寫新代碼。你會(huì)把代碼寫得既漂亮又完美。你將永不會(huì)再看你的代碼,并且你將永遠(yuǎn)不會(huì)維護(hù)一個(gè)有十年之久的項(xiàng)目。在理想的世界中…
不幸的是,我們生活在現(xiàn)實(shí)的而非理想的世界。我們必須理解修改和增強(qiáng)年代久遠(yuǎn)的代碼這件事。我們必須處理遺留代碼。那么你還在等什么?讓我們一頭扎進(jìn)第一篇教程,拿著代碼,讀懂一點(diǎn)點(diǎn),并為了我們?nèi)蘸蟮男薷木幙椧粡埌踩W(wǎng)。
遺留代碼的定義
遺留代碼有如此之多的方式去定義,不可能為其找到一個(gè)單一的,普遍被接受的定義。這篇教程開(kāi)始的一些例子僅是九牛一毛。所以我不會(huì)給你們?nèi)魏喂俜降亩x。相反,我會(huì)給大家引用我喜歡的解釋。
對(duì)于我來(lái)說(shuō),遺留代碼就是沒(méi)有被測(cè)試的簡(jiǎn)單代碼。~ Michael Feathers
好吧,這是第一個(gè)對(duì)遺留代碼正式的定義,由 Michael Feathers 在他的書(shū)《修改代碼的藝術(shù)》(Working Effectively with Legacy Code)中給出。當(dāng)然,業(yè)界很久以來(lái)都使用這個(gè)表述,主要針對(duì)任何很難修改的代碼。但是這個(gè)定義給出了一些不同的方面。它把問(wèn)題解釋得很清晰,以至于解 決方法變得很明顯。“很難修改”是如此得模糊。我們應(yīng)該做什么來(lái)使得它容易修改?我們不知道!另一方面“未測(cè)試的代碼”是具體的。對(duì)于我們之前的一個(gè)問(wèn)題 就簡(jiǎn)單了,讓代碼可以測(cè)試并且測(cè)試它。那么讓我們開(kāi)始吧。
得到遺留代碼
這個(gè)系列將基于J.B. Rainsberger為遺留代碼撤退事件所寫的特殊益智問(wèn)答游戲而來(lái)。它被開(kāi)發(fā)得像是真的遺留代碼,并在一個(gè)相當(dāng)困難的等級(jí)上,提供了各種各樣重構(gòu)的機(jī)會(huì)。
檢出源代碼
益智問(wèn)答游戲放在GitHub上,并且遵循GPLv3許可,所以你可以自由使用。我們將從檢出官方資料庫(kù)開(kāi)始我們的系列教程。我們將要做出修改的代碼也會(huì)附在本教程中,所以如果你仍有疑惑,你可以對(duì)最后的結(jié)果來(lái)個(gè)先睹為快。
| 1 2 3 4 5 6 7 8 | $ git clone https://github.com/jbrains/trivia.git Cloning into 'trivia'... remote: Counting objects: 429, done. remote: Compressing objects: 100% (262/262), done. remote: Total 429 (delta 100), reused 419 (delta 93) Receiving objects: 100% (429/429), 848.33 KiB | 305.00 KiB/s, done. Resolving deltas: 100% (100/100), done. Checking connectivity... done. |
當(dāng)你打開(kāi)Trivia的目錄,你會(huì)發(fā)現(xiàn)我們的代碼有幾種編碼語(yǔ)言。我們將用PHP來(lái)演示,當(dāng)然你可以選擇你最喜歡的一個(gè)語(yǔ)言,并且適用于這里介紹的技巧。
理解代碼
根據(jù)定義,遺留代碼很難理解,特別是當(dāng)我們不知道它能做什么的時(shí)候。所以第一步是執(zhí)行代碼,并且做出某些推理它是關(guān)于什么的。
在目錄中我們有兩個(gè)文件。
| 1 2 3 4 5 6 7 | $ cd php/ $ ls -al total 20 drwxr-xr-x? 2 csaba csaba 4096 Mar 10 21:05 . drwxr-xr-x 26 csaba csaba 4096 Mar 10 21:05 .. -rw-r--r--? 1 csaba csaba 5568 Mar 10 21:05 Game.php -rw-r--r--? 1 csaba csaba? 410 Mar 10 21:05 GameRunner.php |
對(duì)我們運(yùn)行代碼,GameRunner.php似乎是個(gè)不錯(cuò)的選擇。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | $ php ./GameRunner.php Chet was added They are player number 1 Pat was added They are player number 2 Sue was added They are player number 3 Chet is the current player They have rolled a 4 Chet's new location is 4 The category is Pop Pop Question 0 Answer was corrent!!!! Chet now has 1 Gold Coins. Pat is the current player They have rolled a 2 Pat's new location is 2 The category is Sports Sports Question 0 Answer was corrent!!!! Pat now has 1 Gold Coins. Sue is the current player They have rolled a 1 Sue's new location is 1 The category is Science Science Question 0 Answer was corrent!!!! Sue now has 1 Gold Coins. Chet is the current player They have rolled a 4 ?? ## Some lines removed to keep ## the tutorial at a reasonable size ?? Answer was corrent!!!! Sue now has 5 Gold Coins. Chet is the current player They have rolled a 3 Chet is getting out of the penalty box Chet's new location is 11 The category is Rock Rock Question 5 Answer was correct!!!! Chet now has 5 Gold Coins. Pat is the current player They have rolled a 1 Pat's new location is 10 The category is Sports Sports Question 1 Answer was corrent!!!! Pat now has 6 Gold Coins. |
好的,我們的猜測(cè)是正確的。我們的代碼跑起來(lái)并且有了一些輸出。分析這些輸出,有助于我們推斷一些代碼做了什么的基本概念。
- 1.我們知道它是一個(gè)益智問(wèn)答游戲。當(dāng)我們檢出源代碼的時(shí)候我們知道的。
- 2.我們的例子有三個(gè)玩家:Chet、Pat和Sue。
- 3.有擲骰子或者相似的概念。
- 4.一個(gè)玩家有一個(gè)當(dāng)前位置。可能在某種告示牌上?
- 5.對(duì)于被問(wèn)及的問(wèn)題有各種分類。
- 6.用戶回答問(wèn)題。
- 7.答案正確,會(huì)給予玩家金幣。
- 8.錯(cuò)誤的答案會(huì)把玩家送入禁區(qū)。
- 9.玩家可以從禁區(qū)出來(lái),基于一些不明確的邏輯。
- 10.似乎第一個(gè)拿到6枚金幣的用戶就獲勝了。
這已經(jīng)知道很多了。我們可以僅通過(guò)輸出就弄清楚該應(yīng)用的基本行為。在真實(shí)的應(yīng)用當(dāng)中,輸出未必顯示在屏幕上,但它可能是一個(gè)網(wǎng)頁(yè),一個(gè)錯(cuò)誤日志,一 個(gè)數(shù)據(jù)庫(kù),一個(gè)網(wǎng)絡(luò)連接,一個(gè)轉(zhuǎn)儲(chǔ)文件等等。在其他的情況下,你需要修改的模塊是不能單獨(dú)運(yùn)行的。如果這樣,你將需要通過(guò)更大的應(yīng)用程序中的其他模塊來(lái)運(yùn) 行它。僅僅嘗試添加最小的模塊組合,從你的遺留代碼中得到一些合理的輸出。
?
掃描代碼
現(xiàn)在我們對(duì)于代碼輸出有了一些認(rèn)識(shí),我們可以開(kāi)始看代碼了。我們將從運(yùn)行器(runner)代碼開(kāi)始。
?
Game Runner
用 IDE 格式化所有代碼后,我喜歡這樣來(lái)運(yùn)行代碼。通過(guò)以我習(xí)慣的方式,能極大提高代碼可讀性,所以這段代碼:
…將變成這樣:
…這樣比較好一些。對(duì)于這樣少量的代碼來(lái)說(shuō),可能不是很大的變化,但它將用在我們后面的文件中。
查看GameRunner.php文件,我們很容易認(rèn)出一些之前我們看到的輸出中的關(guān)鍵點(diǎn)。我們可以看到增加用戶的行(9-11),roll()方 法被調(diào)用了并且勝出者也選出了。當(dāng)然,離這個(gè)邏輯游戲的內(nèi)在秘密還有很遠(yuǎn),但至少我們開(kāi)始認(rèn)出關(guān)鍵方法,這將幫助我們探索剩下的代碼。
?
游戲文件
我們也要對(duì)Game.php文件進(jìn)行同樣的格式化。
這個(gè)文件很大;大約200行代碼。大部分方法都是大小適中,但其中一些卻很大并且在格式化之后,我們可以看到在兩個(gè)地方代碼的縮進(jìn)已經(jīng)超過(guò)四個(gè)層次了。高層次的縮進(jìn)通常意味著很多更復(fù)雜的抉擇,所以目前,我們假定代碼中的這些點(diǎn)將更復(fù)雜并且對(duì)修改更敏感。
?
金牌大師
改變的想法促使我們認(rèn)識(shí)到缺少測(cè)試。我們?cè)贕ame.php中看到的代碼相當(dāng)復(fù)雜。如果你不理解它們那么別擔(dān)心。此時(shí),它們對(duì)于我來(lái)說(shuō)也是個(gè)迷。遺留代碼是個(gè)我們需要解決和理解的謎題。我們第一步去理解它,現(xiàn)在是時(shí)候進(jìn)行我們的第二步了。
?
那么什么是金牌大師?
當(dāng)面對(duì)遺留代碼時(shí),幾乎不可能理解它并且寫出完全運(yùn)行代碼所有路徑的測(cè)試代碼。對(duì)于這種測(cè)試,我們需要理解代碼,但我們還沒(méi)能這么做。所以我們需要采取另一個(gè)方法。
替代試圖弄清楚去測(cè)試什么,我們可以測(cè)試所有東西許多遍,以便我們有大量的輸出來(lái)結(jié)束,這樣我們幾乎可以認(rèn)為這些輸出是執(zhí)行了遺留代碼的所有部分產(chǎn)生的。建議是運(yùn)行代碼至少10000次。我們將寫一個(gè)測(cè)試程序運(yùn)行它兩次并保存輸出。
?
寫金牌大師生成器
我們可以提前考慮并開(kāi)始創(chuàng)建一個(gè)生成器和一個(gè)測(cè)試程序作為將來(lái)測(cè)試的兩個(gè)文件,但有必要嗎?我們還不能肯定。那么為什么不從一個(gè)基本的測(cè)試文件開(kāi)始,運(yùn)行我們的代碼一次并且從那里構(gòu)建我們的邏輯。
你將發(fā)現(xiàn)附件代碼存檔,在source文件夾里面但在trivia文件夾外面有我們的Test?文件夾。在這個(gè)文件夾里,我們創(chuàng)建了一個(gè)文件:GoldenMasterTest.php。
| 1 2 3 4 5 6 7 8 9 10 11 12 | class GoldenMasterTest extends PHPUnit_Framework_TestCase { ?? ????function testGenerateOutput() { ????????ob_start(); ????????require_once __DIR__ . '/../trivia/php/GameRunner.php'; ????????$output = ob_get_contents(); ????????ob_end_clean(); ?? ????????var_dump($output); ????} ?? } |
我們可以用很多種方式做這個(gè)。舉個(gè)例子,我們可以從控制臺(tái)運(yùn)行我們的代碼并將它輸出到文件。然而,我們不應(yīng)該忽視這樣一個(gè)優(yōu)勢(shì),創(chuàng)建測(cè)試文件并在我們的IDE中是很容易運(yùn)行的。
代碼很簡(jiǎn)單,它緩沖了輸出,并且將其放入$output這個(gè)變量。在包含的文件內(nèi),方法require_once()也會(huì)運(yùn)行所有代碼。在我們的變量區(qū)我們將看到一些已經(jīng)熟悉的輸出。
但在第二次運(yùn)行時(shí),我們看到一些奇怪的東西:
…輸出不一樣了。即使我們運(yùn)行了同樣的代碼,輸出卻不一樣了。滾動(dòng)的數(shù)字不一樣,玩家的位置不一樣。
?
為隨機(jī)數(shù)生成器播種
| 1 2 3 4 5 6 7 8 9 10 11 | do { ?? ????$aGame->roll(rand(0, 5) + 1); ?? ????if (rand(0, 9) == 7) { ????????$notAWinner = $aGame->wrongAnswer(); ????} else { ????????$notAWinner = $aGame->wasCorrectlyAnswered(); ????} ?? } while ($notAWinner); |
通過(guò)分析運(yùn)行器的基本代碼,我們看到它使用rand()這個(gè)方法來(lái)生成隨機(jī)數(shù)。我們接下來(lái)做的是通過(guò)官方的PHP文檔來(lái)研究rand()這個(gè)方法。
隨機(jī)數(shù)生成器是自動(dòng)播種的。
文檔告訴我們播種是自動(dòng)發(fā)生的。現(xiàn)在我們有了另一個(gè)任務(wù)。我們需要找到一種方式去控制種子。srand()方法可以幫助做到。這里是它從文檔來(lái)的定義。
為隨機(jī)數(shù)發(fā)生器播種或者沒(méi)提供種子時(shí)生成隨機(jī)值。
它告訴我們,如果我們?cè)谌魏螌?duì)rand()的調(diào)用前執(zhí)行它,我們應(yīng)該總會(huì)以相同結(jié)果結(jié)束運(yùn)行。
| 1 2 3 4 5 6 7 8 9 | function testGenerateOutput() { ????ob_start(); ????srand(1); ????require_once __DIR__ . '/../trivia/php/GameRunner.php'; ????$output = ob_get_contents(); ????ob_end_clean(); ?? ????var_dump($output); } |
我們?cè)趓equire_once()之前放上srand(1)。現(xiàn)在輸出總是一樣的了。
?
將輸出放入文件
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class GoldenMasterTest extends PHPUnit_Framework_TestCase { ?? ????function testGenerateOutput() { ????????file_put_contents('/tmp/gm.txt', $this->generateOutput()); ????????$file_content = file_get_contents('/tmp/gm.txt'); ????????$this->assertEquals($file_content, $this->generateOutput()); ????} ?? ????private function generateOutput() { ????????ob_start(); ????????srand(1); ????????require_once __DIR__ . '/../trivia/php/GameRunner.php'; ????????$output = ob_get_contents(); ????????ob_end_clean(); ????????return $output; ????} ?? } |
這個(gè)修改看起來(lái)很合理。對(duì)嗎?我們提取代碼生成一個(gè)方法,運(yùn)行兩次,并期待輸出相同結(jié)果。但是它們不同。
原因是require_once()兩次沒(méi)有請(qǐng)求相同的文件。第二次調(diào)用generateOutput()方法將產(chǎn)生一個(gè)空的字符串。所以,我們能做什么呢?我們單單調(diào)用require()怎么樣?那樣應(yīng)該就可以每次運(yùn)行到了。
好吧,這又導(dǎo)致了另一個(gè)問(wèn)題:”Cannot redeclare echoln()”。但它從哪里來(lái)?恰恰是在Game.php文件的開(kāi)始處。這個(gè)錯(cuò)誤發(fā)生的原因是因?yàn)镚ameRunner.php 中我們有 include __DIR__ . ‘/Game.php’;,每次當(dāng)我們調(diào)用generateOutput()方法的時(shí)候它會(huì)試圖引入Game文件兩次。
| 1 | include_once __DIR__ . '/Game.php'; |
使用GameRunner.php中的include_once將解決我們的問(wèn)題。是的,到目前為止我們需要修改GameRunner.php使得 沒(méi)有針對(duì)它的測(cè)試。然而,我們可以99%得確定我們的修改不會(huì)破壞代碼本身。這是一個(gè)小而簡(jiǎn)單的修改并不會(huì)讓我們很害怕。最重要的是,它會(huì)使測(cè)試通過(guò)。
?
運(yùn)行許多次
現(xiàn)在我們有了可以運(yùn)行多次的代碼,是時(shí)候生成一些輸出了。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | function testGenerateOutput() { ????$this->generateMany(20, '/tmp/gm.txt'); ????$this->generateMany(20, '/tmp/gm2.txt'); ????$file_content_gm = file_get_contents('/tmp/gm.txt'); ????$file_content_gm2 = file_get_contents('/tmp/gm2.txt'); ????$this->assertEquals($file_content_gm, $file_content_gm2); } ?? private function generateMany($times, $fileName) { ????$first = true; ????while ($times) { ????????if ($first) { ????????????file_put_contents($fileName, $this->generateOutput()); ????????????$first = false; ????????} else { ????????????file_put_contents($fileName, $this->generateOutput(), FILE_APPEND); ????????} ????????$times--; ????} } |
這里我們抽出了另一個(gè)方法:generateMany()。它有兩個(gè)參數(shù)。一個(gè)是我們想要運(yùn)行生成器的次數(shù),另一個(gè)是目標(biāo)文件。它將把生成的輸出放到文件當(dāng)中去。第一次運(yùn)行時(shí),它清空文件,剩下的迭代,它會(huì)附加數(shù)據(jù)。你可以查看文件,看看運(yùn)行20次生成的輸出。
但等等!同一個(gè)玩家每次都贏?這可能嗎?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | at /tmp/gm.txt | grep "has 6 Gold Coins." Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. |
是的,這是可能的!它不單單是可能的。而是肯定的事。對(duì)于隨機(jī)功能我們提供了相同的種子。我們一遍遍得玩同一個(gè)游戲。
?
每次以不同的方式運(yùn)行程序
我們需要玩?zhèn)€不一樣的游戲,否則幾乎可以肯定我們的遺留代碼僅有一小部分在真正地一遍遍執(zhí)行。金牌大師的范圍是運(yùn)行盡可能多的代碼。我們需要每次都給隨機(jī)數(shù)生成器以種子,但通過(guò)控制的方式。一種選擇是使用計(jì)數(shù)器作為種子值。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | private function generateMany($times, $fileName) { ????$first = true; ????while ($times) { ????????if ($first) { ????????????file_put_contents($fileName, $this->generateOutput($times)); ????????????$first = false; ????????} else { ????????????file_put_contents($fileName, $this->generateOutput($times), FILE_APPEND); ????????} ????????$times--; ????} } ?? private function generateOutput($seed) { ????ob_start(); ????srand($seed); ????require __DIR__ . '/../trivia/php/GameRunner.php'; ????$output = ob_get_contents(); ????ob_end_clean(); ????return $output; } |
這仍然能使我們的測(cè)試程序運(yùn)行,所以我們確信,當(dāng)輸出每次迭代都執(zhí)行一個(gè)不同的游戲時(shí),其都生成了相同的完整輸出。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | cat /tmp/gm.txt | grep "has 6 Gold Coins." Sue now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Pat now has 6 Gold Coins. Pat now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Sue now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Sue now has 6 Gold Coins. Chet now has 6 Gold Coins. Sue now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. Pat now has 6 Gold Coins. Chet now has 6 Gold Coins. Chet now has 6 Gold Coins. |
在隨機(jī)方式下游戲有了多個(gè)勝出者。看起來(lái)不錯(cuò)。
?
運(yùn)行20000次
你要嘗試的第一件事是讓我們代碼迭代20000次游戲過(guò)程。
| 1 2 3 4 5 6 7 8 | function testGenerateOutput() { ????$times = 20000; ????$this->generateMany($times, '/tmp/gm.txt'); ????$this->generateMany($times, '/tmp/gm2.txt'); ????$file_content_gm = file_get_contents('/tmp/gm.txt'); ????$file_content_gm2 = file_get_contents('/tmp/gm2.txt'); ????$this->assertEquals($file_content_gm, $file_content_gm2); } |
這幾乎就運(yùn)行了。將生成兩個(gè)55M的文件。
| 1 2 3 | ls -alh /tmp/gm* -rw-r--r-- 1 csaba csaba 55M Mar 14 20:38 /tmp/gm2.txt -rw-r--r-- 1 csaba csaba 55M Mar 14 20:38 /tmp/gm.txt |
另一方面,測(cè)試會(huì)因?yàn)閮?nèi)存不足的錯(cuò)誤而失敗。和你的機(jī)器有多少內(nèi)存無(wú)關(guān),測(cè)試將會(huì)失敗。我有8G多的內(nèi)存并有4G的交換區(qū),它仍然失敗了。兩個(gè)字符串只是太大了而不能在斷言中比較。
換句話說(shuō),我們生成了正常的文件,但是PHPUnit不能比較他們。我們需要一個(gè)解決方法。
| 1 | $this->assertFileEquals('/tmp/gm.txt', '/tmp/gm2.txt'); |
看上去這是一個(gè)好的選擇,但它仍然失敗了。真可惜。我們需要進(jìn)一步研究現(xiàn)狀。
| 1 | $this->assertTrue($file_content_gm == $file_content_gm2); |
然而這個(gè)可以運(yùn)行。
這可以比較兩個(gè)字符串而當(dāng)它們不同時(shí)就會(huì)失敗。然而它有些小代價(jià)。當(dāng)字符串不同時(shí),它不會(huì)準(zhǔn)確地告知哪里錯(cuò)了。而僅僅會(huì)告知“Failed asserting that false is true.”。但我們將在后面的教程處理這個(gè)問(wèn)題。
?
最后的思考
這篇教程結(jié)束了。在第一課我們學(xué)到了很多并且對(duì)于將來(lái)的工作有了一個(gè)好的開(kāi)始。我們看了代碼,以不同的方式分析它并且主要了解了它的基本邏輯。然后 我們創(chuàng)建了一套測(cè)試程序來(lái)保證盡可能多得執(zhí)行它。是的,測(cè)試運(yùn)行非常慢。在我的Core i7 CPU的配置中它花了24秒才生成兩次輸出文件。幸運(yùn)的是,在我們將來(lái)的開(kāi)發(fā)中,我們將保留gm.txt文件不變,并且每次運(yùn)行只生成另一個(gè)文件一次。但 12秒對(duì)于這樣一小段代碼來(lái)說(shuō),仍然是一個(gè)大量的時(shí)間。
在我們即將完成這個(gè)系列的時(shí)候,我們的測(cè)試程序運(yùn)行將少于一秒并正確測(cè)試所有代碼。所以,敬請(qǐng)期待我們的下一個(gè)教程,我們會(huì)處理魔術(shù)常量,魔幻字符串和復(fù)雜的條件句這些問(wèn)題。感謝你的閱讀。
轉(zhuǎn)載于:https://www.cnblogs.com/code-style/p/4120155.html
總結(jié)
以上是生活随笔為你收集整理的重构遗留代码(1):金牌大师的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 日历,日期类(copy)
- 下一篇: android 从零单排 第一期 按键显