編程新手入門踩過的25個(gè)“坑”,你犯過其中哪些錯(cuò)誤?
大數(shù)據(jù)文摘作品
編譯:傅一洋、汪小七、張南星、GAO Ning、夏雅薇
高級(jí)的編程是邏輯思維的流露,會(huì)編程只代表你懂了這門語言的語法,但是會(huì)寫清晰簡潔易懂可迭代的代碼才是程序員該追求的境界。編程入門已經(jīng)不容易,但是如果能夠在早期樹立一些正確的“代碼觀”,或許可以讓你的編程之路升級(jí)得更快。作者苦口婆心地給出了25條建議,句句真言。
首先我要聲明的是:如果你是編程新手,本文并不是要讓你對(duì)自己犯的錯(cuò)誤感到愧疚,而是要你對(duì)這些錯(cuò)誤有更好的認(rèn)知,并避免在未來再犯。
當(dāng)然,這些錯(cuò)誤我也經(jīng)歷過,但是從每個(gè)錯(cuò)誤中都學(xué)到了一些新東西。現(xiàn)在,我已經(jīng)養(yǎng)成了一些好的編程習(xí)慣,我相信你也可以!
下面是這些常見的錯(cuò)誤,順序不分先后。
1. 寫代碼前缺少規(guī)劃
一般來說,創(chuàng)作一篇高質(zhì)量的文章不易,因?yàn)樗枰磸?fù)推敲研究,而高質(zhì)量的代碼也不例外。
編寫高質(zhì)量代碼是這樣一個(gè)流程:思考、調(diào)研、規(guī)劃、編寫、驗(yàn)證、修改。(貌似沒辦法編成一個(gè)好記的順口溜)
按照這樣的思路走,你會(huì)逐漸形成良好的編程習(xí)慣。
新手最大的錯(cuò)誤之一就是太急于寫代碼,而缺乏足夠的規(guī)劃和研究。雖然對(duì)于編寫小程序而言是沒多大問題的,但對(duì)于大項(xiàng)目的開發(fā),這樣做是很不利的。
為了防止代碼寫完之后發(fā)現(xiàn)重大問題,寫之前的深思熟慮是必不可少的。代碼只是你想法的流露。
生氣的時(shí)候,在開口說話前先數(shù)到十。如果非常生氣,就數(shù)到一百。
——托馬斯·杰斐遜 |
我把它改成針對(duì)寫代碼的版本:
審查代碼時(shí),重構(gòu)每一行之前,先數(shù)到十。如果代碼還沒有測(cè)試,就數(shù)到一百。
——Samer Buna |
編程的過程主要是研讀之前的代碼,思考還需要修改什么,如何適應(yīng)當(dāng)前系統(tǒng),并規(guī)劃盡量小的改動(dòng)量。而實(shí)際編寫代碼的過程只占整個(gè)過程時(shí)間花費(fèi)的10%。
不要總認(rèn)為編程就是寫代碼。編程是基于邏輯的創(chuàng)造,慢工出細(xì)活。
2. 寫代碼之前規(guī)劃過度
雖說寫代碼前充分規(guī)劃是好,但凡事都有個(gè)度,還沒開始做,就思考太多,也是不可取的。
不要期望世界上存在完美的規(guī)劃,至少編程的世界中是不存在。好的規(guī)劃可以作為起點(diǎn),但實(shí)際情況是,規(guī)劃是會(huì)隨后續(xù)進(jìn)行而改變的,規(guī)劃的好處只是能讓程序結(jié)構(gòu)條理更清晰,而規(guī)劃太多只會(huì)浪費(fèi)時(shí)間。
瀑布式開發(fā)是一種系統(tǒng)線性規(guī)劃的開發(fā)方法,它嚴(yán)格遵循預(yù)先計(jì)劃的需求、分析、設(shè)計(jì)、編碼、測(cè)試的步驟順序進(jìn)行,步驟成果作為進(jìn)度的衡量標(biāo)準(zhǔn)。在這種方法中,規(guī)劃是重中之重。如果只是編寫小程序,也完全可以采用這種方法,但要對(duì)于大的項(xiàng)目,這種方法完全不可取。任何復(fù)雜的事情都需要根據(jù)實(shí)際情況隨機(jī)應(yīng)變。
編程是一個(gè)隨時(shí)需要根據(jù)實(shí)際情況作出改變的工作。你后續(xù)可能會(huì)因?yàn)橐恍┰蛞砑踊騽h除的某些功能,但這些情況瀑布計(jì)劃中可能你永遠(yuǎn)也想不到。所以,你需要敏捷的開發(fā)模式。
但是,每一步之前是要有所規(guī)劃的,只不過規(guī)劃的過少或過多都會(huì)影響代碼的質(zhì)量,代碼的質(zhì)量非常重要。
3. 低估代碼質(zhì)量的重要性
如果你無法兼顧代碼的多項(xiàng)質(zhì)量指標(biāo),至少要保證它的可讀性。凌亂的代碼就相當(dāng)于廢品,而且不可回收。
永遠(yuǎn)不要低估代碼質(zhì)量的重要性。你要將代碼看作溝通的一種方式,作為程序員,你的任務(wù)是交代清楚目前任務(wù)是如何實(shí)施的。
我最喜歡一句編程俚語是:
寫代碼的時(shí)候可以這樣想,維護(hù)你代碼的家伙是一個(gè)知道你住在哪里的暴力精神病患者。
——John Woods |
很形象是不是?
即便是一些細(xì)節(jié)。例如,你的代碼可能會(huì)因?yàn)榕虐鎲栴}或大小寫不一致而不被認(rèn)可。
- tHIS is
- WAY MORE important
- than
- you think
還需要注意的是避免語句過長。任何超過80個(gè)字符的文本都是難以閱讀的。你可能想在同一行放置長條件以便看到完整的if語句,這是不可取的,一行永遠(yuǎn)不要超過80個(gè)字符。
這種小問題可以通過linting工具或格式化工具輕松解決。比如在JavaScript中兩個(gè)完美結(jié)合的優(yōu)秀工具:ESLint和Prettier。多用它們,讓工作更輕松。
還有一些與代碼質(zhì)量相關(guān)的錯(cuò)誤:
- 任何超過10行的函數(shù)都太長了。
- 一定不要出現(xiàn)雙重否定句。
- 使用簡短的,通用的或基于類型的變量命名。盡量保證變量命名能清晰地表述變量。計(jì)算機(jī)科學(xué)領(lǐng)域只有兩件難事:緩存失效和變量命名。
- 缺乏描述地插入一些字符串和數(shù)字。如果要使用固定的字符串或數(shù)值,應(yīng)該將其定義為常量,并命名。
- “對(duì)于簡單的問題,擔(dān)心花費(fèi)時(shí)間而草率地處理”。不要在眾多問題中進(jìn)行跳躍式選擇,按部就班地來。
- 認(rèn)為代碼越長越好。其實(shí),大多數(shù)情況下,代碼越短越好。只有在追求可讀性的情況下可適當(dāng)詳細(xì)些。比如,不要為了縮短代碼而使用很長的單行表達(dá)式或嵌套表達(dá)式,但也不要增加冗余的代碼。最好的是,刪去所有不必要的代碼。
- 過多使用條件語句。大部分你認(rèn)為需要條件語句的情況都可以不通過
它來解決。因此,考慮盡可能多的備選方案,根據(jù)可讀性進(jìn)行挑選。除非你知道如何測(cè)試代碼性能,否則,不要試圖優(yōu)化。還有就是:避免Yoda條件或條件嵌套。
4. 選擇1號(hào)方案
當(dāng)我剛開始編程時(shí),一旦遇到問題,我會(huì)立刻尋找解決方案并重新運(yùn)行我的程序。而不是先考慮我的頭號(hào)方案復(fù)雜性和潛在的失敗原因。
雖然1號(hào)方案極具誘惑性,但在研究了所有解決方案后,通常能發(fā)現(xiàn)更好的。如果無法想出多種方案,說明你對(duì)問題了解不夠。
作為專業(yè)程序員,你的工作不是找到辦法,而是找到最簡捷的辦法。“簡捷”的意思是方案必須正確,可執(zhí)行,且足夠簡單,易讀,又便于理解和維護(hù)。
軟件設(shè)計(jì)有兩種方法。一種是設(shè)計(jì)的足夠簡單,沒有瑕疵,另一種是設(shè)計(jì)的足夠復(fù)雜,沒人看得出明顯瑕疵。
——C.A.R.霍爾 |
5. 吊死在一棵樹上
這是我常犯的錯(cuò)誤,即便確定了我的頭號(hào)方案并不是最簡單的解決方案,仍然不放手。這可能與我的性格有關(guān)。大多數(shù)情況下這是一種很好的心態(tài),但不適用于編程。事實(shí)上,正確的編程心態(tài)是,將早期失敗和經(jīng)常性失敗看成一種常態(tài)。
當(dāng)你開始懷疑某個(gè)方案的時(shí)候,你應(yīng)該考慮放下它并重新思考,不管你之前在它這里投入了多少精力。學(xué)會(huì)利用像GIT這樣的源代碼管理工具,它可以幫助你實(shí)現(xiàn)代碼分支,嘗試多種方案。
不要認(rèn)為你付出了精力的代碼就是必須采用的。錯(cuò)誤的代碼要摒棄。
6. 閉門造車
很多次,在解決問題需要查閱資料時(shí),我卻直接嘗試解決問題,浪費(fèi)了很多時(shí)間。
除非你正在使用的是某種尖端技術(shù),否則,遇到問題時(shí),谷歌一下吧,因?yàn)橐欢〞?huì)有人也遇到了同樣的問題,并找到了解決方法,這樣,能節(jié)省很多時(shí)間。
有時(shí)候谷歌之后,你會(huì)發(fā)現(xiàn)你所認(rèn)為的問題并不是問題,你需要做的不是修復(fù)而是接受。不要認(rèn)為你了解一切,Google會(huì)讓你大吃一驚的。
不過,要謹(jǐn)慎地使用谷歌。新手會(huì)犯的另一個(gè)錯(cuò)誤是,在不理解代碼的情況下,原樣照搬。盡管這可能成功解決了你的問題,但還是不要使用自己不完全了解的代碼。
如果想成為一名創(chuàng)造性的程序員,就永遠(yuǎn)不要認(rèn)為,自己對(duì)在做的事情了如指掌。
作為一個(gè)有創(chuàng)造力的人,最危險(xiǎn)的想法是認(rèn)為自己知道自己在做什么。
——布雷特·維克多 |
7. 不使用封裝
這一點(diǎn)不只是針對(duì)使用面向?qū)ο笳Z言的例子,封裝總是有用的,如果不使用封裝,會(huì)給系統(tǒng)的維護(hù)帶來很大的困難。
在應(yīng)用程序中,每個(gè)功能要與用來處理它的對(duì)象一一對(duì)應(yīng)。在構(gòu)建對(duì)象時(shí),除了保留被其他對(duì)象調(diào)用時(shí)必須傳遞的參數(shù),其他內(nèi)容都應(yīng)該封裝起來。
這不是出于保密,而是為減少應(yīng)用程序不同部分之間的依賴。堅(jiān)持這個(gè)原則,可以使你在對(duì)類,對(duì)象和函數(shù)的內(nèi)部進(jìn)行更改時(shí),更加的安全,無需擔(dān)心大規(guī)模的毀壞代碼。
對(duì)每一個(gè)邏輯概念單元或者塊都應(yīng)該構(gòu)建對(duì)應(yīng)的類。通過類能夠勾畫出程序的藍(lán)圖。這里的類可以是一個(gè)實(shí)際對(duì)象或一個(gè)方法對(duì)象,你也可以將它稱作模塊或包。
在每個(gè)類中,其包含的每套任務(wù)要有對(duì)應(yīng)的方法,方法只針對(duì)這一任務(wù)的執(zhí)行,且能成功的完成。相似的類可共同使用一種方法。
作為新手,我無法本能地為每一個(gè)概念單元?jiǎng)?chuàng)建一個(gè)新類,而且經(jīng)常無法確定哪些單元是獨(dú)立的。因此,如果你看到一套代碼中到處充斥著“Util”類,這套代碼一定是新手編寫的?;蛘?,你做了個(gè)簡單的修改,發(fā)現(xiàn)很多地方也要進(jìn)行相應(yīng)地修改,那么,這也是新手寫的。
在類中添加方法或在方法中添加更多功能前,兼顧自己的直覺,花時(shí)間仔細(xì)思考。不要認(rèn)為過后有機(jī)會(huì)重構(gòu)而馬虎跳過,要在第一次就做對(duì)。
總而言之,希望你的代碼能具有高內(nèi)聚性和低耦合性,這是一個(gè)特定術(shù)語。意思就是將相關(guān)的代碼放在一起(在一個(gè)類中),減少不同類之間的依賴。
8. 試圖規(guī)劃未知
在目前項(xiàng)目還正在編寫的時(shí)候,總是去想其他的解決方案,這是忌諱的。所有的謎團(tuán)都會(huì)隨著代碼的一行行編寫而逐一解開。如果,對(duì)于測(cè)試邊緣案例進(jìn)行假設(shè),是件好事,但如果總想要滿足潛在需求,是不可取的。
你要明確你的假設(shè)屬于哪一類,避免編寫目前并不需要的代碼,也不要空想什么計(jì)劃。
僅憑空想,就認(rèn)為未來會(huì)需要某種功能,因而嘗試編寫代碼,是不可取的。
根據(jù)目前的項(xiàng)目,始終尋求最少的代碼量。當(dāng)然,邊緣情況是要考慮的,但不要過早落實(shí)到代碼中。
為了增長而增長是癌細(xì)胞的意識(shí)形態(tài)。
——Edward Abbey |
9. 錯(cuò)誤使用數(shù)據(jù)結(jié)構(gòu)
在準(zhǔn)備面試的時(shí)候,新手往往太過于關(guān)注算法。掌握好的算法并在需要時(shí)使用它們固然不錯(cuò),但記住,這與你的所謂“編程天賦資質(zhì)”無關(guān)。
然而,掌握你所用語言中各種數(shù)據(jù)結(jié)構(gòu)的優(yōu)缺點(diǎn),對(duì)你成為一名優(yōu)秀的開發(fā)者大有裨益。
一旦你的代碼中使用了錯(cuò)誤的數(shù)據(jù)結(jié)構(gòu),那明擺著,你就是個(gè)新手。
盡管本文并不是要教你數(shù)據(jù)結(jié)構(gòu),但我還是要提幾個(gè)錯(cuò)誤示例:
(1) 使用list(數(shù)組)來替代map(對(duì)象)
最常見的數(shù)據(jù)結(jié)構(gòu)錯(cuò)誤是,在管理記錄表時(shí),使用了list而非map。其實(shí),要管理記錄表,是應(yīng)該使用map的。
例如,在JavaScript中,最常見的列表結(jié)構(gòu)是數(shù)組,最常見的map結(jié)構(gòu)是對(duì)象(最新JavaScript版本中也包含圖結(jié)構(gòu))。
因此,用list來表示map結(jié)構(gòu)的數(shù)據(jù)是不可取的。雖然這種說法只是針對(duì)于大型數(shù)據(jù)集,但我認(rèn)為,任何情況下都應(yīng)如此,幾乎沒有什么情況,list能比map更好了,而且,這些極端情況在新版本的語言中也逐漸消失了。所以,只使用map就好。
這一點(diǎn)很重要。主要是由于訪問map中的元素會(huì)比訪問list中的元素快得多,訪問元素又是常有的過程。
在以前,list結(jié)構(gòu)是很重要的,因?yàn)樗鼙WC元素的順序,但現(xiàn)在,map結(jié)構(gòu)同樣能實(shí)現(xiàn)這個(gè)功能。
(2) 不使用棧
在編寫任何需要遞歸的代碼時(shí),總是去使用遞歸函數(shù)。但是,這樣的遞歸代碼難以優(yōu)化,特別在單線程環(huán)境下。
而且,優(yōu)化遞歸代碼還取決于遞歸函數(shù)返回的內(nèi)容。比如,優(yōu)化兩個(gè)或多個(gè)返回的遞歸函數(shù),就要比優(yōu)化單個(gè)返回值的遞歸函數(shù)困難得多。
新手常常忽略了使用棧來替代遞歸函數(shù)的做法。其實(shí),你可以運(yùn)用棧,將遞歸函數(shù)的調(diào)用變?yōu)閴簵_^程,而回溯變?yōu)閺棗_^程。
10. 把目前的代碼變得更糟
想象一下,給你這樣一間凌亂的房間:
然后,要求你在房間里再增加一個(gè)物件。既然已經(jīng)一團(tuán)糟了,你可能會(huì)想,把它放在任何地方都可以吧。因此,很快就能完成任務(wù)。
但是,在編寫代碼時(shí),這樣做只會(huì)讓代碼越來越糟糕!你要做的是,保證代碼隨著開發(fā)的進(jìn)行,變得越來越清晰。
所以,對(duì)于那間凌亂的房間,正確的做法是:做必要的清理,以便能將新增的物品放置在正確的位置。比如,你要在衣柜中添置一件衣服,那就需要先清理好地面,留出一條通向衣柜的路,這是必要的一步。
以下是一些錯(cuò)誤的做法,通常會(huì)使代碼變得更糟糕(只舉了一部分例子):
- 復(fù)制代碼。如果你貪圖省事而復(fù)制代碼,那么,只會(huì)讓代碼更加混亂。就好比,要在混亂的房間中,添加一把新椅子,而不是調(diào)整現(xiàn)有椅子的高度。因此,頭腦中始終要有抽象的概念,并盡可能地去使用它。
- 不使用配置文件。如果你的某個(gè)值在不同時(shí)間、不同環(huán)境下是不一樣的,則該值應(yīng)寫入配置文件中?;蛘?,你需要在代碼中的多個(gè)位置使用某值,也應(yīng)將它寫入配置文件。這樣的話,當(dāng)你引入一個(gè)新的值時(shí),只需要問自己:該值是否已經(jīng)存在于配置文件?答案很可能是肯定的。
- 使用不必要的條件語句或臨時(shí)變量。每個(gè)if語句都包含邏輯上的分支,需要進(jìn)行雙重測(cè)試。因此,在不影響可讀性的情況下,盡量避免使用條件語句。與之相關(guān)的一個(gè)錯(cuò)誤就是,使用分支邏輯來擴(kuò)展函數(shù),而不去引入新函數(shù)。每當(dāng)你認(rèn)為你需要一個(gè)if語句或一個(gè)新的函數(shù)變量時(shí),先問問自己:是否在將代碼往正確的方向推進(jìn)?有沒有站在更高的層面去思考問題?
關(guān)于不必要的if語句的問題,參考一段代碼:
- function isOdd(number) {
- if (number % 2 === 1) {
- return true;
- } else {
- return false;
- }
- }
上面的isOdd函數(shù)是存在一些問題的,你能看出最明顯問題嗎?
那就是,它使用了一個(gè)不必要的if語句。以下為其等效的代碼:
- function isOdd(number) {
- return (number % 2 === 1);
- };
11. 注釋泛濫
我已經(jīng)學(xué)會(huì)了,盡量不去寫注釋。因?yàn)榇蠖鄶?shù)的注釋可以通過對(duì)變量更好的命名來代替。
例如以下代碼:
- // 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);
- };
其實(shí),也可以寫成這樣沒有注釋的,效果相同:
- const sumOddValues = (array) => {
- return array.reduce((accumulator, currentNumber) => {
- if (isOdd(currentNumber)) {
- return accumulator + currentNumber;
- }
- return accumulator;
- }, 0);
- };
所以,每次寫注釋前,先思考一下:能否通過改善參數(shù)的命名來避免寫注釋呢?
但有一些情況下,是必須寫注釋的。比如,當(dāng)你用需要注釋來表述代碼的目的,而不是代碼在做什么時(shí)。
如果你實(shí)在想寫注釋的話,那就不要描述那些過于明顯的問題。以下是一些無用注釋的例子,它們只會(huì)干擾代碼的閱讀:
- // create a variable and initialize it to 0
- let sum = 0;
- // Loop over array
- array.forEach(
- // For each number in the array
- (number) => {
- // Add the current number to the sum variable
- sum += number;
- }
- );
所以,不要成為這樣的程序員,也不要接受這樣的代碼。如果必須處理這些注釋的話,那就刪掉好了。要是碰巧你雇傭的程序員總是寫出這樣的代碼的話,快點(diǎn)解雇他們。
12. 不寫測(cè)試
我認(rèn)同這一點(diǎn):如果你自認(rèn)為是專家,且有信心在不測(cè)試的情況下編寫代碼,那么在我看來,你就是個(gè)新手。
如果不編寫測(cè)試代碼,而用手動(dòng)方式測(cè)試程序,比如你正在構(gòu)建一個(gè)Web應(yīng)用,在每寫幾行代碼后就刷新并與應(yīng)用程序交互的話,我也這樣做過,這沒什么問題。
但是,手動(dòng)測(cè)試代碼,是為了更明確如何在之后進(jìn)行自動(dòng)測(cè)試。如果成功測(cè)試了與應(yīng)用的交互,那就應(yīng)該返回到代碼編輯頁,編寫自動(dòng)測(cè)試代碼,以便下次向項(xiàng)目添加更多代碼時(shí),自動(dòng)執(zhí)行完全相同的測(cè)試。
畢竟,作為人類,每次更改代碼后,難免會(huì)有忘記去重新測(cè)試曾經(jīng)成功過的代碼,所以,還是把它交給計(jì)算機(jī)完成吧!
如果可以,就在編寫代碼之前,先猜測(cè)或設(shè)計(jì)測(cè)試的過程。測(cè)試驅(qū)動(dòng)開發(fā)(TDD)這種方法不僅僅是流行,它還能使你對(duì)功能的看法發(fā)生積極的變化,以及為它們提供更好的設(shè)計(jì)方案。
TDD并不適合每個(gè)人,每個(gè)項(xiàng)目,但是,至少要會(huì)用它。
13. 認(rèn)為不出錯(cuò)就是正確的
看看這個(gè)實(shí)現(xiàn)了sumOddValues功能的函數(shù),有什么問題嗎?
- 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
- );
測(cè)試通過,一切順利,但情況真是如此?
上述代碼問題在于,沒有考慮到所有情況。盡管,它能正確地處理一部分的情況(測(cè)試時(shí)恰好命中這些情況之一)。來看看其中的幾個(gè)問題:
問題#1:沒有考慮輸入為空的情況。在沒有傳遞任何參數(shù)的情況下調(diào)用函數(shù),會(huì)發(fā)生什么?會(huì)出現(xiàn)如下所示的錯(cuò)誤:
- TypeError: Cannot read property 'reduce' of undefined.
這通常是個(gè)壞兆頭,原因主要有二:
- 用戶無法看到函數(shù)內(nèi)部,不知其如何實(shí)現(xiàn)的。
- 異常提示對(duì)用戶沒有任何幫助,但你的函數(shù)又無法滿足用戶需求。倘若異常提示表述的更明確些,用戶就能知道自己是如何錯(cuò)誤地調(diào)用了函數(shù)。比如,可以在函數(shù)中,設(shè)計(jì)拋出一個(gè)異常,提示用戶定義出錯(cuò)了,如下所示:
- TypeError: Cannot execute function for empty list.
也可以不拋出異常,忽略空輸入并返回0的總和。但是,無論如何,必須對(duì)這些情況有所處理。
問題#2:沒有處理無效輸入的情況。如果傳入的參數(shù)是字符串,整數(shù)或?qū)ο蠖皇菙?shù)組,會(huì)發(fā)生什么情況?
出現(xiàn)了下面的情況:
- sumOddValues(42);
- TypeError: array.reduce is not a function
那么,很不幸,因?yàn)閍rray.reduce確實(shí)是定義過的!
我們命名了函數(shù)的參數(shù)數(shù)組,因此,在函數(shù)中,將所有調(diào)用該函數(shù)的對(duì)象(42)標(biāo)記為數(shù)組。所以,就會(huì)拋出異常:42.reduce不是一個(gè)函數(shù)。
這個(gè)錯(cuò)誤很令人困惑不是?也許,更值得注意的錯(cuò)誤是:
- TypeError: 42 is not an array, dude.
問題#1和#2被稱為邊緣情況,他們都是常見的邊緣案例。但通常,有一些不太明顯的邊緣案例也是需要考慮的。例如,我們傳入負(fù)數(shù),會(huì)發(fā)生什么?
- sumOddValues([1, 2, 3, 4, 5, -13]) // => still 9
-13是奇數(shù),但結(jié)果是你想要的嗎?或許它應(yīng)該拋出異常?求和過程是否應(yīng)該包括參數(shù)中的負(fù)數(shù)?還是應(yīng)該忽略?也許你意識(shí)到,該函數(shù)應(yīng)命名為sumPositiveOddNumbers。
這種情況處理起來很容易,但是,更重要的一點(diǎn),如果不寫一個(gè)測(cè)試文檔來記錄測(cè)試案例的話,后續(xù)的維護(hù)者也將對(duì)此毫無線索,甚至認(rèn)為忽視負(fù)數(shù)是故意的或是出現(xiàn)了疏忽。
問題#3:測(cè)試沒有涵蓋所有的一般情況。除了邊緣情況,函數(shù)也有可能無法正確處理某個(gè)合理、有效的情況:
- sumOddValues([2, 1, 3, 4, 5]) // => 11
上例中,不應(yīng)將2計(jì)入總和。
原因很簡單:reduce函數(shù)是將第二個(gè)參數(shù)作為累加器的初始值的,如果該參數(shù)為空(如代碼所示),reduce將使用數(shù)組中第一個(gè)值作為累加器的初始值。這就是為什么在上面測(cè)試用例中,第一個(gè)偶數(shù)值也包含在了總和中。
即便你在編寫的過程中就發(fā)現(xiàn)了這個(gè)問題(并解決了),也是要編寫相應(yīng)的測(cè)試案例并記錄的,測(cè)試記錄還應(yīng)包含其他測(cè)試用例,如全偶數(shù)的情況,列表中存在0的情況,列表為空的情況。
如果測(cè)試記錄很少,又忽略了很多情況,忽視了邊緣情況,那么,這一定是新手干的。
14. 對(duì)已經(jīng)存在的代碼不再質(zhì)疑
除非你是超級(jí)程序員,可以獨(dú)當(dāng)一面。否則,毫無疑問你會(huì)碰到許多愚蠢的代碼。新手往往意識(shí)不到這些,他們會(huì)認(rèn)為,既然作為代碼庫一部分,又用了很長時(shí)間的代碼,一定是沒有問題的。
更糟的是,如果這些代碼中存在不妥,新手可能就會(huì)在其他地方重復(fù)這些不妥。因?yàn)樗麄冋J(rèn)為,代碼庫中的代碼是沒有問題的,從中學(xué)到的方法也是沒有問題的。
還有一些代碼,看起來很糟糕,但是,它可能包含著某種特殊的情況,從而迫使開發(fā)人員必須這么寫。這些地方,常常會(huì)有詳細(xì)的注釋,以將情況告知給新手,并說明,代碼為何要這么寫。
作為新手,你應(yīng)該假設(shè)任何不明白或不正規(guī)的代碼都是不好的。然后,去提問,去質(zhì)疑,去查他的git blame記錄!
如果代碼的作者無處可尋,那就仔細(xì)研究代碼本身,理解其中的所有。只有當(dāng)你完全理解后,才能形成自己的觀點(diǎn)(不論好與壞)。在此之前,不要草率地對(duì)代碼下結(jié)論。
15. 沉迷于最佳實(shí)踐
我認(rèn)為“最佳實(shí)踐”這個(gè)詞著實(shí)不好,它意味著無需再深入研究,這已經(jīng)是最好的結(jié)果了,毋庸置疑!
但編程中沒有最好只有更好,只能說對(duì)某種程序而言,目前這已經(jīng)是比較好的方案了。
甚至某些我們以前認(rèn)為的最佳實(shí)踐,現(xiàn)在已經(jīng)不是最好的解決方案了。
只要你肯花時(shí)間去研究,總能發(fā)現(xiàn)更好的方案,所以不要再執(zhí)著于最佳實(shí)踐,盡你努力做到最好即可。
不要因?yàn)槟阍谀硞€(gè)地方讀到的一句名言,或是你看到別人這么做了,或是聽人說這是最佳實(shí)踐就去做某件事。
16. 沉迷于性能優(yōu)化
在編程中過早優(yōu)化是萬惡之源(至少大部分是)。
——Donald Knuth (1974年) |
自從Donald Knuth發(fā)表了以上觀點(diǎn)之后,編程就發(fā)生了很大的變化,至今為止,我認(rèn)為這個(gè)觀點(diǎn)都是有價(jià)值的。
記住一條好的規(guī)則:如果你不能有效地量化代碼中的問題,那就別試圖去優(yōu)化它。
如果在執(zhí)行代碼前已經(jīng)在優(yōu)化了,那么你很可能過早的進(jìn)行了優(yōu)化,這是完全沒必要的,只是在浪費(fèi)時(shí)間。
當(dāng)然在你寫新代碼之前一些明顯需要優(yōu)化的內(nèi)容還是要考慮優(yōu)化的。例如,在Node.js中,你要確保你的代碼中沒有泛濫的使用循環(huán)或阻止調(diào)用堆棧,這些是非常重要的,這是你必須牢記要提前優(yōu)化的一個(gè)例子。所以在編寫過程中,可以時(shí)常問問自己:我準(zhǔn)備寫的代碼會(huì)阻止調(diào)用堆棧嗎?
應(yīng)該避免對(duì)任何不能量化的代碼進(jìn)行任何不明顯的優(yōu)化,否則反而會(huì)不利??赡苣阏J(rèn)為你這樣做會(huì)帶來性能上的提升,但事實(shí)上這會(huì)成為新的不可預(yù)料的bug來源。
因此,不要浪費(fèi)時(shí)間去優(yōu)化那些不能量化的性能問題。
17. 不以最終的用戶體驗(yàn)為目標(biāo)
在應(yīng)用程序中添加特性最簡單的方法是什么?從你自己的角度看,或許是看它如何適應(yīng)當(dāng)前的用戶界面,對(duì)吧?如果這個(gè)功能是要捕獲用戶輸入的,那么把它加到已有的那些表單中。如果這個(gè)功能是要添加一個(gè)頁面鏈接,那就把它加到已有的嵌套鏈接菜單中。
不要自以為是。要站在終端用戶角度來開發(fā),這樣才是真正的專業(yè)人員。這樣的開發(fā)者才會(huì)去思考有這個(gè)功能訴求的用戶需要什么,用戶又會(huì)如何操作。而且他們會(huì)考慮如何能讓用戶更便捷地找到和使用這個(gè)功能,而不是只考慮如何在應(yīng)用程序中添加這個(gè)功能,而不考慮這個(gè)功能的可發(fā)現(xiàn)性和可用性。
18. 工作時(shí)沒有選對(duì)適合的工具
每個(gè)人在完成編程的相關(guān)活動(dòng)中,都有一套自己喜歡使用的工具。其中有一些很好用,也有一些不好用,但是大多數(shù)工具只是對(duì)某一項(xiàng)特定任務(wù)很棒,而對(duì)其他任務(wù)來說都沒有那么好。
如果要把釘子釘在墻上,錘子確實(shí)是把好工具,但如果要用錘子來旋螺絲釘,那就是很糟糕的工具了。不能只是因?yàn)槟?ldquo;喜歡”錘子,就用它來旋螺絲釘。也不能因?yàn)殄N子是亞馬遜中最受歡迎的工具,用戶評(píng)價(jià)得分5.0,就用它來旋螺絲釘。
根據(jù)受歡迎程度來選擇工具,而不是針對(duì)問題的適用性來選擇工具是新手的一個(gè)標(biāo)志。
對(duì)于新手而言,另一個(gè)問題是:你也許根本不知道對(duì)一項(xiàng)特定工作來說什么工具“更好”。在你當(dāng)前的認(rèn)知范圍內(nèi),也許某一種工具就是你所知道的最好的工具。但是,跟其他工具相比時(shí),它并不是首選。你需要熟悉所有可用的工具,并且對(duì)剛開始使用的新工具保持開放的心態(tài)。
一些程序員是拒絕使用新工具的,他們對(duì)于現(xiàn)有的工具很滿意,而且他們可能也不想去學(xué)習(xí)任何新的工具。我明白,我也能理解,但是這顯然是不對(duì)的。
工欲善其事,必先利其器。你可以用原始工具建造一個(gè)小屋,并享受你的甜蜜時(shí)光;你也可以投入時(shí)間和資金去獲得好工具,這樣你就可以更快地建造一座更好的房子。工具是不斷更新的,而你也需要習(xí)慣去不斷學(xué)習(xí)并使用它們。
19. 不理解代碼問題會(huì)造成數(shù)據(jù)問題
一個(gè)程序非常重要的一方面就是某種格式數(shù)據(jù)的管理,該程序?qū)⑹翘砑有掠涗?、刪除舊記錄和修改其他記錄的界面。
程序的代碼即使有一點(diǎn)點(diǎn)的小問題,都會(huì)給其管理的數(shù)據(jù)帶來不可預(yù)估的后果,尤其當(dāng)你所有的數(shù)據(jù)驗(yàn)證都是通過那個(gè)漏洞程序完成時(shí),則更是如此。
當(dāng)涉及到代碼和數(shù)據(jù)的關(guān)系時(shí),初學(xué)者可能不會(huì)立即將這些點(diǎn)聯(lián)系起來。他們可能覺得在生產(chǎn)中繼續(xù)使用一些錯(cuò)誤代碼也是可以的,因?yàn)樘卣鱔是不用運(yùn)行的,它沒那么重要。但問題是錯(cuò)誤代碼可能會(huì)不斷地導(dǎo)致數(shù)據(jù)完整性問題,雖然這些問題在一開始的時(shí)候并不明顯。
更糟糕的是,在修復(fù)漏洞時(shí),并沒有修復(fù)漏洞所導(dǎo)致的細(xì)微的數(shù)據(jù)問題,就這樣交付代碼只會(huì)積累更多的數(shù)據(jù)問題,且這樣的問題會(huì)被貼上“不可修復(fù)”的標(biāo)簽。
那么如何避免讓自己發(fā)生這些問題呢?你可以簡單地使用多層次的數(shù)據(jù)完整性驗(yàn)證,不只依賴于單個(gè)用戶界面,應(yīng)該在前端、后端、網(wǎng)絡(luò)通信和數(shù)據(jù)庫中都創(chuàng)建驗(yàn)證。如果你不想這么做,那么請(qǐng)至少使用數(shù)據(jù)庫級(jí)別的約束。
要熟練掌握數(shù)據(jù)庫約束,并學(xué)會(huì)在數(shù)據(jù)庫中添加新列或新表時(shí)使用它們:
- NOT NULL是對(duì)列的空值約束,表示該列不允許使用空值。如果你的應(yīng)用程序中設(shè)定某個(gè)字段必須有值,那么在數(shù)據(jù)庫中它的源數(shù)據(jù)就應(yīng)該定義為not null。
- UNIQUE是對(duì)列的單一約束,表示在整個(gè)表中該列不允許有重復(fù)值。比如,用戶信息表的用戶姓名或者電子郵件字段,就適合使用這個(gè)約束。
- CHECK約束是一個(gè)自定義表達(dá)式,對(duì)于滿足條件的數(shù)據(jù),計(jì)算結(jié)果為True。例如,如果有一列值必須是介于0到100之間的百分比,則可以使用CHECK約束來強(qiáng)制執(zhí)行。
- PRINARY KEY(主鍵)約束表示某一列的值必須不為空,且不重復(fù)。你可能一直在用這個(gè)約束,數(shù)據(jù)庫中的每個(gè)表都必須有一個(gè)主鍵來識(shí)別不同的記錄。
- FOREIGN KEY(外鍵)約束表示某一列的值必須與另一個(gè)表的某一列值相匹配,通常來說外鍵約束也會(huì)是主鍵約束。
對(duì)于新手來說,另一個(gè)與數(shù)據(jù)完整性相關(guān)的問題是缺乏對(duì)事務(wù)處理(transactions)的思考。如果多個(gè)操作需要更改同一個(gè)數(shù)據(jù)源,且它們相互依賴時(shí),則必須把它們包裝在一個(gè)事務(wù)當(dāng)中,這樣當(dāng)其中一個(gè)操作失敗時(shí)就可以進(jìn)行回滾。
20. 推倒重來
這是一件很麻煩的事情。編程過程中,有時(shí)的確是需要推倒重來。編程不是一個(gè)界限分明的領(lǐng)域,變化層出不窮,新需求提出的速度遠(yuǎn)超于任何團(tuán)隊(duì)可以應(yīng)對(duì)的能力范圍。
打個(gè)比方,基于當(dāng)前的速度,如果你需要不同種類的輪胎,除了改進(jìn)我們都熟悉且喜愛的輪胎以外,也許我們需要換一種角度思考。然而,除非真的需要特殊設(shè)計(jì)的輪胎,否則沒有必要推倒重來。就將就用用原來的輪胎吧。
不要浪費(fèi)寶貴的時(shí)間在尋找所謂的最好的輪胎之上??焖偎阉?,然后使用所尋找到的內(nèi)容。只有在這些輪胎真的沒法像宣傳的那樣好好工作時(shí),再進(jìn)行更換。
編程最酷的一件事就是大多數(shù)輪胎都是透明的,你可以看到它內(nèi)部的構(gòu)造,能夠非常容易地判斷代碼的質(zhì)量高低。
所以盡量使用開源代碼。開源包的缺陷更容易解決,更容易被替代,也更容易從內(nèi)部支持。然而,當(dāng)你需要一個(gè)輪子時(shí),不要買一個(gè)全新的車,然后把你現(xiàn)在的車放在那輛新車上。
也就是說,不要在代碼里加載一整個(gè)包,然后只使用里面的一兩個(gè)函數(shù)。最好的例子就是JavaScript中的lodash程序包。如果你只是想隨機(jī)排列一個(gè)數(shù)組,只需要加載shuffle方法就好,不要加載一整個(gè)令人絕望的lodash程序包。
21. 厭惡代碼審查
新程序員們的一個(gè)明顯特征就是把代碼審查當(dāng)做批評(píng),他們不喜歡、不珍惜,甚至是恐懼代碼審查。
大錯(cuò)特錯(cuò),如果你也有同樣的感受,那么你需要立刻改變你的態(tài)度。把每次代碼審查都看做是學(xué)習(xí)機(jī)會(huì),用開放的心態(tài)歡迎、珍惜它們,并且從中學(xué)習(xí)。更為重要的是,要向給你提供了指導(dǎo)的審查員們表示感謝。
你需要接受一個(gè)事實(shí)——每個(gè)人都是終生代碼學(xué)習(xí)者。大多數(shù)代碼審查都能教給一些你以前可能不知道的知識(shí),所以請(qǐng)將代碼審查當(dāng)做是一項(xiàng)學(xué)習(xí)資源吧。
有時(shí),審查員也會(huì)犯錯(cuò)誤,這時(shí)候就輪到你去教他們一些東西了。然而,如果審查出來的問題不僅僅是由于你的代碼導(dǎo)致的錯(cuò)誤,那么也許還是需要進(jìn)行代碼修改。如果無論如何你都需要教審查員一些東西的話,那么謹(jǐn)記:教授別人是你作為程序員最有收獲的一件事。
22. 不使用源代碼控制
新手們有時(shí)會(huì)低估一個(gè)好的源代碼/版本控制系統(tǒng),所謂好的系統(tǒng),我指的是Git。
源代碼控制并不僅僅是指把代碼修改推送給別人,然后進(jìn)行版本變更,這個(gè)行為的意義遠(yuǎn)不止如此。源代碼控制的主要目的在于清晰的歷史記錄。
代碼需要時(shí)常進(jìn)行回顧,而代碼的修改過程的記錄將會(huì)極大助力于一些疑難雜癥的解決,這也是為什么我們會(huì)很在意提交信息。代碼控制同樣也是一個(gè)溝通實(shí)施信息的渠道,使用這些零碎的提交歷史,能夠幫助未來的代碼維護(hù)人員了解代碼的發(fā)展情況以及現(xiàn)在所處的狀態(tài)。
常常提交、盡早提交,并且出于對(duì)連貫性的尊重,請(qǐng)?jiān)谔峤粯?biāo)題中使用現(xiàn)在時(shí)態(tài)。信息最好盡量詳盡,但謹(jǐn)記它們應(yīng)該是經(jīng)過提煉總結(jié)的。如果你需要好幾行來闡述想表達(dá)的內(nèi)容,也許意味著你的提交信息太長了。重來吧!
不要在提交信息中不要放入任何不必要的信息。例如,不要列出被加載、被修改或者被刪除的文件。
這些列表本身已經(jīng)包含在提交的代碼中了,并且能夠通過一些Git命令參數(shù)實(shí)現(xiàn),它們只會(huì)成為總結(jié)信息中的噪音。一些團(tuán)隊(duì)喜歡在每個(gè)文件改變中都做一次總結(jié),我認(rèn)為這是另一種提交信息太冗長的標(biāo)志。
源代碼控制和可發(fā)現(xiàn)性也有關(guān)系。當(dāng)你遇到一個(gè)函數(shù),需要開始了解它的需求或者設(shè)計(jì),你可以尋找介紹它的提交信息,然后閱讀函數(shù)相關(guān)內(nèi)容。
提交信息甚至可以幫你找到程序中導(dǎo)致缺陷的代碼是哪些。Git在提交中提供了一個(gè)二進(jìn)制搜索(bisect命令)來精準(zhǔn)定位導(dǎo)致缺陷的罪惡源頭。
源代碼控制也可以在代碼變動(dòng)正式生效之前發(fā)揮極大的作用。諸如階段轉(zhuǎn)換、選擇性打補(bǔ)丁、重置、隱藏、修復(fù)、應(yīng)用、區(qū)分、撤銷以及其他許多對(duì)代碼編輯有用的工具。所以好好理解、學(xué)習(xí)、使用并且珍惜他們吧。
你知道的Git特性越少,那么你離文章中所說的新手就越接近。
23. 過度使用共享狀態(tài)
同樣的,這一點(diǎn)并不是在比較函數(shù)式編程與其他算法的優(yōu)劣區(qū)別,那是另外一篇文章要談?wù)摰脑掝}。
需要指出的是,共享狀態(tài)往往是問題的源頭,如果可能的話,盡量避免使用它。如果無法避免,那么需要把使用共享狀態(tài)控制在最低限度。
當(dāng)我還是編程初學(xué)者的時(shí)候,我沒有意識(shí)到我們所定義的每一個(gè)變量都是一個(gè)共享狀態(tài)。變量當(dāng)中包含了數(shù)據(jù),并且可以被該變量所處的域內(nèi)所有元素改變。域的范圍越大,那么這個(gè)共享狀態(tài)的范圍就越廣。盡量把新變量聲明維持在一個(gè)小范圍內(nèi),并確保它們不會(huì)向上滲透。
情況比較嚴(yán)重的問題就是當(dāng)共享狀態(tài)生效、多個(gè)源頭都會(huì)導(dǎo)致同一個(gè)事件循環(huán)標(biāo)記發(fā)生改變時(shí)(在事件循環(huán)環(huán)境中),會(huì)發(fā)生爭用條件。
事實(shí)是:新手有可能會(huì)采取計(jì)時(shí)器作為共享狀態(tài)爭用條件的曲線救國之道,特別是當(dāng)他們需要處理數(shù)據(jù)鎖定的問題時(shí)。
這是在立flag,別這樣做。切記,處處留心,并且在代碼審查時(shí)指出這個(gè)問題,絕對(duì)不要接受這種情況。
24. 不正確地面對(duì)錯(cuò)誤
錯(cuò)誤是一個(gè)好東西,它們的存在意味著進(jìn)步,意味著你更容易獲得成長。
編程大牛們對(duì)錯(cuò)誤愛不釋手,而新手則恨之入骨。
如果看著這些可愛的小小紅色錯(cuò)誤信息,會(huì)讓你覺得心煩,那么你需要改變一下態(tài)度,把它們視為助手。你需要好好對(duì)待它們,并充分發(fā)揮它們的作用,促進(jìn)自己的成長。
有些錯(cuò)誤需要升級(jí)至異常情況。異常情況是需要你給出解決方法的用戶自定義錯(cuò)誤。有些錯(cuò)誤需要單獨(dú)進(jìn)行處理,它們的存在將會(huì)讓程序崩潰,并且強(qiáng)制退出。
25. 從不休息
程序員也是人類,你的大腦、你的身體都需要休息。常常,當(dāng)你進(jìn)入編程狀態(tài)時(shí),就忘記了休息。我把這一點(diǎn)也視為新手的一個(gè)標(biāo)志。這不是你可以妥協(xié)的點(diǎn)。把一些能夠強(qiáng)制你休息的內(nèi)容整合到你的工作流中,然后短暫地休息一下。
離開椅子,在附近走走,同時(shí)想想下面需要做的事情。當(dāng)你回到代碼的世界時(shí),就可以用全新的視角看待你的成果。
這篇文章很長,現(xiàn)在你可以休息一下了。
原文鏈接:
https://medium.com/@samerbuna/the-mistakes-i-made-as-a-beginner-programmer-ac8b3e54c312
【本文是51CTO專欄機(jī)構(gòu)大數(shù)據(jù)文摘的原創(chuàng)譯文,微信公眾號(hào)“大數(shù)據(jù)文摘( id: BigDataDigest)”】