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