Args練習

一. 基本信息

項目介紹

我們經常會遇到需要解析命令行參數的場景。如果沒有趁手的工具,我們可以自己寫一個,自己想辦法處理傳給main函數的參數。

Args是一個經典的編程練習題,復雜度與我們日常開發中接觸到的需求大致相當,能很好地體現出程序員的熟練度。

教學目標

  • 準確框定需求范圍
  • 把需求拆解成明細的任務列表
  • 練熟測試驅動開發的節奏

學習時長

7小時

技能點

  • 單元測試
  • 任務拆分
  • 測試驅動開發
  • 面向對象設計
  • 重構

能力目標

  • 用測試用例描述和溝通需求
  • 把需求拆解成可以逐步開發的任務列表
  • 基本的面向對象設計
  • 識別代碼壞味道
  • 重構的基本節奏和原則

二. 項目導學

項目介紹

“Args”也是一個經典的編程練習題。“鮑勃大叔”Robert Martin在《Clean Code》的第14章裡介紹過這個題目。

初始需求

我們經常會遇到需要解析命令行參數的場景。如果沒有趁手的工具,我們可以自己寫一個,自己想辦法處理傳給main函數的參數。

傳入一個程序的參數包含瞭“標記”(flag)和“值”(value)。標記都是一個字母,前面加上“-”號(例如“-p”這樣)。每個標記可以有一個值與之對應,也可以沒有對應的值。

我們要開發一個解析器(parser)來處理這些參數。這個解析器需要一個參數結構(schema)來描述“這個程序應該接受哪些參數”的信息,包括:

  • 應該有幾個標記;
  • 每個標記應該是什麼類型;
  • 每個標記的缺省值是什麼。

參數結構指定好以後,就可以把實際接收到的參數列表傳給參數解析器。解析器會首先驗證參數列表是否與參數結構相匹配。然後,程序就可以向參數解析器查詢每個參數的值(根據參數的標記名)。返回值的類型應該與參數結構中規定的類型相一致。

例如程序接收到的參數是這樣:

-l -p 8080 -d /usr/logs

那麼對應的參數結構應該規定3個標記:l、p、d

  • “l”(logging,是否記錄日志)標記沒有與之對應的值,這是一個佈爾型的標記,如果傳入瞭“-l”就為True,否則為False。
  • “p”(port,端口號)標記的值是整數型。
  • "d"(directory,目錄)標記的值是字符串型。

如果參數結構中規定瞭的標記在實際的參數列表中沒有出現,那麼就應該返回合理的缺省值,例如佈爾型的缺省值可以是False,數值型的缺省值可以是0,字符串型的缺省值可以是空字符串。

如果實際給出的參數與參數結構不匹配,需要給出良好的錯誤信息,解釋清楚出錯的原因。

擴展需求

擴展代碼,支持列表類型的參數。例如下列參數中:

-g this,is,a,list -d 1,2,-3,5

“g”標記對應的是字符串類型的列表([“this”, “is”, “a”, “list”]),“d”標記對應的是整數類型的列表([1, 2, -3, 5])。

代碼應該具有良好的可擴展性,這樣在添加新的值類型時才會簡單明瞭。

教學目標

  • 準確框定需求范圍
  • 把需求拆解成明細的任務列表
  • 練熟測試驅動開發的節奏

學習周期

7小時

