我在編程初級階段常犯的錯誤
學會辨識錯誤赁酝,養(yǎng)成習慣去避免它們
我要先聲明一點厂抖,如果你是一個初級程序員,本文并非要讓你因為你可能正在犯這些錯感到藍瘦香菇帘瞭,而是想讓你意識到它們洋丐,學會如何辨識這些錯誤呈昔,并且提醒你避免犯這些錯。
過去我經(jīng)常犯這類錯誤友绝,從每一個錯誤中我都吸取了很多教訓堤尾。可喜的是如今我已經(jīng)養(yǎng)成了很好的編程習慣迁客,這些習慣能幫我避免再次犯同樣的錯郭宝。你也應該嘗試著去這樣做。
以下錯誤排名不分先后掷漱。
1)毫無計劃地寫代碼
高質(zhì)量的寫作內(nèi)容大都不那么容易產(chǎn)出粘室,它要求仔細的思考和研究。高質(zhì)量的程序代碼也不例外卜范。
編寫高質(zhì)量代碼是有一個工作流的:
思考衔统。調(diào)研。計劃。編碼锦爵。驗證舱殿。修改。
很遺憾棉浸,這個工作流沒有一個很好的(英文)首字母縮寫來幫助記憶怀薛。你需要養(yǎng)成好的習慣來履行工作流中的各個階段,一個都不能少迷郑。
在我編程初級階段的時候枝恋,我犯過最嚴重的錯誤之一就是寫代碼之前沒有思考和調(diào)研。雖然這在小型獨立的項目中能夠奏效嗡害,但在更大的工程中就會有很嚴重的負面影響了焚碌。
在說出你可能會后悔的話之前,你要三思而后說霸妹。編程也一樣十电,在你寫下可能會后悔的代碼之前,那你就要三思而后寫了叹螟。代碼也是交流思想的一種方式鹃骂。
生氣的時候,如果要說話罢绽,先從 1 數(shù)到 10畏线。如果非常生氣,那就數(shù)到 100良价。
— Thomas Jefferson.
套用一下這句話:
復審代碼的時候寝殴,如果要重構(gòu)代碼,先從 1 數(shù)到 10明垢。如果沒有測試代碼蚣常,那就數(shù)到 100。
— Samer Buna
編程大部分情況下都是關于如何閱讀以前的代碼痊银,調(diào)研需求及其如何適應當前系統(tǒng)抵蚊,以及使用可測試的增量代碼實現(xiàn)新功能。實際寫代碼的時間在整個過程中可能才占比 10% 溯革。
不要覺得編程就是寫一行一行的代碼贞绳。編程是一個有邏輯的創(chuàng)造過程,是需要培養(yǎng)教育的鬓照。
2)寫代碼前過度計劃
是的熔酷。在一頭鉆進代碼前做點計劃是好事,但是即便是好事豺裆,也可能物極必反拒秘。喝太多的水都會使你中毒呢号显。
在編程的世界里尋找完美的計劃?不存在的躺酒。要尋找一個足夠好的計劃押蚤,足夠讓你啟動項目就行了。因為計劃總是趕不上變化羹应,但是計劃可以推動你向有組織的方向進行揽碘,這會使你的代碼更清晰。計劃太多不過是浪費時間而已园匹。
我現(xiàn)在講的都只是對于小功能的計劃雳刺,想在一開始就計劃好所有功能的做法就更不可取裸违!這在軟件工程中被叫做瀑布模型掖桦,是一種系統(tǒng)線性的計劃,每一個步驟都按順序完成供汛∏雇簦可以設想這種瀑布模型需要多少計劃啊。這不是這里所討論的計劃類型怔昨。瀑布模型在大多數(shù)軟件工程中都是無效的雀久。任何復雜的事物都只能根據(jù)現(xiàn)實靈活適應來實現(xiàn)。
編寫程序必須是一個響應的過程趁舀。你會添加以前從未想過的新功能赖捌,這在瀑布模型中是無法想象的(譯注:因為瀑布模型把整個生命周期都計劃好了,不會有意外的功能)赫编,你也會因為從未想過的原因移除一些功能巡蘸。你需要修復 bug 以適應變化奋隶。你需要靈活一些擂送。
雖然如此(不能過度計劃),但一定要計劃一下后續(xù)的少數(shù)新功能唯欣。要很小心地計劃嘹吨,因為不足或者過度的計劃都可能損害你代碼的質(zhì)量,可不能拿代碼質(zhì)量來冒風險境氢。
3)低估代碼質(zhì)量的重要性
如果你只能夠關注你所寫的代碼的一個方面蟀拷,那么肯定是可讀性。表意不明的代碼就是垃圾萍聊,甚至是不可回收的垃圾问芬。
永遠都不要低估代碼質(zhì)量的重要性。把編寫代碼看作是一種溝通實現(xiàn)的方式寿桨。作為程序員最主要的工作就是清晰地交流當前解決方案的實現(xiàn)此衅。
關于編程强戴,我最喜歡的名言之一是:
總是以這樣的心態(tài)寫代碼:仿佛最終維護你代碼的那個人是個變態(tài)暴力狂,他知道你住在哪里挡鞍。
— John Woods
Jonh, 真是個明智的建議骑歹!
即便是微小的細節(jié)也很重要。舉個例子墨微,如果你在你的代碼中道媚,縮進和大小寫的風格不一致,你簡直就該被“吊銷編程執(zhí)照”翘县。
tHIS is
WAY MORE important
than
you think
另一點是關于長行的使用最域。任何超過 80 個字符的代碼行都要難讀很多。你可能試圖把一些很長的條件判斷放在同一行锈麸,好讓 if
語句更清晰羡宙,別這么做。永遠都不要讓一行代碼超過 80 個字符掐隐,永遠都不要狗热。
很多這樣的簡單問題都可以通過 linting 和 formatting 工具修復。在 JavaScript 中虑省,有兩個非常棒的工具可以完美協(xié)作:ESLint 和 Prettier匿刮。給自己行行好,把它們用起來吧探颈。
以下還有一些和代碼質(zhì)量相關的誤區(qū):
在一個方法或一個文件中寫非常多行代碼熟丸。你應該總是把很長的代碼拆分成小的片段,以便測試和單獨管理伪节。我個人認為超過 10 行的方法都算太長了光羞,但這只是個經(jīng)驗之談。
使用雙重否定怀大。拜托纱兑,請別不要不這么做(譯注:此處故意使用雙重否定╮(╯▽╰)╭)。
使用雙重否定也并不是不會錯(譯注:此處作者故意使用雙重否定)
- 使用短而通用化借、或基于類型的變量名潜慎。給你的變量一個描述性和沒有歧義的名字。
計算機科學中只有兩件難事:清除緩存和命名蓖康。
—?Phil Karlton
- 毫無描述地硬編碼字符串字面量和數(shù)字字面量(譯注:即“魔數(shù)”)铐炫。如果你需要寫一些依賴固定字面量字符串值或數(shù)字值的代碼,那就使用常量保存這些固定值蒜焊,并起一個好變量名倒信。
const answerToLifeTheUniverseAndEverything = 42;
使用邋遢的捷徑和奇技淫巧避免在簡單問題上花費更多時間秋柄。不要與問題共舞贱纠,直面現(xiàn)實吧。
覺得代碼越長越好。然而在大多數(shù)情況下税肪,代碼是越短越好褥傍。只有以代碼可讀性更強為前提吴藻,才用長代碼的寫法暖途。舉個例子,不要內(nèi)嵌大量三目運算符(
?:
)來讓代碼變短瘾带。當然鼠哥,也不要有意讓代碼變得沒必要的冗長。刪除沒必要的代碼在任何項目中都是你能做的最好的事看政。
使用代碼行數(shù)來衡量編程進度就像使用重量來衡量飛行器的建造進度一樣朴恳。
—?Bill Gates
- 過度使用條件邏輯。你覺得需要條件邏輯的大多數(shù)情況都可以不使用條件邏輯來實現(xiàn)允蚣∮谟保考慮所有的代替方案,基于可讀性嚷兔,僅僅選擇其中的一種。除非你已經(jīng)可以測量性能了冒晰,否則不要優(yōu)化性能同衣。相關:避免 Yoda conditions 和根據(jù)條件賦值。
4)使用初次方案
還記得在我剛開始編程的時候壶运,當我遇到一個問題時耐齐,我能找到一個解決方案然后馬上就投入這個方案中,我急忙忙地實現(xiàn)它蒋情,沒有考慮過這個初次方案復雜度和潛在的失敗情況埠况。
雖然初次方案可能很誘人,但是好的方案卻往往是在你開始盤查多種方案時才發(fā)現(xiàn)的棵癣。如果你沒辦法找出這個問題的多種解決方案辕翰,那很可能是你沒有完全明白這個問題。
作為專業(yè)的程序員浙巫,你的職責不是找出問題的一個解決方案金蜀,而是找出問題的最簡單的解決方案刷后。我所說的“簡單的解決方案”是指這個解決方案必須是準確的的畴,性能足夠好,還要簡單易讀尝胆、易理解和易維護丧裁。
有兩種方式來構(gòu)建軟件設計。一種是把它做得足夠簡單以至于明顯沒有缺陷含衔,另一種是把它做得足夠復雜以至于沒有明顯的缺陷煎娇。
— C.A.R. Hoare
5)不放棄
另一個我經(jīng)常犯的錯誤是我堅持我的初次解決方案二庵,即使我已經(jīng)確認了這可能不是最簡單的解決方案。這可能就是心理學上所說的“不放棄”心態(tài)吧缓呛。這在大多數(shù)活動中是一種好的心態(tài)催享,但在編程中卻不適用。事實上哟绊,當說到編程的時候因妙,正確的心態(tài)應該是盡快失敗和多多失敗。
當你開始懷疑一個解決方案的時候票髓,你就應該考慮拋棄它攀涵,并且重新思考這個問題。不管你已經(jīng)在這個解決方案中投入了多少精力洽沟。像 GIT 這樣的版本控制系統(tǒng)能夠幫助你分開管理和嘗試多種不同的解決方案以故,把它利用起來吧。
不要因為你在代碼中花費了很多精力就為它著了魔裆操。壞的代碼就應該被丟棄怒详。
6)不谷歌(譯注:不使用搜索引擎)
我花費了很多寶貴的時間去嘗試解決一個問題,但我本可以簡單地使用搜索引擎就能搜索到結(jié)果踪区,這樣的例子數(shù)不勝數(shù)棘利。
除非你正在使用一種極其邊緣的技術,否則當你遇到一個問題時朽缴,很可能別人早就遇到過同樣的問題了善玫,并且也找到了解決方案了。給自己省點時間密强,先 Google 一下茅郎。
有時候,Google 一下可能會披露這樣一個事實:你覺得這是個問題但其實那并不是或渤,你需要做的并非修復它系冗,而是擁抱它。也不要覺得你知道了尋找解決方案必備的所有知識薪鹦,Google 會讓你吃驚掌敬。
盡管如此,你在 Google 搜索時需要小心池磁。一個新手的標志就是在沒有理解的情況下就復制粘貼別人的代碼奔害,盡管這些代碼可能正確地解決了你的問題,但你永遠都不應該使用你沒有完全理解的代碼地熄,哪怕只有一行华临。
如果你想成為一個有創(chuàng)造性的程序員,不要以為你知道自己在做什么端考。
作為一個有創(chuàng)造力的人雅潭,最危險的想法就是以為你知道自己在做什么揭厚。
— Bret Victor
7)沒有封裝
這一點不只是關于面向?qū)ο蠓妒降摹J褂梅庋b總是有用的扶供。不使用封裝往往導致難以維護的系統(tǒng)筛圆。
在一個應用中,一個功能應該只在一個地方被處理椿浓,這通常就是一個對象應有的職責顽染,這個對象應該只向其他需要用到它的對象暴露必須的接口。這無關乎機密轰绵,而是為了降低一個應用中的不同部分之間的相互依賴粉寞。堅持這些法則能夠讓你安全地改變你的類、對象左腔、函數(shù)的內(nèi)部實現(xiàn)唧垦,而不用擔心破壞了修改之處外更大范圍的東西。
概念的邏輯單元和狀態(tài)應該有他們自己的類液样。我說的類是指一個藍圖模版振亮,這可能確實是一個類對象,也可能是一個函數(shù)對象鞭莽,你也可以認為是一個模塊或一個包坊秸。
在一個邏輯單元以內(nèi),自包含的任務塊應該有他們自己的方法澎怒,一個方法應該只做一件事褒搔,并把這件事做好。相似的類應該使用相同的方法名喷面。
作為一個初級程序員星瘾,我以前經(jīng)常都無法自然地寫一個新的類來組織概念性的單元,也經(jīng)常無法辨識什么才是自包含的惧辈。如果你看到了一個“Util
“工具類琳状,就像一個垃圾場,堆放了很多互不關聯(lián)的代碼盒齿,這就是新手寫的代碼的特點念逞。如果你做了個微小的改動,發(fā)現(xiàn)這個改動有連鎖反應边翁,需要改動很多其他地方翎承,那就是新手寫的代碼的另一個特點。
在往一個類添加一個方法或者向一個方法添加更多職責的時候倒彰,思考一下审洞,并且問問你的本能。這里你需要花費點時間待讳,不要跳過或者想著“我稍后再來重構(gòu)”芒澜,剛開始的時候就要做。
基本的想法就是你想你的代碼高內(nèi)聚和低耦合创淡,這只是個時髦一點的術語痴晦,意思是說保持相關的代碼在一起(在一個類中),降低不同類之間的相互依賴琳彩。
8)為未知做計劃
經(jīng)常會傾向于去考慮超出當前正在寫的解決方案的問題誊酌。每寫一行代碼就有各種各樣的“萬一”在你腦海浮現(xiàn)。這在測試邊界情況的時候是很好的習慣露乏,但如果僅僅是作為驅(qū)動你去考慮可能的需求的時候碧浊,就大錯特錯了。
你需要辨識你的這些“萬一”屬于上面說的兩種類型中的哪一類瘟仿。不要編寫你現(xiàn)在不需要的代碼箱锐。不要為未知的將來作計劃。
因為你覺得可能以后會使用到一個功能劳较,就去編寫代碼實現(xiàn)它驹止,這是很顯然的錯誤。不要這樣做观蜗。
盡可能編寫目前正在實現(xiàn)的方案所需的最少量代碼臊恋。當然,要處理邊界情況墓捻,但不要添加邊界功能抖仅。
為了增長而增長是癌細胞的思想。
— Edward Abbey
9)沒有使用合適的數(shù)據(jù)結(jié)構(gòu)
初級程序員在準備面試的時候通常會太過關注算法砖第。能夠辨識好的算法并在需要的時候使用是很好的事岸售。但記憶這些算法可不會給你的編程技能帶來提升。
不同的是厂画,記憶你所用的編程語言中的各種數(shù)據(jù)結(jié)構(gòu)的優(yōu)缺點肯定能使你成為更好的開發(fā)者凸丸。
使用錯誤的數(shù)據(jù)結(jié)構(gòu)是一個巨大和明顯的廣告牌,上面寫著“這是新手的代碼“袱院。
本文并不是要教你數(shù)據(jù)結(jié)構(gòu)方面的知識屎慢,但這里快速舉幾個例子:
- 使用列表(數(shù)組)而不是映射表(對象)來管理記錄
最經(jīng)常犯的數(shù)據(jù)結(jié)構(gòu)方面的錯誤就是使用列表而不是映射表來管理一系列對象。沒錯忽洛,你應該使用映射表來管理一個記錄列表腻惠。
要注意的是我這里討論的記錄列表是其中的每一項記錄都有一個可以用于查找對象的唯一標識。使用列表來管理標量值是可以的欲虚,并且通常也是更好的選擇集灌,特別在重點用法是“壓入”一些值到列表的情況下。
在 JavaScript 中,最常用的列表結(jié)構(gòu)是數(shù)組欣喧,最常用的映射表結(jié)構(gòu)是對象(在現(xiàn)代 JavaScript 中也有映射表的結(jié)構(gòu))腌零。
使用列表而不使用映射表來管理對象通常都是錯的。盡管這個說法確實是在管理大量記錄的時候才成立唆阿,而我想說堅持一直這么做吧益涧。這么做很重要的主要原因就是使用唯一標識來查找對象的時候,映射表比列表要快得多驯鳖。
- 沒有使用堆棧
當編寫一些需要遞歸形式的代碼的時候闲询,通常很容易使用簡單的遞歸函數(shù)。然而浅辙,優(yōu)化遞歸代碼通常很難扭弧,特別是在單線程環(huán)境下。
優(yōu)化遞歸代碼取決于遞歸函數(shù)返回了什么记舆。比如說鸽捻,優(yōu)化一個返回調(diào)用自身兩次以上的遞歸函數(shù)比優(yōu)化只返回調(diào)用自身一次的函數(shù)要難得多。
作為初學者我們通常會忽視的就是其實有遞歸函數(shù)的替代方法氨淌。你可以使用棧數(shù)據(jù)結(jié)構(gòu)泊愧。手動把函數(shù)調(diào)用結(jié)果 Push 入棧,然后在需要獲取結(jié)果的時候把結(jié)果 Pop 出棧盛正。
10)把既有代碼弄得更糟
假設給你一個像這樣的凌亂的屋子:
現(xiàn)在要求你在這個房間里面放置一件東西。由于房間現(xiàn)在已經(jīng)是很混亂了痰滋,你很可能把東西隨便一放续崖,幾秒鐘就完事了敲街。
當你面對的是混亂的代碼的時候严望,千萬別這么做。不要把代碼弄得更亂像吻!總是要讓代碼比你剛接手的時候干凈那么一點峻黍。
以上房間問題中你應該做的是清理需要的部分拨匆,來給新的東西騰出合適的位置。比如說骨饿,如果這件東西是一件衣服,需要被放到衣柜里绒北,那么你就需要清理出一條到衣柜的路出來置鼻。這就是完成這個任務的正確方式箕母。
以下有一些錯誤的實踐俱济,通常會使得代碼比以前更糟糕(不完備的列表):
- 復制代碼蛛碌。如果你復制/粘貼一份代碼之后只改了一行,你簡直就是在產(chǎn)生重復代碼并且把代碼弄得更糟希太。放到以上凌亂房間的例子中就是酝蜒,你拿進了一張更低基座的椅子而不是考慮使用一張可調(diào)節(jié)高度的椅子⊥瞿裕總是要在心里想著抽象的概念堕澄,一旦可以就要使用它蛙紫。
- 沒有使用配置文件途戒。如果你要使用一個在其他環(huán)境下可能不一樣的值喷斋,或者在其他時間可能不一樣的值继准,這個值就應該放在配置文件中。如果你需要在你代碼的不同地方都使用一個值室谚,這個值也應該放置在配置文件中秒赤。當你要引入一個新的值到你的代碼中的時候,只需要問問你自己:這個值應不應該放在配置文件中陈瘦?答案很可能是“應該”痊项。
-
使用沒必要的條件語句和臨時變量鞍泉。每一個
if
語句都是一個需要測試兩次的邏輯分支肮帐。當你可以在不犧牲可讀性的情況下避免條件語句的時候训枢,你就應該這么做恒界。這里主要的問題在于使用分支邏輯擴展一個函數(shù)還是引入另一個函數(shù)仗处。每一次你覺得需要if
語句或一個新的函數(shù)變量的時候,你應該問問自己:我是否在正確的層次修改代碼吃环,還是說我應該在更高的層次考慮一下這個問題郁轻。
關于不必要的 if
語句好唯,看一下以下的代碼:
function isOdd(number) {
if (number % 2 === 1) {
return true;
} else {
return false;
}
}
以上的 isOdd
函數(shù)有幾個問題燥翅,但你能看出最明顯的一個嗎森书?
他使用了不必要的 if
語句,以下是一種等價的寫法:
function isOdd(number) {
return (number % 2 === 1);
};
11)給顯而易見的代碼寫注釋
如今我已經(jīng)學會了如何盡我所能去避免在寫注釋時面臨的難題了脏榆。大多數(shù)的注釋都可以使用更好命名的元素(譯注:類台谍、方法趁蕊、變量)替換介衔。
舉個例子炎咖,不要寫下面這樣的代碼:
// This function sums only odd numbers in an array
const sum = (val) => {
return val.reduce((a, b) => {
if (b % 2 === 1) { // If the current number is even
a+=b; // Add current number to accumulator
}
return a; // The accumulator
}, 0);
};
同樣的代碼可以不需要注釋乘盼,像這樣重寫:
const sumOddValues = (array) => {
return array.reduce((accumulator, currentNumber) => {
if (isOdd(currentNumber)) {
return accumulator + currentNumber;
}
return accumulator;
}, 0);
};
僅僅是給函數(shù)起更好的的名字就可以使大多數(shù)注釋變得沒必要俄烁,在寫注釋前請記住這一點页屠。
然而辰企,有時候你可能被迫進入這樣的情況牢贸,只有通過增加注釋才能提高代碼的清晰性。這時候你就應該組織你的注釋來回答為什么是用這段代碼而不是這段代碼是干嘛的臭增。
如果你強烈地想要寫一段注釋來解釋“這段代碼是干什么的”誊抛,以增加代碼清晰性拗窃,請不要寫那些顯而易見的并炮。以下是一個例子,其中無用的注釋只會徒增代碼的干擾性荤西。
// 創(chuàng)建一個變量并初始化為 0
let sum = 0;
// 遍歷數(shù)組
array.forEach(
// 對于數(shù)組中的每一個數(shù)字
(number) => {
// 把當前數(shù)字加到變量 sum 中
sum += number;
}
);
別做上面那樣的程序員邪锌。也不要接受這樣的代碼觅丰。不得不處理的情況下妇萄,刪了那些注釋咬荷。如果你碰巧雇用了寫出上面那樣注釋的程序員幸乒,炒了他罕扎,馬上炒腔召。
12)沒有寫測試
我將簡單地闡述這一點宴咧,如果你覺得你是個程序員專家并且這樣的想法給你寫代碼不帶測試的自信,在我的字典里你就是個新手烙肺。
如果你不在代碼里寫測試的話桃笙,那么你很可能在用某些手動的方式測試你的代碼搏明。如果你在構(gòu)建一個網(wǎng)頁應用的話星著,每次修改幾行代碼你就得在瀏覽器中刷新然后做一些交互來再次測試虚循。我也是這么做的。手動測試代碼沒有錯铺遂,但你應該在手動測試完之后襟锐,找到一種自動化測試的方法粮坞。如果你在你的應用中測試了一個交互功能捞蚂,那么在你添加更多功能代碼之前跷究,你就應該先把這個交互功能的測試用代碼自動化俊马。
你是一個人柴我,你就很難保證在每次修改完代碼之后還能把之前所做的所有測試校驗都再做一遍艘儒,那就讓計算機幫你做吧界睁。
如果可能的話兵拢,甚至在開始寫代碼實現(xiàn)需求之前说铃,你就應該開始預估和設計需要測試校驗的情況了。測試驅(qū)動開發(fā) (Testing-driven development, TDD)可不是什么花俏的炒作砾嫉,它是會實實在在會對你思考功能特性焰枢、尋找更好的設計方案產(chǎn)生積極影響的济锄。
測試驅(qū)動開發(fā)(TDD)并非對每一個人和對每一個項目都奏效的荐绝,但如果你能夠把它利用起來(哪怕只在項目的某一部分)低滩,你都完全應該這樣去做恕沫。
13)覺得代碼運行起來了就是能正確運行
看一下這個實現(xiàn)“把所有奇數(shù)相加”功能的函數(shù) sumOddValues
婶溯,有什么問題嗎偷霉?
const sumOddValues = (array) => {
return array.reduce((accumulator, currentNumber) => {
if (currentNumber % 2 === 1) {
return accumulator + currentNumber;
}
return accumulator;
});
};
console.assert(
sumOddValues([1, 2, 3, 4, 5]) === 9
);
測試斷言通過了类少,生活真美好啊硫狞,真的残吩,真的對了嗎世剖?
問題在于以上的代碼是不完備的旁瘫,在少數(shù)情況下它能正確處理,碰巧測試使用的斷言剛好就是這些情況中的一種遭庶,但除此之外還有很多問題峦睡,讓我們列舉其中的幾個:
- 問題一: 沒有處理空輸入榨了。如果這個函數(shù)被調(diào)用的時候沒有傳遞任何參數(shù)呢攘蔽?這種情況下就會產(chǎn)生一個錯誤,暴露了這個函數(shù)的內(nèi)部實現(xiàn)转捕。
TypeError: Cannot read property 'reduce' of undefined.
這通常是糟糕代碼的一個標志,理由如下:
- 函數(shù)的使用者不應該看到函數(shù)的具體實現(xiàn)枢步。
- 出錯的信息對用戶沒有任何幫助价捧,函數(shù)不起作用就是不起作用。但是渔彰,如果函數(shù)的出錯信息能對函數(shù)的使用方法描述得更清晰具體一點恍涂,函數(shù)的使用者就可能知道了是他們使用姿勢不當再沧。比如你可以選擇讓這個函數(shù)拋出自定義的異常炒瘸,像下面這樣:
TypeError: Cannot execute function for empty list.
除了拋出一個異常,你也可以重新設計這個函數(shù)拐邪,忽略掉空的輸入扎阶,然后返回 0
东臀。不管怎么樣惰赋,在這種情況下你都應該做些處理谤逼。
- 問題二: 沒有處理異常輸入流部。如果調(diào)用函數(shù)的時候沒有傳遞一個數(shù)組枝冀,而是傳入了一個字符串果漾,一個整數(shù)绒障,或者一個對象户辱,此時會發(fā)生什么庐镐?
現(xiàn)在這個函數(shù)就會拋出這樣的錯誤了:
sumOddValues(42);
TypeError: array.reduce is not a function //(譯注:array.reduce 不是一個函數(shù))
好吧必逆,真是不幸名眉,因為 array.reduce
絕對是一個函數(shù)拌嫡搿探橱!
由于我們給函數(shù)的參數(shù)命名為 array
隧膏,任何調(diào)用這個函數(shù)的參數(shù)(即以上例子中的 42
)在函數(shù)內(nèi)部都會打上 array
的標簽胞枕,這個錯誤其實就是說 42.reduce
不是一個函數(shù)腐泻。
你親眼看到了這樣的錯誤多么令人費解构诚,不是嗎铆惑?也許更有幫助的錯誤是這樣的:
TypeError: 42 is not an array, dude. // (譯注:42 可不是數(shù)組啊丑蛤,我的大胸弟受裹。)
問題一和問題二有時候被稱為邊界情況名斟,有一些基本的邊界情況要考慮,但是也經(jīng)常有一些不那么明顯的邊界情況坑律,也需要考慮進來晃择。比如說宫屠,如果我們傳遞了負數(shù)作為參數(shù)呢浪蹂?
sumOddValues([1, 2, 3, 4, 5, -13]) // => 還是 9
呃坤次,-13
也是一個奇數(shù)缰猴。這是你預期的行為嗎闷堡?是不是應該拋出異常杠览?負數(shù)是不是也應該被求和加起來?還是說像它現(xiàn)在的行為這樣焰扳,僅僅忽略掉負數(shù)倦零?現(xiàn)在你可能會意識到這個函數(shù)的名稱本來應該被叫做 sumPositiveOddNumbers
(對正奇數(shù)進行求和)。
這種情況下做決策很容易吨悍,但更重要的一點是扫茅,如果你不寫測試用例記錄你這次決策的原因,這個函數(shù)將來的維護者可能會不知道你是有意忽略掉負數(shù)還是這里有 bug育瓜,并對此毫無頭緒。
這不是 bug 躏仇,這是特性恋脚。
— 忘了寫測試代碼的某某
- 問題三: 并非所有的有效情況都被測試了。拋開邊界情況不說焰手,這個函數(shù)還有一種合法的糟描、非常簡單的情況沒有被正確處理:
sumOddValues([2, 1, 3, 4, 5]) // => 11
以上的 2
也被加到求和結(jié)果里面去了,但這本不應該书妻。
答案很簡單船响,reduce
接受另一個參數(shù),作為求和器的初始值。如果這個參數(shù)沒有傳遞的話(如上代碼)见间,reduce
函數(shù)就會用集合的第一個值作為求和器的初始值聊闯。這就是以上例子中第一個偶數(shù)也會被求和加起來的原因。
盡管你可能在你一開始寫代碼的時候就馬上意識到這個問題了米诉,但是暴露出這個問題的測試用例還是應該首先被包含到測試集中菱蔬,跟其他很多基本測試用例一起,如“傳遞全是偶數(shù)的數(shù)組”史侣,“傳遞包含 0
的數(shù)組“拴泌,還有”傳遞空數(shù)組“。
如果你看到了很少量的測試用例抵窒,還沒有處理大多數(shù)甚至根本不處理邊界情況弛针,那就是新手代碼的另一個標志。
14)沒有質(zhì)疑既有代碼
除非你是個一直單飛的超級碼農(nóng)李皇,否則毫無疑問削茁,你在生活中肯定會遇到一些很傻逼的代碼。初學者通常無法辨別這些代碼的好壞掉房,并且通常會覺得這些代碼是好代碼茧跋,因為這些代碼似乎正常運行,還在代碼倉庫里面待了很長時間卓囚。
更糟的是瘾杭,如果糟糕的代碼用了一些糟糕的實踐,初學者很可能就在代碼倉庫里的其他地方應用了這些糟糕的實踐哪亿,因為他們把這些當作好代碼給學習過來了粥烁。
有一些代碼看起來很糟糕,但可能是有一些特殊的情況迫使開發(fā)者寫出這樣的代碼蝇棉,這就是應該編寫詳細注釋的地方了讨阻,可以在這里告訴初學者這些特殊的情況以及代碼這樣寫的原因。
作為一個初學者篡殷,總是應該假定那些你讀不懂的钝吮、且沒有文檔注釋的代碼很可能就是糟糕的代碼。質(zhì)疑之板辽,詢問之奇瘦,使用 git blame
揪出罪魁禍首!
如果代碼作者已經(jīng)離開很久了劲弦,或者他自己也記不起來了耳标,那就好好研究這些代碼,盡力弄懂相關的一切邑跪。只有當你完全理解了這份代碼麻捻,你才能夠建立起這份代碼好壞的認知纲仍,在那之前不要做任何假設哭尝。
15)迷戀最佳實踐
我覺得“最佳實踐”其實是害人的养筒,它暗示著你不需要深入研究它盟迟,這就是有史以來最佳實踐,不用質(zhì)疑明棍!
沒有最佳實踐這回事,也許有目前來說寇僧、針對這門語言的摊腋、好的實踐。
一些以前被認為是編程中最佳實踐的嘁傀,現(xiàn)在卻被貼上了最差實踐的標簽兴蒸。
如果你投入足夠多實踐,你總是可以找到更佳實踐细办。別被最佳實踐困擾了橙凳,把能做的做到最好。
不要因為你在某些地方讀到過笑撞,說可以這么做你就去這么做岛啸,也不要因為你看過別人這么做你也去做,也不要因為別人說這是最佳實踐你就這么做茴肥,包括本文中給出的所有建議坚踩!質(zhì)疑一切,挑戰(zhàn)權威瓤狐,了解所有可能的選擇瞬铸,作出明智的決定。
16)迷戀性能
過早優(yōu)化是萬惡之源
— Donald Knuth (1974)
盡管自 Donald Knuth 寫下上面的言論以來础锐,計算機編程已經(jīng)發(fā)生了翻天覆地的變化嗓节,我覺得這句話在今天仍然是很有價值的建議。
記住這一點的法則就是:如果你無法測量出被懷疑有性能問題的代碼郁稍,那就不要試圖去優(yōu)化它赦政。
如果你在運行代碼之前就在優(yōu)化它了,那很可能你就是在過早優(yōu)化代碼了耀怜,也很可能你正在費時費力做的優(yōu)化是完全沒必要的恢着。
當然了,有一些很明顯的優(yōu)化是你必須在引入新代碼前就要考慮的财破。比如說在 Node.js 中掰派,不要讓事件泛濫成災或阻塞調(diào)用棧,這是至關重要的左痢。這是你應該始終牢記的早期優(yōu)化的一個例子靡羡。
在未經(jīng)測量的既有代碼中進行的任何不明顯的優(yōu)化都是有害的系洛,也應該盡量避免。你可能覺得完成之后是一種性能收益略步,然而結(jié)果卻可能是新的描扯、意料之外的 bug 的源頭。
不要浪費時間去優(yōu)化未經(jīng)測量的性能問題趟薄。
17)沒有以最終用戶體驗為目標
給應用新增一個功能最簡單的方法是什么绽诚?從你自己的視角來看這個功能,或者看新功能如何融入到目前的用戶界面中杭煎,對吧恩够?如果新功能是要從用戶那里獲取一些輸入,那就在你已有的表單上添加羡铲,如果新功能是要在頁面上添加一個鏈接蜂桶,那就在你已有的菜單列表里面添加。
別做那樣的開發(fā)者也切。 要做專業(yè)的開發(fā)者扑媚,站在最終用戶的角度看問題。專業(yè)的開發(fā)者要考慮這個特定功能的用戶需要什么贾费、怎樣使用钦购,要想方設法使得這個功能容易讓用戶發(fā)現(xiàn)和使用,而不是想方設法在應用中用最便捷添加這個功能褂萧,毫不考慮這個功能的可發(fā)現(xiàn)性和可用性押桃。
18)沒有為任務挑選合適的工具
每一人都有一個最喜愛工具的列表,在他們的編程相關的活動中起到輔助的作用导犹。一些工具很優(yōu)秀唱凯,也有一些工具很辣雞,但是大部分工具都很擅長處理某一特定的任務谎痢,對除此之外的任務就不那么在行了磕昼。
錘子是把釘子釘進墻壁里的絕妙工具,但是用來擰螺絲就是最差的工具了节猿。不要因為你很喜愛那個錘子你就用它來擰螺絲票从。不要因為這是個很流行的錘子、在亞馬遜上用戶評分有 5.0 分就用它來擰螺絲滨嘱。
依賴一個工具的流行度而不是它對一個問題的適用性來選擇工具是真正新手的標志峰鄙。
關于這點有一個問題是,你可能不知道適用某個特定任務的“更好的”的工具太雨。在你目前的認知里吟榴,這個工具可能就是你所知道的最好的。但是囊扳,如果跟其他可選工具做比較的話吩翻,它就不是首選了兜看。你需要使自己了解這些可選的工具,對這些新工具保持開放的心態(tài)狭瞎,以后你可能會使用到它們细移。
一些程序員不愿意使用新的工具。他們對于目前正在使用的工具很滿意脚作,也不想學習新的工具葫哗。我理解這種做法,但這種做法很明顯是錯的球涛。
你可以使用最原始的工具建造房子,然后享受甜蜜時光校镐。你也可以花費一些時間和金錢去了解先進的工具亿扁、更快地建造更好的房子。工具在不斷地改進中鸟廓,你要樂意去學習它們从祝、使用它們。
19)不理解代碼問題會導致數(shù)據(jù)問題
程序的一個重要方面通常是管理某些形式的數(shù)據(jù)引谜。程序就是一個添加新數(shù)據(jù)牍陌、刪除舊數(shù)據(jù)、修改已有數(shù)據(jù)的接口员咽。
即使是程序中最小的 bug 也會導致它所管理的數(shù)據(jù)去到一種不可預測的狀態(tài)毒涧。尤其是當所有數(shù)據(jù)校驗都完全在這個有 bug 的程序中進行時。
當涉及到“代碼-數(shù)據(jù)”關系時贝室,初學者可能無法馬上理解其中的聯(lián)系契讲。他們可能覺得在生產(chǎn)環(huán)境中繼續(xù)使用這段有 bug 的代碼沒什么大不了的,因為失效的功能 X 也不是超級重要滑频。問題在于這段有 bug 的代碼會持續(xù)產(chǎn)生數(shù)據(jù)完整性問題捡偏,這在一開始可不那么明顯。
更糟的是峡迷,如果部署了 bugfix 的代碼而沒有修復相應 bug 導致的細微數(shù)據(jù)問題银伟,也會讓更多的數(shù)據(jù)問題累積,最終“積重難返”绘搞。
你怎么做才能使自己免受這些問題的困擾彤避?你可以簡單地使用多層數(shù)據(jù)完整性驗證。不要依賴于單一的用戶接口看杭。在前端忠藤、后臺、網(wǎng)絡層楼雹、數(shù)據(jù)層都進行數(shù)據(jù)驗證模孩。如果做不到這樣尖阔,那至少要在數(shù)據(jù)庫層做約束。
要熟悉數(shù)據(jù)庫的約束榨咐,在你往數(shù)據(jù)庫里添加表介却、列的時候盡可能的用上這些約束:
- NOT NULL 非空約束表示空值 NULL 無法被保存到該列上。如果你的應用假定了這個列的值是存在的块茁,那就應該在數(shù)據(jù)庫里把這個列定義為非空齿坷。
- UNIQUE 唯一約束表示在整個數(shù)據(jù)表中,該列上的所有值都不能出現(xiàn)重復数焊。舉個例子永淌,這在用戶表中的用戶名字段和郵件字段是極好的使用場景。
- CHECK 約束是一個自定義的表達式佩耳,想要被數(shù)據(jù)庫接受的數(shù)據(jù)都必須使該表達式計算結(jié)果為 true遂蛀。舉個例子,如果有一個常規(guī)的百分比列干厚,它的值介于 0 到 100 之間李滴,就可以使用 CHECK 約束來確保這一點。
- PRIMARY KEY 主鍵約束表示這個列的值必須同時是非空和唯一的蛮瞄。你目前可能就已經(jīng)在用了所坯。數(shù)據(jù)庫的每一個表都應該有一個主鍵以唯一標識一條記錄。
- FOREIGN KEY 外鍵約束表示該列的值必須和另一個表的一個列(通常是主鍵)的值匹配挂捅。
新手關于數(shù)據(jù)完整性的另一個問題是缺乏事務的觀念芹助。如果改變同個數(shù)據(jù)源的多個操作彼此依賴,他們就需要在一個事務里運行籍凝,以便在其中一個操作失敗時能夠回滾周瞎。
20)重復造輪子
This is a tricky point. In programming, some wheels are simply worth reinventing. Programming is not a well-defined domain. So many things change so fast and new requirements are introduced faster than any team can handle.
這是很棘手的一點。在編程領域饵蒂,有些輪子確實值得重新造一遍声诸。編程不是一個明確定義的領域。如此多的事物退盯、變化如此之快彼乌、新需求的引進也如此之快,沒有一個團隊能夠完美處理這些渊迁。
比如說慰照,如果你需要一個輪子,根據(jù)一天的不同時間以不同的速度旋轉(zhuǎn)琉朽,也許我們就不是去考慮改造那些我們所知的毒租、所愛的輪子了,而要考慮重新造一個箱叁。但是墅垮,除非你確實需要一個非常規(guī)設計的輪子惕医,否則都不要重新造一個新的輪子,就用那個該死的輪子吧算色。
有時候在眾多可選的牌子里選擇一個所需的輪子是很具挑戰(zhàn)性的抬伺。在購買之前要先調(diào)研和使用!關于軟件“輪子”很酷的一點是灾梦,大部分的輪子都是免費的峡钓、開放的,你可以看到它們的內(nèi)部設計若河。你能夠輕而易舉地通過判斷它們的內(nèi)部設計的質(zhì)量來判斷軟件輪子的好壞能岩。如果可能,使用開源的輪子萧福。開源的軟件包可以很容易的調(diào)試和修復捧灰。也可以很容易的替換掉。此外统锤,你還可以足不出戶地支持它們。
不過炭庙,如果你需要一個輪子饲窿,千萬不要去買一整輛車,然后把你正在維護的那輛車放到新買的車頂部焕蹄。不要僅僅為了使用一兩個函數(shù)就引入一整個代碼庫逾雄,在 JavaScript 中的典型例子就是 lodash 代碼庫。如果你要隨即打亂一個數(shù)組腻脏,只要引入 shuffle
方法就好了鸦泳。不要引入整個的 loadash 代碼庫,很可怕永品。
21)對代碼復審的錯誤態(tài)度
程序員新手的一個標志就是他們經(jīng)常把代碼復審看作是批評做鹰。他們不喜歡代碼復審、不感激代碼復審鼎姐、甚至恐懼代碼復審钾麸。
這是錯誤的。如果你也這么覺得炕桨,你需要馬上就改變這種態(tài)度饭尝。把每一次代碼復審當作是學習的機會,歡迎他們献宫、感激他們钥平、從中學習,最重要的姊途,當你從你的代碼復審人員那里學習到東西的時候涉瘾,要感謝他們知态。
在編程道路上,你永遠都是個學習者睡汹。承認這一點肴甸。大多數(shù)的代碼復審都能夠讓你學到以前不知道的一些東西。把它們當作學習的資源囚巴。
有時候代碼審核人員也會犯錯原在,那就輪到你來教他們了。但是如果無法僅僅從你的代碼中就明顯看出問題彤叉,也許在那種情況下你的代碼就需要相應的修改了庶柿。如果你無論如何都需要給你的復審人員上一課的話,那就記得秽浇,對程序員來講浮庐,教會別人是最有利的活動之一。
22)沒有使用版本控制系統(tǒng)
新手往往低估了一個好的版本控制系統(tǒng)的威力柬焕,我這里所說的好的版本控制系統(tǒng)其實就是指 Git审残。
源碼控制并不僅僅是你把你的更改推送給別人,讓他們在此之上接著開發(fā)斑举,除此之外還有更重要的搅轿。源碼控制是和清晰的歷史相關的。源碼會被質(zhì)疑富玷,代碼的歷史進程能夠輔助回答這些困難的質(zhì)疑璧坟。這就是為什么我們?nèi)绱岁P注提交信息的原因。它們還是又一種交流實現(xiàn)的通道赎懦,使用提交信息能夠幫助將來的維護者弄清代碼為什么會發(fā)展到目前的狀態(tài)雀鹃。
應該經(jīng)常提交和盡早提交。為了一致性励两,在提交信息主題行(譯注:Git 提交信息的第一行)使用(譯注:英文)現(xiàn)在分詞黎茎。提交信息要盡可能詳細但時刻注意也要盡可能是總結(jié)性的。如果你需要多幾行來寫提交信息伐蒋,那很可能就是你的提交包含太多了工三,Rebase!
不要在提交信息中包含任何無關的東西先鱼。比如說俭正,不要列出你添加、修改焙畔、刪除了哪些文件掸读,這些在提交對象的本身就包含了,并且可以輕易地使用一些 Git 命令參數(shù)就顯示出來,在提交信息中包含這些顯然就是混淆視聽儿惫。有些團隊喜歡為每一個更改的文件寫一個總結(jié)信息澡罚,在我看來那是“提交信息太多”的標志。
源碼控制也是和可發(fā)現(xiàn)性相關的肾请。如果你看到一個函數(shù)留搔,并且開始質(zhì)疑它的必要性和設計,你可以找到引入這個函數(shù)的提交記錄铛铁,看到這個函數(shù)的上下文隔显。提交記錄甚至可以幫助你認清哪些代碼帶來了 bug。Git 甚至提供了在各個提交之間進行二分查找饵逐,幫助定位到引入 bug 的提交的工具(bisect
命令)括眠。
源碼控制也可以被利用在一些神奇的地方,甚至在更改還沒進入官方提交記錄的時候倍权。諸如暫存區(qū)變更掷豺、選擇性打補丁、重置薄声、暫存当船、修改、應用默辨、比較生年、撤回等等工具為你的工作流提供了豐富的工具。了解它們廓奕、學習它們、使用它們档叔、感激它們桌粉。
在我的字典里,Git 功能你懂得越少衙四,你就越像一個新手铃肯。
23)過度使用共享狀態(tài)**
再次聲明,這不是關于函數(shù)式編程和其他范式的討論传蹈,那是另一篇文章的主題了押逼。
事實就是,共享狀態(tài)是出現(xiàn)問題的來源之一惦界,如果可能的話挑格,應該避免使用共享狀態(tài),如果避免不了沾歪,就應該最低限度地使用共享狀態(tài)漂彤。
在我還是初級程序員的時候,我經(jīng)常沒有意識到,我定義的每一個變量都代表了一種共享狀態(tài)挫望,它所持有的數(shù)據(jù)能夠被同一作用域內(nèi)的所有元素修改立润。一個共享狀態(tài)的作用域越大,它的跨度就越長媳板。盡量讓新的狀態(tài)包含在小的作用域內(nèi)桑腮,并且保證它們不會逸出。
關于共享狀態(tài)的大問題是蛉幸,在同一個 event loop(對于基于 event-loop 的環(huán)境) 中破讨,當有多個資源需要同時修改這個狀態(tài)的時候,就會出錯巨缘。就會發(fā)生競態(tài)條件添忘。
重點來了:一個新手可能會嘗試使用定時器來解決這個共享變量的競態(tài)條件問題,特別是當他們必須處理一個數(shù)據(jù)鎖的問題時若锁。這是危險的標志搁骑,別這么做,注意它又固,在代碼復審中指出它仲器,永遠也不要接受這樣的代碼。
24)面對 Erros 時的錯誤態(tài)度
Error 是好東西仰冠。這意味著你在進步乏冀,意味著你可以通過簡單的后續(xù)修改就獲得更多的進步。
專業(yè)程序員喜愛 Error洋只。新手則痛恨 Error辆沦。
如果看到這些驚艷的紅色錯誤信息讓你很困擾,你就需要改變這種態(tài)度了识虚。你需要把它們看作助手肢扯。你需要處理它們,需要利用它們來進步担锤。
有一些 Error 需要被改進為異常(Exception)蔚晨。異常是用戶定義的錯誤,這些錯誤是你在計劃之內(nèi)的肛循。有些 Error 則不需要管铭腕,它們就應該使程序奔潰并退出。
25)沒有休息
你是一個人多糠,你的頭腦就需要休息累舷。你的身體也需要休息。你經(jīng)常很在狀態(tài)以致于忘了休息夹孔。我把這視作新手的另一個標志笋粟。這不是你可以妥協(xié)的事情怀挠。在你的工作流中集成一些東西來迫使你中途休息。中途短暫休息很多次害捕,離開你的椅子绿淋,走一小段路,以此來想清楚接下來應該做什么尝盼,稍后雙眼清晰地回來繼續(xù)寫代碼吞滞。
這真是篇長文章。你應該休息一下了盾沫。
感謝閱讀
掘金翻譯計劃 是一個翻譯優(yōu)質(zhì)互聯(lián)網(wǎng)技術文章的社區(qū)裁赠,文章來源為 掘金 上的英文分享文章。內(nèi)容覆蓋 Android赴精、iOS佩捞、前端、后端蕾哟、區(qū)塊鏈一忱、產(chǎn)品、設計谭确、人工智能等領域帘营,想要查看更多優(yōu)質(zhì)譯文請持續(xù)關注 掘金翻譯計劃、官方微博逐哈、知乎專欄芬迄。