代碼(ma)的印(yin)象派(pai):寫點好代碼(ma)吧
最近有(you)一(yi)位(wei)獵頭顧(gu)問(wen)打(da)電話詢問(wen)是否(fou)有(you)換工作的(de)(de)(de)意(yi)向(xiang),對(dui)推薦的(de)(de)(de)公(gong)(gong)司和職位(wei)的(de)(de)(de)描述為:"我們這里有(you)一(yi)家非常(chang)關注(zhu)軟件質(zhi)量的(de)(de)(de)公(gong)(gong)司,在尋找(zhao)一(yi)位(wei)不僅能完成(cheng)有(you)挑(tiao)戰的(de)(de)(de)軟件開發任務,并(bing)且還對(dui)代碼(ma)質(zhi)量有(you)非常(chang)高追求的(de)(de)(de)軟件工程師(shi)。"。
很難得獵頭顧(gu)問會以(yi)這(zhe)樣(yang)的(de)(de)切入(ru)點(dian)來推(tui)薦職位(wei),而不(bu)是(shi)(shi)諸如(ru) "我(wo)們(men)是(shi)(shi)互聯(lian)網公(gong)司","我(wo)們(men)是(shi)(shi)著名互聯(lian)網公(gong)司","我(wo)們(men)可以(yi)提(ti)供業(ye)內領先的(de)(de)薪資","我(wo)們(men)創業(ye)的(de)(de)方向是(shi)(shi)行業(ye)的(de)(de)趨(qu)勢","我(wo)們(men)提(ti)供創業(ye)公(gong)司期權","我(wo)們(men)提(ti)供人(ren)體工程(cheng)座(zuo)椅","我(wo)們(men)公(gong)司漂亮妹子多" 等等。
誰會(hui)為了把椅子或者每天能看(kan)公司漂亮的前(qian)臺妹子而跳槽呢?將這(zhe)些描(miao)述可以概(gai)括成(cheng)一點,就是 "我們給的錢多(duo)"。誠(cheng)然,好的薪(xin)水(shui)是可以招(zhao)募到杰(jie)出的軟(ruan)件工程(cheng)師的。然而,優秀的軟(ruan)件工程(cheng)師通常(chang)已經得到了較好的薪(xin)水(shui),所以如(ru)果不(bu)是給出足夠的量,不(bu)一定會(hui)為五斗(dou)米而折腰(yao)。大多(duo)數的軟(ruan)件工程(cheng)師更看(kan)重的是技術興趣,所以諸如(ru) "來(lai)我們這(zhe)做 Openstack 吧","我們需要用 Go 實現 Docker 相關(guan)組件的人才" 看(kan)起來(lai)更有吸引力。
而軟件質量,代碼風格,則是另一個吸引優秀工程師的方向。追求卓越軟件質量的工程師,通常有著自己的軟件設計與實現思路,并在不斷的實踐中錘煉出自己的編程風格和代碼品味。這一類工(gong)程(cheng)師,其(qi)實無關使(shi)用什(shen)么語言、做(zuo)什(shen)么產品,他(ta)們會(hui)始終保持自己的(de)(de)品味,追求軟件(jian)實現的(de)(de)卓越,進而(er)產出高質(zhi)量的(de)(de)軟件(jian)。更重(zhong)要(yao)的(de)(de)是,優(you)秀(xiu)的(de)(de)工(gong)程(cheng)師希(xi)望與(yu)更多(duo)優(you)秀(xiu)的(de)(de)工(gong)程(cheng)師合作(zuo),并(bing)更愿意工(gong)作(zuo)在崇尚代碼質(zhi)量的(de)(de)氛圍中。
一(yi)般條件下,對軟件質量的(de)要求通常(chang)與軟件生命周(zhou)(zhou)期(qi)的(de)長(chang)短相關。按照軟件生命周(zhou)(zhou)期(qi)的(de)長(chang)短,我們(men)可以將軟件公司分為兩(liang)類:
- 快公司:創業公司,互聯網公司。推崇快速開發,快速試錯。軟件生命周期較短,代碼質量相對要求不高。
- 慢公司:傳統行業軟件公司,產品公司。推崇穩定可靠的軟件設計。軟件生命周期較長,代碼質量相對要求較高。
軟(ruan)(ruan)件(jian)生(sheng)命周期(qi)的長短,通常(chang)也決定了實現軟(ruan)(ruan)件(jian)所使(shi)(shi)用(yong)的編程語言。比如,快公司通常(chang)會使(shi)(shi)用(yong) PHP/Python/Ruby 等動態類(lei)型語言,而慢公司通常(chang)會選擇 C/C++/C#/Java 等靜(jing)態類(lei)型語言。
當(dang)獵(lie)頭顧問(wen)還沒有(you)(you)說出(chu)公司的名字之前(qian),我(wo)們也可以(yi)大(da)體猜測出(chu)該(gai)公司所屬的行(xing)業方(fang)向或軟(ruan)(ruan)件(jian)(jian)(jian)(jian)產(chan)品(pin)類(lei)型(xing)。比如,該(gai)公司可能來自傳統的金融、電信、醫療(liao)、石油(you)、ERP 行(xing)業等(deng),這些行(xing)業中(zhong)(zhong)(zhong)有(you)(you) IBM、Huawei、GE、Schlumberger、SAP 等(deng)等(deng)世界(jie) 500 強巨頭更重視(shi)軟(ruan)(ruan)件(jian)(jian)(jian)(jian)質量。當(dang)然(ran),通常推(tui)崇軟(ruan)(ruan)件(jian)(jian)(jian)(jian)質量的公司中(zhong)(zhong)(zhong),大(da)概(gai)率條件(jian)(jian)(jian)(jian)下碰到(dao)的會是(shi)外(wai)企,即使(shi)是(shi)中(zhong)(zhong)(zhong)小(xiao)外(wai)企,也會對軟(ruan)(ruan)件(jian)(jian)(jian)(jian)質量有(you)(you)相對較(jiao)高(gao)(gao)的要(yao)求(qiu)(qiu)。對于(yu)產(chan)品(pin)類(lei)型(xing),越是(shi)接近 Mission Critical 的產(chan)品(pin)形態則(ze)對軟(ruan)(ruan)件(jian)(jian)(jian)(jian)質量的要(yao)求(qiu)(qiu)越高(gao)(gao)。各種軟(ruan)(ruan)件(jian)(jian)(jian)(jian)中(zhong)(zhong)(zhong)間件(jian)(jian)(jian)(jian)的軟(ruan)(ruan)件(jian)(jian)(jian)(jian)質量要(yao)求(qiu)(qiu)也相對較(jiao)高(gao)(gao),比如內存(cun)數據庫(ku)、Message Queue 等(deng)。或者(zhe)如果(guo)區分(fen) Framework 和 Application,則(ze) Framework 的軟(ruan)(ruan)件(jian)(jian)(jian)(jian)質量顯然(ran)要(yao)求(qiu)(qiu)更高(gao)(gao)。
每一(yi)家(jia)較成熟的(de)(de)(de)軟(ruan)件公司(si),都會(hui)設(she)計(ji)自己的(de)(de)(de)軟(ruan)件編碼(ma)(ma)規(gui)范(fan),增強軟(ruan)件工(gong)程師的(de)(de)(de)協同效應,相互(hu)之間可以(yi)(yi)讀懂對(dui)方的(de)(de)(de)代碼(ma)(ma)。但(dan)實(shi)際操作中(zhong),又(you)有(you)多(duo)少人會(hui)執著遵守呢?編碼(ma)(ma)規(gui)范(fan)并不能保證產(chan)生出好代碼(ma)(ma),那代碼(ma)(ma)編寫的(de)(de)(de)好壞又(you)如(ru)何具體衡量呢?筆者經(jing)歷過(guo)的(de)(de)(de)公司(si)中(zhong),多(duo)半都是(shi)(shi)以(yi)(yi)軟(ruan)件發(fa)布后的(de)(de)(de) Bug 數量來衡量軟(ruan)件的(de)(de)(de)質量的(de)(de)(de),這種統(tong)計(ji)形式簡(jian)單粗(cu)暴,優點就(jiu)(jiu)是(shi)(shi)可以(yi)(yi)量化(hua),缺(que)點就(jiu)(jiu)是(shi)(shi)很難評判出軟(ruan)件代碼(ma)(ma)編寫的(de)(de)(de)優雅程度。我聽過(guo)一(yi)則笑話,說軟(ruan)件質量也(ye)不能做(zuo)的(de)(de)(de)太好,軟(ruan)件一(yi)定要有(you) Bug,這樣客(ke)戶才會(hui)買(mai)我們的(de)(de)(de)服(fu)務,而我們就(jiu)(jiu)是(shi)(shi)靠后期(qi)賣服(fu)務賺(zhuan)錢(qian)的(de)(de)(de)。好吧,情(qing)何以(yi)(yi)堪~~
好了,說了這么多,好像文不切題,這篇文章不是叫《代碼的印象派》嗎?和上(shang)面說的這(zhe)些(xie)有什(shen)么關系呀?
關系就在于,軟(ruan)(ruan)件(jian)質量與(yu)代碼編寫的優(you)雅(ya)程(cheng)度(du)息(xi)(xi)(xi)(xi)息(xi)(xi)(xi)(xi)相關。而是否能編寫出優(you)雅(ya)的代碼與(yu)軟(ruan)(ruan)件(jian)工程(cheng)師(shi)的個(ge)人風格和(he)品(pin)味(wei)息(xi)(xi)(xi)(xi)息(xi)(xi)(xi)(xi)相關。
在(zai)軟件(jian)工程領域(yu),通常生命周期(qi)長的(de)軟件(jian)會有著更(geng)高的(de)軟件(jian)質量需求(qiu),描述(shu)軟件(jian)質量的(de)內容可以參(can)考(kao)下面兩篇(pian)文(wen)章(zhang)。
在各種軟(ruan)(ruan)件(jian)質量模型(xing)的(de)(de)(de)描述中(zhong),都(dou)包含著軟(ruan)(ruan)件(jian)可(ke)(ke)(ke)維(wei)(wei)護(hu)性(xing)(Maintainability)這一屬性(xing)。而越(yue)是(shi)生(sheng)命(ming)周期長的(de)(de)(de)軟(ruan)(ruan)件(jian),對其軟(ruan)(ruan)件(jian)可(ke)(ke)(ke)維(wei)(wei)護(hu)性(xing)的(de)(de)(de)要求(qiu)越(yue)高。而提高軟(ruan)(ruan)件(jian)可(ke)(ke)(ke)維(wei)(wei)護(hu)性(xing)的(de)(de)(de)根本方式就(jiu)是(shi)編寫可(ke)(ke)(ke)閱讀的(de)(de)(de)代(dai)碼(ma),讓其他人理解(jie)代(dai)碼(ma)的(de)(de)(de)時間最小(xiao)化。代(dai)碼(ma)生(sheng)來就(jiu)是(shi)為人閱讀的(de)(de)(de),只是(shi)順(shun)便(bian)在機器上執行以完成功能。
在漫(man)長的(de)(de)軟件(jian)(jian)生(sheng)命周期中(zhong),我們(men)有(you)很多機會去修(xiu)改(gai)(gai)軟件(jian)(jian)代碼,比(bi)如發現了(le)新(xin)的(de)(de) Bug、增加新(xin)的(de)(de)功能、改(gai)(gai)進已(yi)有(you)功能等(deng)。修(xiu)改(gai)(gai)代碼的(de)(de)第一步當然就是(shi)閱讀(du)代碼,以了(le)解當前(qian)的(de)(de)設計和思路。如果代碼都讀(du)不懂的(de)(de)話,何談(tan)修(xiu)改(gai)(gai)呢(ni)?還(huan)有(you),大概率條件(jian)(jian)下(xia),修(xiu)復(fu)自(zi)己(ji)(ji)實現模塊的(de)(de) Bug 的(de)(de)人通(tong)常就是(shi)你自(zi)己(ji)(ji),如果時隔幾個月后自(zi)己(ji)(ji)都讀(du)不懂自(zi)己(ji)(ji)編寫的(de)(de)代碼,會是(shi)什么感受呢(ni)?
所以,如何(he)編(bian)(bian)寫出(chu)易(yi)讀的(de)代碼就(jiu)成(cheng)了問(wen)題的(de)關鍵。而能否(fou)編(bian)(bian)寫出(chu)易(yi)讀代碼,則直(zhi)接取決于軟件工程(cheng)師自己(ji)的(de)的(de)編(bian)(bian)程(cheng)風格(ge)和代碼品味。
在《孫子兵法》中(zhong)有云:"上兵(bing)伐謀,其(qi)次(ci)(ci)伐交,其(qi)次(ci)(ci)伐兵(bing),其(qi)下攻(gong)城(cheng)(cheng)。攻(gong)城(cheng)(cheng)之法,為不得已。" 對應到軟(ruan)(ruan)(ruan)件領域(yu),軟(ruan)(ruan)(ruan)件架(jia)構(gou)師可(ke)以通過(guo)出(chu)色(se)的(de)系統分(fen)析來(lai)構(gou)建可(ke)演進的(de)軟(ruan)(ruan)(ruan)件架(jia)構(gou),講究謀略;而軟(ruan)(ruan)(ruan)件工(gong)程(cheng)師則通過(guo)良好的(de)設計和編程(cheng)風格來(lai)完(wan)成攻(gong)城(cheng)(cheng)任務(wu),講究方法。
Paul Graham 的《黑客與畫家》中描述了(le)黑(hei)客與畫家的(de)共同點,就(jiu)(jiu)是他們都(dou)是創作者(zhe),并且都(dou)在努力(li)創作優秀(xiu)的(de)作品。畫家創作的(de)作品就(jiu)(jiu)是畫,內嵌(qian)著(zhu)自己的(de)風格和品味。軟(ruan)件(jian)工(gong)程師的(de)作品就(jiu)(jiu)是軟(ruan)件(jian)和代碼,如果可(ke)以的(de)話,你可(ke)以將代碼打(da)印成卷(juan),出版成書,只是,閱讀的(de)人會向你那樣幸福嗎?
畫(hua)(hua)家(jia)(jia)的(de)(de)(de)(de)作(zuo)品(pin)(pin)都(dou)會(hui)保留下來,如果你(ni)把一個畫(hua)(hua)家(jia)(jia)的(de)(de)(de)(de)作(zuo)品(pin)(pin)按照時間順(shun)序(xu)排列(lie),就會(hui)發現每幅(fu)畫(hua)(hua)所用的(de)(de)(de)(de)技巧,都(dou)是建立在上(shang)一幅(fu)作(zuo)品(pin)(pin)學(xue)(xue)到的(de)(de)(de)(de)東西之上(shang)。某幅(fu)作(zuo)品(pin)(pin)如果特別出眾,你(ni)往往能在更早期的(de)(de)(de)(de)作(zuo)品(pin)(pin)中找到類似(si)的(de)(de)(de)(de)版本。軟件(jian)工(gong)程(cheng)師也(ye)是通(tong)過實踐來學(xue)(xue)些編程(cheng),并且(qie)所進行的(de)(de)(de)(de)工(gong)作(zuo)也(ye)是具有(you)原(yuan)創(chuang)性的(de)(de)(de)(de),通(tong)常不會(hui)有(you)他人的(de)(de)(de)(de)完(wan)美的(de)(de)(de)(de)成果可以依靠,如果有(you)的(de)(de)(de)(de)話我們為什么(me)還要再造輪子呢?
創作者的(de)(de)另一(yi)(yi)個學(xue)習途徑是(shi)通過范例。對于畫(hua)家而言,臨(lin)摹大師的(de)(de)作品一(yi)(yi)直是(shi)傳統美(mei)(mei)術教育的(de)(de)一(yi)(yi)部(bu)分,因為臨(lin)摹迫使(shi)你仔細觀察一(yi)(yi)幅畫(hua)是(shi)如(ru)何完成的(de)(de)。軟(ruan)件(jian)(jian)工程師也可以通過學(xue)習優秀的(de)(de)程序源(yuan)碼(ma)來(lai)學(xue)會編程,不(bu)是(shi)看其執(zhi)行(xing)結果(guo),而是(shi)看源(yuan)碼(ma)實現思路(lu)和風格。優秀的(de)(de)軟(ruan)件(jian)(jian)一(yi)(yi)定都是(shi)在軟(ruan)件(jian)(jian)工程師對軟(ruan)件(jian)(jian)美(mei)(mei)的(de)(de)不(bu)懈追(zhui)求中實現的(de)(de),現如(ru)今有眾多優秀的(de)(de)開源(yuan)軟(ruan)件(jian)(jian)存(cun)在,如(ru)果(guo)你查看優秀軟(ruan)件(jian)(jian)的(de)(de)內部(bu),就會發現,即使(shi)在那些不(bu)被(bei)人知的(de)(de)部(bu)分,也同(tong)樣被(bei)優美(mei)(mei)的(de)(de)實現著。
所以(yi)說(shuo)(shuo),代(dai)(dai)(dai)碼(ma)(ma)是有畫面(mian)感的(de)(de)(de),看(kan)一段代(dai)(dai)(dai)碼(ma)(ma)就可(ke)以(yi)了解一個軟(ruan)件(jian)工程(cheng)師(shi)(shi)的(de)(de)(de)風(feng)格,進而塑造出該工程(cheng)師(shi)(shi)在(zai)(zai)你心目(mu)中的(de)(de)(de)印(yin)(yin)象(xiang)。工作中,我們每天都在(zai)(zai)閱讀同(tong)(tong)事們的(de)(de)(de)代(dai)(dai)(dai)碼(ma)(ma),進而對不同(tong)(tong)的(de)(de)(de)同(tong)(tong)事產生不同(tong)(tong)的(de)(de)(de)印(yin)(yin)象(xiang),對各種(zhong)不同(tong)(tong)印(yin)(yin)象(xiang)的(de)(de)(de)感受也在(zai)(zai)不斷影響著自身風(feng)格的(de)(de)(de)塑造。代(dai)(dai)(dai)碼(ma)(ma)的(de)(de)(de)印(yin)(yin)象(xiang)派,說(shuo)(shuo)的(de)(de)(de)就是,你想(xiang)讓(rang)你的(de)(de)(de)同(tong)(tong)事對你產生何種(zhong)印(yin)(yin)象(xiang)呢?
筆者不(bu)能自(zi)詡為我(wo)就是(shi)那類有(you)著良好(hao)的(de)(de)(de)編程(cheng)風(feng)(feng)格,并且代(dai)碼(ma)品(pin)味高雅的(de)(de)(de)軟件工程(cheng)師,只能說,我(wo)還在(zai)向這個目(mu)標努力著。風(feng)(feng)格和品(pin)味不(bu)是(shi)一(yi)(yi)朝一(yi)(yi)夕就能養成的(de)(de)(de),世間存在(zai)多(duo)少種風(feng)(feng)格我(wo)們也無(wu)法列舉,而說某種風(feng)(feng)格比(bi)另一(yi)(yi)種風(feng)(feng)格要好(hao)也會(hui)陷入(ru)無(wu)意的(de)(de)(de)爭辯。況且,軟件工程(cheng)師多(duo)少都會(hui)有(you)點自(zi)戀情節,在(zai)沒(mei)有(you)見到更好(hao)的(de)(de)(de)代(dai)碼(ma)之(zhi)前,始(shi)終都會(hui)感覺自(zi)己寫(xie)出的(de)(de)(de)代(dai)碼(ma)就是(shi)好(hao)代(dai)碼(ma),并且有(you)時(shi)不(bu)管你說什么,咱就是(shi)這個味兒!
我個人總結(jie)了幾點關于優雅(ya)代(dai)碼風格的(de)描(miao)述:
- 代碼簡單:不隱藏設計者的意圖,抽象干凈利落,控制語句直截了當。
- 接口清晰:類型接口表現力直白,字面表達含義,API 相互呼應以增強可測試性。
- 依賴項少:依賴關系越少越好,依賴少證明內聚程度高,低耦合利于自動測試,便于重構。
- 沒有重復:重復代碼意味著某些概念或想法沒有在代碼中良好的體現,及時重構消除重復。
- 戰術分層:代碼分層清晰,隔離明確,減少間接依賴,劃清名空間,理清目錄。
- 性能最優:局部代碼性能調至最優,減少后期因性能問題修改代碼的機會。
- 自動測試:測試與產品代碼同等重要,自動測試覆蓋 80% 的代碼,剩余 20% 選擇性測試。
下面,我會列(lie)舉一些我在工作中遇到的不同的編程風(feng)格(ge),用切身的體會來(lai)感悟代(dai)碼的風(feng)格(ge)和(he)品味。當然,吐槽為(wei)主(zhu),因(yin)為(wei)據(ju)說在 Code Review 時記錄說 "我擦" 的數(shu)量(liang)就可以(yi)衡(heng)量(liang)代(dai)碼的好壞。
變量
關(guan)于變(bian)量,很遺憾(han),不(bu)得不(bu)提變(bian)量的命(ming)名。時至今(jin)日,在 Code Review 中仍(reng)然(ran)可以看到下(xia)面這樣(yang)的代(dai)碼。
1 public class Job 2 { 3 private DateTime StartTime; 4 private DateTime mStartTime; 5 private DateTime m_StartTime; 6 private DateTime _StartTime; 7 public DateTime endTime; 8 private Command pCommand; 9 private long schId; 10 }
有各(ge)種奇葩(pa)的(de)前(qian)綴(zhui)(zhui)出現,有時同一個(ge)人的(de)命名居(ju)然也不(bu)統一。雖(sui)然,眼睛(jing)和大(da)腦在(zai)重(zhong)復的(de)觀察變量(liang)名后會自動學習以(yi)忽略前(qian)綴(zhui)(zhui),并不(bu)會太影(ying)響(xiang)閱(yue)讀(du)。實際上,使(shi)用(yong)前(qian)綴(zhui)(zhui)的(de)目的(de)主(zhu)要(yao)是為了在(zai)局部(bu)代碼中區分全局變量(liang)和局部(bu)變量(liang)。使(shi)用(yong)類似(si)于 C# 這樣(yang)的(de)高級語(yu)言,我們已經(jing)不(bu)再需(xu)要(yao)為變量(liang)添加前(qian)綴(zhui)(zhui)了,可以(yi)利用(yong) this 關鍵字來區分。如(ru)果非要(yao)添加的(de)話,建議使(shi)用(yong) "_" 單下劃線(xian)前(qian)綴(zhui)(zhui),促進大(da)腦更快速的(de)忽略。
1 public class Job 2 { 3 private DateTime _startTime; // use _ as prefix 4 private DateTime endTime; // no prefix 5 6 public DateTime StartTime 7 { 8 get { return _startTime; } 9 set { _startTime = value; } 10 } 11 12 public DateTime EndTime 13 { 14 get { return endTime; } 15 set { endTime = value; } 16 } 17 18 public long ScheduleId { get; private set; } // or no field needed 19 }
將 Field 標記為 public 應該是沒有分(fen)清 Field 與 Property 的(de)作用,進而推測對面向(xiang)對象編程中的(de)封裝概念理(li)解(jie)也不(bu)會有多好(hao)。
使用(yong) "p" 前綴的顯然(ran)有 C/C++ 編程(cheng)情節,想描述這(zhe)個變量是一個指針(zhen),好(hao)吧(ba),這(zhe)種(zhong)寫(xie)法在 C# 中(zhong)只能稱為不倫(lun)不類。
使用(yong)縮寫(xie),這(zhe)里(li)的(de) "sch" 其實是想代表 "schedule",但在(zai)(zai)沒有(you)上(shang)下文的(de)條件下誰(shui)能想的(de)出來呢?我個人是絕對不推薦使用(yong)縮寫(xie)的(de),除非(fei)是普世的(de)理(li)解性存在(zai)(zai),例如 "obj", "ctx", "pwd", "ex", "args" 這(zhe)樣非(fei)常常見(jian)的(de)縮寫(xie)。
使用拼音和有(you)拼寫錯誤的單(dan)詞(ci)(ci)作(zuo)為(wei)變(bian)量(liang)名(ming)會(hui)直接(jie)拉低工程師的檔次。使用合適單(dan)詞(ci)(ci)描述可以直接(jie)提高代碼(ma)的質量(liang),比(bi)如通(tong)常 "Begin", "End" 會(hui)成對兒出現,上面的示例(li)代碼(ma)中(zhong)涉及到了(le)時間(jian),"StartTime" 和 "BeginTime" 是(shi)同義詞(ci)(ci),所以我們參考了(le) Outlook Calendar 中(zhong)的默認術語,也就是(shi) "StartTime" 和 "EndTime",也就是(shi)找范例(li)。
在局(ju)部變量的(de)使用(yong)中(zhong),我認為有一種使用(yong)方式是值得推薦(jian)的(de),那就是 "解釋性變量"。當(dang)今(jin)的(de)編(bian)程風格(ge)中(zhong)流行使用(yong) Fluent API,這樣會產(chan)生(sheng)類似于(yu)下面(mian)這樣的(de)代碼。
1 if(DateTimeOffset.UtcNow >= 2 period 3 .RecurrenceRange.StartDate.ConvertTime(period.TimeZone) 4 .Add(schedule.Period.StartTime.ConvertTime(period.TimeZone).TimeOfDay)) 5 { 6 // do something 7 }
這(zhe)一(yi)串(chuan) "." 看著好帥氣,但我是理解不(bu)了這(zhe)是要(yao)比較(jiao)什(shen)么(me)。可(ke)以簡單重構為解釋性變量(liang)。
1 var firstOccurrenceStartTime = 2 period 3 .RecurrenceRange.StartDate.ConvertTime(period.TimeZone) 4 .Add(schedule.Period.StartTime.ConvertTime(period.TimeZone).TimeOfDay); 5 6 if(DateTimeOffset.UtcNow >= firstOccurrenceStartTime) 7 { 8 // do something 9 }
構造函數
很多工程師還(huan)沒有(you)理解好構造函數(shu)的(de)功效和使(shi)用方(fang)式(shi),在選擇依(yi)賴(lai)注入(ru)方(fang)式(shi)時,更傾向于使(shi)用屬(shu)性(xing)依(yi)賴(lai)注入(ru)。個人認為(wei),使(shi)用 "屬(shu)性(xing)依(yi)賴(lai)注入(ru)" 是(shi)懶惰的(de)一(yi)種表現,其不(bu)僅打破了(le)信息隱(yin)藏的(de)封(feng)裝,而且還(huan)可以暴露了(le)本不(bu)需要暴露的(de)部分。使(shi)用構造函數(shu)進行依(yi)賴(lai)注入(ru)是(shi)最正確(que)的(de)方(fang)式(shi),我們應該竭盡全力將(jiang)代碼重(zhong)構到(dao)這一(yi)點。
好的(de),你說的(de),我信(xin)了!并且,我也開始這(zhe)么做(zuo)了!絕對純(chun)凈的(de)構造函(han)數注入!
1 public class Schedule 2 { 3 public Schedule(long templateId, long seriesId, 4 long promotionId, bool isOnceSche, DateTime startTime, DateTime endTime, 5 List<TimeRange> blackOutList, ScheduleAddtionalConfig addtionalConfig, 6 IDateTimeProvider tProvider, IScheduleMessageProxy proxy, 7 IAppSetting appSetting, RevisionData revisionData) 8 { 9 } 10 }
好吧,你贏了!構造函數居然有 12 個參數,距(ju)離史上最長的構造函數不遠(yuan)了。
一般寫成這樣(yang)的代(dai)碼已經表(biao)示沒法看了,而(er)且注定類的設計也不怎么樣(yang),這要是遺留下來的 Legacy Code,不知道維護者心情(qing)幾何(he)?
還有(you)一類(lei)(lei)構造函數(shu)問題就是參數(shu)順序,這(zhe)直接體現了軟件(jian)工程師最終他人的基本(ben)素養(yang)。因為(wei)(wei)構造函數(shu)生來(lai)就是為(wei)(wei)使用者(zhe)準備的,而為(wei)(wei)使用者(zhe)設計(ji)合理的參數(shu)順序是類(lei)(lei)設計(ji)者(zhe)的基本(ben)職責。
1 public class Command 2 { 3 public Command(int argsA, int argsB) 4 { 5 } 6 7 public Command(int argsC, int argsB, int argsA) 8 { 9 } 10 }
上(shang)面這種反人類思(si)維的(de)參數順序,怎(zen)么描述呢(ni)?寫成下面這樣有多大難度?
1 public class Command 2 { 3 public Command(int argsA, int argsB) 4 { 5 } 6 7 public Command(int argsA, int argsB, int argsC) 8 { 9 } 10 }
屬性
蹩腳的(de)屬性(xing)設(she)計常常彰顯抽象(xiang)對(dui)象(xiang)類型的(de)能力(li)。以下面這個 Schedule 類為例,Schedule 業務上存在 Once 和(he) Recurring 兩種狀態(tai)。我(wo)們最初看到的(de)類是(shi)這個樣(yang)子的(de)。
1 public class Schedule 2 { 3 public Schedule(bool isOnceSchedule) 4 { 5 IsOnceSchedule = isOnceSchedule; 6 } 7 8 public bool IsOnceSchedule { get; set; } 9 }
看(kan)來這(zhe)是(shi)想通過構造函數直(zhi)接注入指定(ding)狀態(tai),但 IsOnceSchedule 屬性(xing)的(de) set 又(you)是(shi) public 的(de)允許修改(gai),不僅暴露了封裝,還沒有起到隱藏的(de)效果!
那么,稍(shao)微改進(jin)下,試圖消滅 IsOnceSchedule 屬性,引入繼承機制。
1 public class Schedule 2 { 3 } 4 5 public class OnceSchedule : Schedule 6 { 7 } 8 9 public class RecurringSchedule : Schedule 10 { 11 }
實現(xian)上在(zai) OnceSchedule 和(he) RecurringSchedule 中均封裝獨立的實現(xian)。如果非要通(tong)過父類抽象暴露 Recurring 狀態(tai),可以(yi)在(zai)父類中通(tong)過屬性暴露只讀接口。
1 public class Schedule 2 { 3 public Schedule() 4 { 5 this.IsRecurring = false; 6 } 7 8 public bool IsRecurring { get; protected set; } 9 } 10 11 public class OnceSchedule : Schedule 12 { 13 public OnceSchedule() 14 : base() 15 { 16 this.IsRecurring = false; 17 } 18 } 19 20 public class RecurringSchedule : Schedule 21 { 22 public RecurringSchedule() 23 : base() 24 { 25 this.IsRecurring = true; 26 } 27 }
函數
我們或許都知道,函數命名要(yao)動詞(ci)開頭,如需(xu)要(yao)可與名詞(ci)結(jie)合。而(er)函數設(she)計(ji)的要(yao)求是盡量只做(zuo)一件事,這件事有時簡單有時困難(nan)。
簡單的可以像下面這種一句話代碼(ma):
1 internal bool CheckDateTimeWeekendDay(DateTimeOffset dateTime) 2 { 3 return dateTime.DayOfWeek == DayOfWeek.Saturday 4 || dateTime.DayOfWeek == DayOfWeek.Sunday; 5 }
復雜(za)的見(jian)到幾百行(xing)的函數也(ye)不新奇。拆解(jie)長函數的方法(fa)有很多,這么(me)不做(zuo)贅述。這里(li)推薦一種使(shi)用 C# 語法(fa)糖衍生出的函數設計(ji)方法(fa)。
上(shang)面的(de)小函(han)數(shu)其(qi)實(shi)是(shi)非常(chang)過程化的(de)代碼(ma),其(qi)是(shi)為類(lei) DateTimeOffset 服務,我們可以使用 C# 中的(de)擴展方法來(lai)優化這(zhe)個小函(han)數(shu)。
1 internal static class DateTimeOffsetExtensions 2 { 3 internal static bool IsWeekendDay(this DateTimeOffset dateTime) 4 { 5 return dateTime.DayOfWeek == DayOfWeek.Saturday 6 || dateTime.DayOfWeek == DayOfWeek.Sunday; 7 } 8 }
這(zhe)樣,我(wo)們就可(ke)以(yi)像下面這(zhe)樣使用了,感(gan)覺會不會好一些?
1 if(DateTimeOffset.Now.IsWeekendDay()) 2 { 3 // do something 4 }
在設計(ji)函數(shu)時,我們時常猶豫的是,到底應(ying)(ying)該返回一(yi)個(ge) null 值還是應(ying)(ying)該拋出一(yi)個(ge)異常呢?
答案就是,如(ru)果你(ni)總是期待(dai)函數返(fan)回(hui)一個(ge)(ge)值(zhi)(zhi)時,而值(zhi)(zhi)不(bu)存在則應該(gai)拋出(chu)異(yi)常;如(ru)果你(ni)期待(dai)函數可(ke)以(yi)(yi)返(fan)回(hui)一個(ge)(ge)不(bu)存在的值(zhi)(zhi),則可(ke)以(yi)(yi)返(fan)回(hui) null。總之,不(bu)要因為懶惰而使得應該(gai)設計(ji)拋出(chu)異(yi)常的函數最終(zhong)返(fan)回(hui)了 null,不(bu)幸的是,這(zhe)種懶惰經常出(chu)現。
正常(chang)的代碼是(shi)不需要 try..catch.. 的,異常(chang)就應該一拋(pao)到底(di)直至應用(yong)程(cheng)序崩潰(kui),當然(ran),這是(shi)開發階段。一拋(pao)到底(di)有利于發現已有代碼路徑中(zhong)的錯誤,畢竟異常(chang)在正常(chang)邏輯(ji)中(zhong)是(shi)不應該產(chan)(chan)生(sheng)的。我們要做(zuo)的是(shi),合(he)理期待某(mou)調用(yong)可(ke)能(neng)會產(chan)(chan)生(sheng)某(mou)類異常(chang),則直接 catch 該特定異常(chang),如 catch (System.IO.FileNotFoundException ex)。
實際上,遇到這種抉(jue)擇(ze)場景,我們可以在函數命名(ming)上下些功夫,以變相(xiang)解決(jue)問題。
1 object FindObjectOrNull(string key); 2 object FindObjectOrThrow(string key); 3 object FindObjectOrCreate(string key, object dataNeededToCreateNewObject); 4 object FindObjectOrDefault(string key, Object defaultReturnValue);
單元測試
在開始(shi)寫代(dai)碼的(de)時候就開始(shi)考慮測試(shi)(shi)問題(ti),有利于產生易于測試(shi)(shi)的(de)代(dai)碼。幸運的(de)是,對(dui)測試(shi)(shi)友好的(de)設計會很(hen)自(zi)然的(de)產生良好的(de)代(dai)碼。
測(ce)試(shi)驅動開發(TDD)是一種編(bian)程風格,包含 TDD 三定(ding)律:
- 在編寫不能通過的單元測試前,不能編寫生產代碼;
- 只編寫剛好無法通過的單元測試,不能編譯不算通過;
- 只編寫剛好通過當前失敗測試的生產代碼;
我們顯然可以(yi)循規(gui)蹈矩的(de)遵循上述(shu) TDD 三定(ding)律(lv)風(feng)格(ge)(ge)編程(cheng),但 TDD 只是(shi)通過測試(shi)來(lai)保證代碼質(zhi)量,驅動(dong)良(liang)好設計的(de)一(yi)種(zhong)風(feng)格(ge)(ge),我們沒有(you)必要完全強(qiang)迫(po)自(zi)己(ji)遵循上述(shu)定(ding)律(lv),找(zhao)到適合自(zi)己(ji)的(de)過程(cheng)可能效率更(geng)高,所以(yi)重點在于,要寫(xie)(xie)單元(yuan)測試(shi),通過寫(xie)(xie)代碼時思考測試(shi)這件事來(lai)幫助把代碼寫(xie)(xie)的(de)更(geng)好。
測試代(dai)碼不是(shi)二(er)等(deng)公民,它和生(sheng)產代(dai)碼一(yi)樣重要(yao)。他(ta)需要(yao)被思(si)考、被設計、被維護,并且要(yao)像生(sheng)產代(dai)碼一(yi)樣保持優雅(ya)的風(feng)格(ge)。
單元測試測什么?
在單元測試中,可通過兩種方式來驗證代碼是否正確地工作。一種是基于結果狀態的測試,一種是基于交互行為的測試。這兩種方式在文章《單元測試的兩種方式》中有描(miao)述(shu),這里就不(bu)再贅(zhui)述(shu)。
單元測試的可讀性
在測試(shi)代碼中,可(ke)(ke)讀性(xing)(xing)仍然(ran)很重要。如果測試(shi)代碼的可(ke)(ke)讀性(xing)(xing)良好(hao),使其更易(yi)于后期(qi)的維(wei)護和修改,不(bu)至于是測試(shi)代碼腐化以致被(bei)刪除。
下面是一些良好測(ce)試(shi)的(de)關鍵(jian)點:
- 測試越簡明越好,每個測試只關注一個點。
- 如果測試運行失敗,則其應發出有幫助性的錯誤消息或提示。
- 使用簡單明確的測試輸入條件。
- 給測試用例取一個可描述的名字。
那么,具體什么樣的單元(yuan)測試用例(li)名(ming)稱,算是好名(ming)稱呢?這里推薦兩(liang)種(zhong):
- 第一種:使用 Test_<ClassName>_<FunctionName>_<Situation> 風格;
- 第二種:使用 Given_<State>_When_<Behavior>_Then_<SomethingHappen> 風格;
第二種實際上是 BDD 風格,其不僅可以應用于單元測試,在更高級的 Component Level 和 System Level 的測試(shi)中同樣有效。
實際上,單元測試用例代(dai)碼的內部實現也是有風格可遵循(xun),常見的就是 模(mo)式。
第三方組件代碼不便于測試
在文章《類依賴項的不透明性和透明性》中描述了依賴(lai)項對單元(yuan)測(ce)試的(de)影響,實踐中,我們碰到(dao)最多的(de)是調用其他類(lei)庫(ku)的(de)代碼而導致的(de)不可測(ce)試性。
1 public class MyClass 2 { 3 private Job _job; 4 5 public MyClass() 6 { 7 _job = new Job(); 8 } 9 10 public void ExecuteJob() 11 { 12 _job.Execute(); 13 } 14 } 15 16 public sealed class Job 17 { 18 public void Execute() 19 { 20 // do something heavy 21 } 22 }
上面(mian)的代碼,如果(guo)寫一個 TestCase 的話(hua),可能是下面(mian)這種情況。
1 [Test] 2 public void Test_MyClass_ExecuteJob() 3 { 4 MyClass instance = new MyClass(); 5 instance.ExecuteJob(); 6 7 // what should we assert? 8 }
這(zhe)樣,調用(yong)了(le) instance.ExecuteJob() 導致了(le)不知道如何驗證。同時(shi),由于 Job 類使用(yong)了(le) sealed 關(guan)鍵字(zi),并(bing)且沒有實(shi)現(xian)任(ren)何接(jie)口,所以也(ye)無法通過 mocking 庫來 mock。
解決辦法(fa),增加(jia)中間層。
1 public class MyClass 2 { 3 private IJob _job; 4 5 public MyClass(IJob job) 6 { 7 _job = job; 8 } 9 10 public void ExecuteJob() 11 { 12 _job.Execute(); 13 } 14 } 15 16 public class JobProxy : IJob 17 { 18 private Job _realJob; 19 20 public JobProxy(Job job) 21 { 22 _realJob = job; 23 } 24 25 public void Execute() 26 { 27 _realJob.Execute(); 28 } 29 } 30 31 public interface IJob 32 { 33 void Execute(); 34 } 35 36 // third-party Job Class 37 public sealed class Job 38 { 39 public void Execute() 40 { 41 // do something heavy 42 } 43 }
這樣,我們在測試 MyClass 類時,就可以通過 IJob 接口注入 Mock 對象。這里選用的 Mocking Library 是 NSubstitute,參考《NSubstitute完全手冊索引》。
1 [Test] 2 public void Test_MyClass_ExecuteJob() 3 { 4 IJob job = Substitute.For<IJob>(); 5 6 MyClass instance = new MyClass(job); 7 instance.ExecuteJob(); 8 9 // assert 10 job.Received(1).Execute(); 11 }
依賴時間的測試
還有一(yi)種較難測試的代(dai)碼(ma)(ma)是依(yi)賴于時間的代(dai)碼(ma)(ma)。比如(ru),我們有一(yi)個依(yi)賴于時間的 Trigger 類(lei),簡寫(xie)為這個樣子(zi)。
1 public class Trigger 2 { 3 public Trigger(DateTime triggeredTime) 4 { 5 this.TriggeredTime = triggeredTime; 6 } 7 8 public DateTime TriggeredTime { get; private set; } 9 10 public bool TryExecute() 11 { 12 if (DateTime.Now >= TriggeredTime) 13 { 14 // do something 15 return true; 16 } 17 18 return false; 19 } 20 }
測試時(shi),我可(ke)能(neng)會挑一些特定時(shi)間進(jin)行測試,特定時(shi)間有可(ke)能(neng)在很遠的未來(lai)。
1 [Test] 2 public void Test_Trigger_TryExecute_AfterTriggeredTime() 3 { 4 DateTime triggeredTimeInFuture = 5 new DateTime(2016, 2, 29, 8, 0, 0, DateTimeKind.Local); 6 7 Trigger trigger = new Trigger(triggeredTimeInFuture); 8 bool result = trigger.TryExecute(); 9 10 // assert 11 Assert.IsTrue(result); 12 }
好(hao)吧(ba),這個 TestCase 應該是到 2016 年才能執行成功,顯然不是我們(men)期待的(de)(de)。改進的(de)(de)辦法(fa)還是增加中(zhong)間層,增加 IClock 接(jie)口用于提供(gong)時間。
1 public class Trigger 2 { 3 private IClock _clock; 4 5 public Trigger(IClock clock, DateTime triggeredTime) 6 { 7 _clock = clock; 8 this.TriggeredTime = triggeredTime; 9 } 10 11 public DateTime TriggeredTime { get; private set; } 12 13 public bool TryExecute() 14 { 15 if (_clock.Now() >= TriggeredTime) 16 { 17 // do something 18 return true; 19 } 20 21 return false; 22 } 23 } 24 25 public interface IClock 26 { 27 Func<DateTimeOffset> UtcNow { get; } 28 Func<DateTimeOffset> Now { get; } 29 } 30 31 public class Clock : IClock 32 { 33 public Func<DateTimeOffset> UtcNow { get { return () => DateTimeOffset.UtcNow; } } 34 public Func<DateTimeOffset> Now { get { return () => DateTimeOffset.Now; } } 35 }
這樣,我們就(jiu)可以在(zai) TestCase 代(dai)碼中使用 Mocking 類庫來替換(huan) IClock 的實例,進(jin)而指定時間。
1 [Test] 2 public void Test_Trigger_TryExecute_AfterTriggeredTime() 3 { 4 IClock clock = Substitute.For<IClock>(); 5 6 clock.Now 7 .Returns<Func<DateTimeOffset>>(() => 8 { 9 return DateTimeOffset.Parse( 10 "2016-02-29T08:00:01.0000000", CultureInfo.CurrentCulture); 11 }); 12 13 DateTime triggeredTimeInFuture = 14 new DateTime(2016, 2, 29, 8, 0, 0, DateTimeKind.Local); 15 16 Trigger trigger = new Trigger(clock, triggeredTimeInFuture); 17 bool result = trigger.TryExecute(); 18 19 // assert 20 Assert.IsTrue(result); 21 }
本篇文章《代碼的印象派》由 Dennis Gao 原創發表自博客園個人博客,未經作者本人同意禁止以任何的形式轉載,任何自動的或人為的爬蟲轉載行為均為耍流氓。