配套資料

  • 《測試驅動開發》,第一部分
  • 《重構》,第三章
  • 在線文章:需求都沒整清楚,你寫啥代碼(http://mp.weixin.qq.com/s/DNrZ5OJlmwjfIT97Cu9TSg
  • 在線文章:不讀《實現模式》就不要寫代碼(http://mp.weixin.qq.com/s/fLJIj0WdDYJcPfjYWJLGPw
  • 在線文章:像機器一樣思考(http://insights.thoughtworkers.org/think-as-a-machine/

1.項目剖析

項目解讀

Args的難度比FizzBuzz高得多。如果沒有做好基本功的準備,貿然開始這道題目,恐怕很多人會感到很受挫。據我們的觀察,大多數程序員首次練習完成這道題需要4小時以上,甚至用瞭一整天還完不成的情況也絕非罕見。

Args這道題目有意地在很多地方對需求描述不夠清晰,例如參數結構的定義方式、參數的輸入格式等。這就需要我們學會用測試來準確地表達自己對需求的理解,用測試作為與客戶溝通的橋梁,用測試框定需求的范圍。

一個有一定規模的需求,不僅要知道建造完成後的樣子,還要知道如何一步步建造出來的過程。這個過程設計得好,整個需求的開發會很平穩,工作量評估會很準確,出錯的可能性會很低。合理拆分任務,是良好建造過程的基礎。

在開發遇到困難的時候,要學會把測試的步伐放得小一點、再小一點,從最簡單的問題開始,每次專註寫一個測試、讓一個測試通過。

雖說作為練習題顯得有些難度,其實這道題的復雜度跟日常工作中拆分良好的用戶故事差不多,也就是大多數程序員需要一到兩天開發出來的規模。練功房的小夥伴已經練到能在半小時內完成這道題。這就是基本功帶來的效率差距。

技能要求

  • 用測試用例描述和溝通需求
  • 把需求拆解成可以逐步開發的任務列表
  • 基本的面向對象設計
  • 識別代碼壞味道
  • 重構的基本節奏和原則

項目拆解

本項目分為7個任務:

  • Args初體驗
  • 框定需求范圍
  • Part I:Schema
  • Part II:讀入參數
  • 封裝所有的容器
  • Part III:取出參數值
  • 大類和長函數

任務1:Args初體驗

第一個任務很簡單:做一遍Args這道題。

做題的要求:

  • 看懂題目,開始編碼之前先花10分鐘拆解任務,把任務清單寫下來
  • 計時,開始編碼
  • 要求用TDD的方式實現:先寫測試,後寫代碼

如果超過90分鐘還沒完成,就先暫停。我們還需要留出一點時間來反思。

反思:

  • 做完整道題用瞭多長時間?
  • 代碼質量怎麼樣?
  • TDD的方式順暢嗎?
  • 實際做的步驟,和一開始拆分的任務是否相同?

任務2:框定需求范圍

如果在任務一遇到瞭困難,有很大可能是對需求的范圍框得不夠清晰,不知不覺中把需求擴大化瞭。(當然,也可能漏掉瞭一些重要的需求。)

比如說,有很多同學會花大量精力思考如何做字符串的解析。但是我們是否先問過客戶,輸入參數的格式應該是什麼樣?我們是否首先思考過,這個需求可以分解成幾個大塊?各個大塊的優先級順序是什麼?

在動手寫代碼之前,我們要首先弄清需求的范圍和優先級,並用測試的形式把它記錄下來。

做題的要求:

  • 看懂題目,仔細想想,最終的用戶會如何使用你開發的程序
  • 隻寫測試,用測試來描述最終用戶如何使用你開發的程序
  • 寫完測試之後,閱讀這篇文章,看看你寫的測試是否合理
  • 再寫一遍測試

反思:

  • 你對用戶需求的理解合理嗎?
  • 從你編寫的測試來看,用戶使用你開發的程序方便嗎?
  • 你理解的需求可以分為幾個大塊?
  • 各個大塊需求的優先級順序是什麼? 最好能再練一遍,驗證反思的結果。

任務3:Part I:Schema

在處理Args的需求時,一個常見的錯誤是沒有考慮必要的靈活性。很多同學把“-l參數是佈爾型”、“-p參數是整數型”這些信息硬編碼寫在代碼中,就像這樣:

於是這個程序就與例子中這個使用場景綁死瞭。比如說,假如另一個使用者要使用我們開發的程序,然而在他的場景中,要處理的是“-w”這個參數,我們怎麼辦呢?難道每增加一個使用者我們就要去改一遍代碼嗎?很多不可維護的焦油坑軟件就是這麼來的。

題目裡已經給出瞭“參數結構(schema)”這個概念。今天我們的任務就是,開發Schema輸入和解析的邏輯。

請註意:今天我們隻處理“將Schema讀入對象結構”的邏輯。至於怎麼使用Schema,暫時不考慮。

做題的要求:

  • 先想一想,與Schema相關的對象結構是什麼樣的,用測試描述出來
  • 然後考慮,用戶會如何輸入Schema信息,用測試描述出來
  • 編寫實現代碼,讓測試通過

反思:

  • 你的測試是否一次性考慮瞭太多的事?
  • 如果再來一遍,如何可以縮小測試-開發的步伐?

最好能再練一遍,驗證反思的結果。

任務4:Part II:讀入參數

除瞭Schema之外,Args的另一個大塊功能,就是從用戶的輸入中讀入參數。今天我們的任務就是實現這部分邏輯。

請註意:今天我們隻處理“將參數讀入對象結構”的邏輯。至於怎麼使用參數,今天暫不考慮。

做題的要求:

  • 先想一想,與參數相關的對象結構是什麼樣的,用測試描述出來
  • 然後考慮,用戶會如何輸入參數信息,用測試描述出來
  • 編寫實現代碼,讓測試通過

反思:

  • 你的測試是否一次性考慮瞭太多的事?
  • 如果再來一遍,如何可以縮小測試-開發的步伐?

最好能再練一遍,驗證反思的結果。

任務5:封裝所有的容器

http://mp.weixin.qq.com/s/YojJITzYIycG6PyGCrRxPw 《用一次Map就失去一個對象》

在前面兩天的練習裡,你很可能用瞭不少的容器,尤其是Map。例如此前有個同學有這樣一段測試:

8a8228feec2e438496f634a23451e925

同樣的邏輯,我寫的測試就沒有暴露任何容器:

我在一篇公眾號文章裡這樣說:

“Map的出現幾乎一定意味著有個對象的概念被錯過瞭” “Map即對象”

從內容來看,Map和對象真的是一樣的:Map的key就是對象的字段名;Map的value就是對象的字段值。JavaScript早年間沒有那麼明確的面向對象特征的時候,大傢真的就是把Map和對象混用的。

但是Map和對象最大的區別在於:你是否給它一個合適的名字。老話說名不正則言不順。沒有一個合適的名字說“這是一個Flag對象”,你就傾向於不會多想想,到底哪些行為應該屬於這個對象。

其實還不僅限於Map。再比如List(甚至數組),很多時候也是被過度使用的。當一個對象給另一個對象提供一個List,很多時候就意味著暴露瞭自己的內部信息。所以我的觀點是,List和Map(以及其他集合結構)不應該在對象之間傳遞,隻應該在對象內部使用。

這些本來應該封裝成對象的集合結構沒有得到合適封裝, 其結果就是我昨晚上說的: “我註意到這是大傢的代碼另一個常見的問題” “對象太少” “帶來的結果就是很多邏輯在一個函數裡一層套一層”

所以下次一旦發現自己開始用Map,不妨多想一想:你需要的真的是一個Map嗎?或者,其實你需要的是一個正等著被定義的對象?

感受一下。把前面做過的題目再練一遍。

做題的要求:

  • 把前面做過的“讀入Schema”和“讀入參數”的邏輯再實現一遍,遵循TDD的節奏,先寫測試,再寫代碼,步伐要小。
  • 註意:封裝所有集合結構(例如Map和List)。集合結構隻在你自己定義的對象內部使用,不能傳遞到對象之外。

反思:

  • 封裝瞭集合結構之後,代碼產生瞭什麼變化?
  • 得到瞭新的對象嗎?
  • 是否有更好的方式對這些新的對象進行測試?

最好能再練一遍,驗證反思的結果。

任務6:Part III:取出參數值

有瞭前面的對象做基礎,完成Args的最後一步“取出參數值”應該很簡單瞭。這就是我們今天的任務。

做題的要求:

  • 完整實現Args的需求。如果時間來不及,可以先不考慮邊界異常情況。
  • 開始編碼之前先花10分鐘拆解任務,把任務清單寫下來
  • 要求用TDD的方式實現:先寫測試,後寫代碼
  • 計時,開始編碼

反思:

  • 做完整道題用瞭多長時間?
  • TDD的方式順暢嗎?每行代碼都是由測試驅動出來的嗎?
  • 實際做的步驟,和一開始拆分的任務是否相同?

最好能再練一遍,驗證反思的結果。

任務7:大類和長函數

代碼質量不是主觀的審美判斷。它是有相當客觀的判斷依據的。這個判斷依據就是Martin Fowler在《重構》裡說的“壞味道”(bad smell)。

這堂課,我們翻開《重構》(第二版),翻到第三章“代碼的壞味道”。這裡面列舉瞭24種壞味道,今天我們專註看其中的兩個壞味道:Long Method(過長函數)和Large Class(過大的類)。

多長的函數就算“過長”?多大的類就算“過大”?我有一個簡單的經驗法則。對於Args這樣一個典型的需求場景:

  • 如果任何一個函數的實現體(不含函數簽名和外圍的大括號)超過5行代碼,這個函數就過長瞭
  • 如果任何一個類的實現體(不含import、類簽名和外圍的大括號)超過50行代碼,這個類就過大瞭

你的代碼中是否已經出現瞭這兩個壞味道?《重構》第三章給出瞭對應的重構手法,試著照書裡寫的重構手法,重構一下看看吧。

練習的要求:

  • 從前一個任務的代碼開始,查看有沒有“過長函數”和“過大的類”壞味道
  • 如果發現瞭壞味道,看看對應的重構手法是什麼
  • 重構,直到壞味道消失

反思:

  • 重構的感覺怎麼樣?是否有信心?
  • 重構之後的代碼質量怎麼樣?

发表回复

相关推荐

淮上《破云》《破云2·吞海》故事梗概和感触

破云: 江停昏睡三年,苏醒后住进了和杨媚联合开的KTV里,KTV后厨冷冻库发现了一具男尸,严峫出场了,经过调查发现死者是一 ...

· 55秒前

HbulderX的安装以及导入uni-app项目开发

一、什么是uni-app? uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到 H5、以及各种小程 ...

· 2分钟前

牙齦萎縮,該知道根本原因瞭

大傢好,我是營養師空谷阿亮,昨天粉絲留言說自己牙齦萎縮,今天就聊聊這個知識,今天主題是:牙齦萎縮,該知道根本原因,現...

· 4分钟前

要不你還是把我刪瞭吧(les)12

“你去哪瞭”“便利店”高筱言從袋子裡掏出一個小盒子“過來,褲子脫瞭”趙子冉捂住臉,“幹嘛”“給你塗藥啊”“我不!我不要”趙子冉臉...

· 4分钟前

现在开超市能赚钱吗?能年入30万?

我想很多人都有一个梦想就是,在大城市赚钱后就自己开一个超市,自己当老板,别说小编当初的梦想也是开一家自己的超市,为什 ...

· 5分钟前