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