寫代碼就是學(xué)一門語言然后開始擼代碼嗎?看完了我的《GoF設(shè)計(jì)模式》系列文章的同學(xué)或者本身已經(jīng)就是老鳥的同學(xué)顯然不會這么認(rèn)為。編程是一項(xiàng)非常嚴(yán)謹(jǐn)?shù)墓ぷ鳎‰m然我們自嘲為碼農(nóng),但是這工作畢竟不是真正的搬磚,我們是軟件工程師。編程需要關(guān)注的問題太多,不僅僅有語言,還有算法、數(shù)據(jù)結(jié)構(gòu)、編程技巧、編碼風(fēng)格、設(shè)計(jì)、架構(gòu)、工程化、開發(fā)工具、團(tuán)隊(duì)協(xié)作等方方面面,涉及到很多層面的問題。本文將分享一下根據(jù)我這幾年來的編程經(jīng)驗(yàn)總結(jié)出的一些關(guān)于如何寫代碼的個(gè)人見解。
由于“跟我混”的一些小伙伴編程功底相對來說比較薄弱,所以在此總結(jié)一篇“編程內(nèi)功心法”幫助他們渡過職業(yè)生涯的第一個(gè)瓶頸期。順便,也造福一下路過的有緣的同學(xué)!于是有了此文。
首先,思考一個(gè)問題,何謂編程?編程就是寫代碼嗎?
所謂的編程,其實(shí)就是不斷的對這個(gè)現(xiàn)實(shí)世界中的問題建立模型并將其固化為代碼自動化執(zhí)行的過程。
~ Bug輝 《GoF設(shè)計(jì)模式 - 解釋器模式》
在對問題建立模型的過程中,我們會遇到非常多不同層面的問題,所以我們需要很多領(lǐng)域的知識去解決這些問題。
我們需要管理被操作的數(shù)據(jù),因?yàn)閿?shù)據(jù)與數(shù)據(jù)之前是相互有關(guān)聯(lián)的。將數(shù)據(jù)結(jié)構(gòu)化,通常是編程的第一步。關(guān)于結(jié)構(gòu)化數(shù)據(jù)的相關(guān)理論以及實(shí)踐,需要有一個(gè)專門的學(xué)科分支或者說課題去研究——數(shù)據(jù)結(jié)構(gòu)。
我們需要解決一個(gè)具體的問題,這個(gè)具體的問題如何一步步去解決,過程是怎么樣子的——算法。
我們需要將解決方案進(jìn)行自動化,并以代碼的形式進(jìn)行交付——編程語言。
如果將一個(gè)抽象的模型進(jìn)行編碼實(shí)現(xiàn),如何實(shí)現(xiàn)“這個(gè)功能”,如何實(shí)現(xiàn)“那個(gè)功能”——編程技巧。
問題的規(guī)模大了,眾多代碼糅合在一起,連程序員自己都看不懂了!怎么來拆分、模塊化這些代碼——設(shè)計(jì)。
代碼量已經(jīng)到了一個(gè)人無法完成的地步了,需要團(tuán)隊(duì)分工合作才能完成了——工程化。
你寫的代碼我看不懂,沒法調(diào)用或者很難調(diào)用,我寫的代碼你也看不懂,或者很難看懂。還怎么愉快的玩耍——編碼風(fēng)格/編碼規(guī)范。
問題的規(guī)模繼續(xù)擴(kuò)大,到了系統(tǒng)工程的規(guī)模了,之前學(xué)的套路已經(jīng)不管用了!怎么來構(gòu)建這個(gè)系統(tǒng)才能實(shí)現(xiàn)正確、安全、高性能、高可用——架構(gòu)。
然而這些也只是一個(gè)系統(tǒng)工程中的冰山一角!這是一個(gè)龐大的體系。也正是因?yàn)檐浖_發(fā)需要考慮到的問題太多且團(tuán)隊(duì)成員水品參差不齊,所以團(tuán)隊(duì)開發(fā)中并不是每個(gè)程序員做的事情都是一樣的。每個(gè)人都有自己的角色、初級工程師、中級工程師、高級工程師、架構(gòu)師、CTO。。。
所以編程不僅僅只是堆砌代碼!
說到這里,我想起來了一件事情——為啥業(yè)界普遍鄙視培訓(xùn)出來半道出家的新人?人與人的區(qū)別是很大的!我見過培訓(xùn)出來也很牛的。其實(shí),說到底,被鄙視的并不是所有人。而是那些培訓(xùn)了幾個(gè)月之后發(fā)現(xiàn)隨便找個(gè)工作也能拿“高薪”然后還自認(rèn)為編程很簡單的新人。因?yàn)檫@種經(jīng)歷給了他們一種錯(cuò)覺——編程如此簡單,我培訓(xùn)幾個(gè)月也會嘛!有點(diǎn)像剛學(xué)會開車的新司機(jī),很囂張的對老司機(jī)說“開車很簡單嘛!你看我也會啊!”。語言和開發(fā)工具只是招式,這是外功。而編程思想、經(jīng)驗(yàn)是內(nèi)功。這些內(nèi)功并不是靠短短幾個(gè)月的培訓(xùn)能夠掌握的,這一點(diǎn)有點(diǎn)像中國制造業(yè)和日本制造業(yè)的區(qū)別。動不動趕英超美可不好。。。
編程并不簡單!這是一件很嚴(yán)肅的事情。不過今天,我沒有辦法介紹完所有的方面!或者說,到今天為止,我也并沒能掌握所有領(lǐng)域的知識。所以今天我只是分享一些關(guān)于編碼本身的一些經(jīng)驗(yàn)。
另外,本文主要分享如何寫代碼,并不是如何用Java寫代碼。所以文章中各種語言都有可能出現(xiàn)。
先來一個(gè)圈內(nèi)的段子。
大部分程序員在工作中都很討厭這四件事情:
寫注釋
寫文檔
別人不寫注釋
別人不寫文檔
o(∩_∩)o 哈哈。。中槍了沒!
這個(gè)段子其實(shí)反映出來一個(gè)問題,即大部分代碼都需要通過大量注釋和文檔來說明才能將意圖傳達(dá)給維護(hù)這些代碼的程序員!然而,就像上面的段子說的那樣,寫大量的注釋和文檔其實(shí)是一件很麻煩的事情。所以很多時(shí)候,由于嫌麻煩,注釋和文檔就沒寫,導(dǎo)致維護(hù)代碼的人相當(dāng)?shù)耐纯唷_@個(gè)苦同學(xué)們肯定都是體會過的!相當(dāng)于給你個(gè)精密儀器要你維護(hù)還不給說明書。
其實(shí),打破上面那個(gè)段子描述的那個(gè)怪圈的一個(gè)很有效的手段就是統(tǒng)一編碼風(fēng)格。優(yōu)秀的代碼可以實(shí)現(xiàn)代碼即注釋,代碼本身就可以非常清晰的體現(xiàn)出它的意圖來,讓別人可以很容易讀懂。這就是所謂的可讀性!
計(jì)算機(jī)科學(xué)領(lǐng)域中最難的兩件事是命名和緩存失效!命名并不簡單,很復(fù)雜。好的名字可以見名知意,非常容易理解。之所以說命名難是因?yàn)槊倪^程同時(shí)也是概念提取的過程!對問題建立模型,需要提取概念并賦予其“術(shù)語”。這個(gè)過程其實(shí)是“萬里長征”中最難的一步。畢竟,設(shè)計(jì)也好,架構(gòu)也罷,都有成熟的套路可以參考。唯獨(dú)這個(gè)過程,是需要程序設(shè)計(jì)者自己進(jìn)行充分的思考的創(chuàng)造性工作!
以下是總結(jié)出來的一些命名經(jīng)驗(yàn):
一個(gè)類是某物、某事、某人的抽象,是數(shù)據(jù)與行為的集合體。這恰好符合名詞的定義,因此 類名 是一個(gè)名詞!
方法名 或者說 函數(shù)名 是某操作或者某過程的抽象,是一個(gè)動作。這恰好符合動詞的定義,因此函數(shù)名通常是一個(gè)動詞。
變量名寧可長一些說明清楚用途也不要用a、b、c之類的無意義的名稱,除非是循環(huán)計(jì)數(shù)器中用i、j、k等約定俗成的一些變量名。比如pageIndex和pageSize就要比取名成i和s好!取成這種和用混淆器混淆過后的代碼一樣的名稱沒有什么好處,如果算法比較復(fù)雜的話,過一段時(shí)間恐怕自己都會看不懂。
變量名最好包含變量本身的業(yè)務(wù)含義。比如ListstudentList = new ArrayList<>();就比Listlist = new ArrayList<>();好很多。如果同一段代碼里再出現(xiàn)一個(gè)List的話,這樣就可以很方便的取名為teacherList或者teachers而不是list1和list2這樣的毫無意義的名稱!
英文不好怎么辦
這個(gè)問題怎么說呢。。
作為一名程序員吧,基礎(chǔ)的英文還是要懂的。要不然發(fā)展也容易遇到天花板,學(xué)不好編程的。畢竟,最新的技術(shù)、解決方案、工具都是從國外傳過來的。如果是解決一些基礎(chǔ)性的問題,每天只做做CRUD,好像英文確實(shí)不怎么用得上。但是一旦遇到一些實(shí)質(zhì)性問題,恐怕只能到英文網(wǎng)站上找嘍!ㄟ(▔ ,▔)ㄏ 不要跟我說你編程可以不需要Stack Overflow。copying and pasting from stackoverflow 可是終極編程大法!o(∩_∩)o 這句話可是編程的真諦啊!(如果你看不懂這個(gè)梗那你有可能是偽程序員)
其實(shí),話說回來,實(shí)在不方便用英文的時(shí)候,我認(rèn)為也可以用拼音命名。這個(gè)問題上可以務(wù)實(shí)一點(diǎn),量力而行。但是,拼音和英語混用的做法就不太好了。最好別這樣!逼格不高。
怎么添加代碼注釋
關(guān)于注釋,我們需要解決的第一個(gè)問題是如何添加代碼注釋。
對于Java、C#之類的語言,有專用的文檔注釋語法,很好處理。對于C/C++,則按約定的格式說明一下類和函數(shù)、代碼片段的作用和意圖即可,至少編譯器會進(jìn)行靜態(tài)檢查。在Python中,有更牛逼的文檔字符串這樣的語言級特性支持,看注釋用help()很方便。不過對于Lua這樣的弱類型解釋型語言,注釋就比較難處理了。這里以Lua為例給出一種注釋的解決方案。
借用Java語言文檔注釋的風(fēng)格。
文件注釋,或者說類/模塊注釋。
函數(shù)注釋
tips: Lua中可以通過metatable機(jī)制實(shí)現(xiàn)類和繼承,這一點(diǎn)與Javascript通過原型機(jī)制來實(shí)現(xiàn)類和繼承有點(diǎn)類似。
注釋里該寫些什么
我們首先來看個(gè)反例。
首先這個(gè)方法名本身就取得不好,這個(gè)暫且不說,先說注釋問題。這里的注釋犯了幾個(gè)錯(cuò):
1. 方法注釋為“查詢”,這簡直就是廢話!方法名已經(jīng)告訴別人這是查詢方法了,還在這個(gè)注釋里寫這兩個(gè)字有什么意義呢?而且到底查詢些什么這里也沒說!
2. 參數(shù)沒有注釋。沒有描述每一個(gè)參數(shù)的意義以及取值范圍等!
3. 什么情況下會拋出PageIndexOutOfBoundsException沒有描述清楚。
4. “定義一個(gè)整型變量”這種垃圾注釋就不要寫了,這么簡單的語句誰看不懂啊!如果要注釋,也是寫上這個(gè)變量的含義。
這里我們先不考慮設(shè)計(jì)問題(分頁索引號最好做成可以自己調(diào)整成合理值),下面再來看改善注釋之后的代碼。
改完之后的注釋有沒有感覺信息更全很多!雖然說代碼本身就是最好的注釋,但是必要的注釋還是得寫上去,畢竟調(diào)用的時(shí)候別人沒法猜測你的索引號到底從0還是從1開始。另外,如果函數(shù)內(nèi)算法比較復(fù)雜,可以在代碼塊內(nèi)注釋,也可以在函數(shù)注釋上直接寫清楚這個(gè)函數(shù)內(nèi)部的大概算法/邏輯。代碼寫出來就是給別人調(diào)用的,如果沒有基本的注釋信息,那么每次調(diào)用你的代碼的時(shí)候,都得去看一下你的函數(shù)內(nèi)具體邏輯才能知道怎么調(diào)用。這顯然是非常低效的!
命名與注釋這兩個(gè)基本方面沒做好的話,會影響到整個(gè)團(tuán)隊(duì)的運(yùn)作。也就是說,你封裝的東西并沒有給隊(duì)友節(jié)省什么時(shí)間,別人用到你的代碼的時(shí)候,又需要花上一些時(shí)間去讀你的代碼。如果團(tuán)隊(duì)里每個(gè)人都這樣,那整個(gè)團(tuán)隊(duì)都會極其低效。我個(gè)人是非常不愿意與這種代碼風(fēng)格惡劣的人合作的。
關(guān)于編碼風(fēng)格的問題,本文只說命名和注釋這兩個(gè)方面。關(guān)于縮進(jìn)、空格、斷行、空行等其他方面的問題,可以參考本節(jié)給出的參考規(guī)范。
不同的企業(yè)會有不同的編碼規(guī)范,所以這里沒有辦法給出一個(gè)符合所有公司的規(guī)范。不過制定自己團(tuán)隊(duì)的規(guī)范的時(shí)候,可以參考一些大企業(yè)的做法。以下是世界上最大的互聯(lián)網(wǎng)公司谷歌的編碼規(guī)范,同學(xué)們可以參考這個(gè)。
Google Java Style Guide
Google C++ Style Guide
Google Python Style Guide
Google HTML/CSS Style Guide
Google JavaScript Style Guide
異常與返回值有什么不同
在C語言中,我們的函數(shù)通常會返回一個(gè)整型值作為狀態(tài)碼用于通知客戶端調(diào)用的結(jié)果。比如0表示成功,非0表示失敗。并且可以通過不同的數(shù)值來表示不同原因?qū)е碌氖 H欢贘ava、C#、C++一類面向?qū)ο笳Z言中,一般不會用返回值來表示狀態(tài)。返回值一般用于表示返回的業(yè)務(wù)值,而異常用于通知客戶端程序運(yùn)行狀態(tài)改變了。
什么時(shí)候需要拋出異常
關(guān)于這個(gè)問題,我想到了一句極其精煉的話:當(dāng)函數(shù)無法完成其宣稱的任務(wù)的時(shí)候拋出異常!
比如上面的那個(gè)日子,當(dāng)listArticle方法由于種種原因無法查詢出文章列表的時(shí)候,則拋出異常。
拋出異常在這種場景下是非常有必要的,因?yàn)檫@樣其他人調(diào)用你的代碼時(shí)可以非常放心的去調(diào)用,只要調(diào)用了你的方法,就會返回文章列表。如果無法返回文章列表,則會拋出異常。完全不用在調(diào)用這個(gè)函數(shù)的時(shí)候去懷疑是否執(zhí)行成功了。
再來一句至理名言:
寧愿終止程序也不要帶錯(cuò)運(yùn)行下去。
也就是說,遇到錯(cuò)誤的時(shí)候,寧愿拋出異常終止程序,也不要帶著錯(cuò)運(yùn)行下去。這是在掩耳盜鈴!
異常需要攜帶什么信息
首先,異常的類型本身會帶有異常種類信息。其次,異常的message屬性可以帶上更詳細(xì)一些的信息。這里需要注意,千萬不要像下面這么做。
拋出異常了肯定是執(zhí)行失敗了呀!帶上這種信息有什么用,不是帶了一句廢話嘛!
應(yīng)該是下面這樣
談到日志,首先要搞清楚一個(gè)問題,日志是干嘛用的?
用來記錄運(yùn)行時(shí)的錯(cuò)誤信息啊!
是啊。好像大家都知道日志是干什么用的,但是為什么寫起代碼來就會忘記初衷呢!
來看看代碼:
這里的代碼是什么意思呢?程序員們應(yīng)該都能明白的!很顯然,這位程序員是想借助這些標(biāo)記來調(diào)試,想知道代碼到底執(zhí)行到哪一行了。但是,這里很明顯地犯了兩個(gè)錯(cuò)。
1. 為什么是System.out.println('');而不是logger.debug('');?
2. 為什么是1、2而不是一些更明確的文字信息呢?
在這里,合理的方式是下面這樣。
我想給正在犯上面的錯(cuò)的同學(xué)提個(gè)醒:
1. 使用日志框架,并用合適的級別輸出日志非常重要。
好多程序員從來不負(fù)責(zé)也不參與運(yùn)維相關(guān)的工作,甚至是做了好幾年的Web都從來沒有自己發(fā)布過網(wǎng)站。所以壓根沒有后期維護(hù)的意識!
如果沒有這些日志,當(dāng)項(xiàng)目上線之后,運(yùn)維的背鍋俠兄弟發(fā)現(xiàn)網(wǎng)站掛了之后只能直接重啟,然后當(dāng)作什么也沒看到。因?yàn)闆]有排錯(cuò)的線索。
2. 輸出有效信息。
不要去輸出一些像1、2、3、成功、失敗、hello這樣的毫無意義的日志,要輸出logger.debug('郵件發(fā)送任務(wù)成功入隊(duì)。任務(wù)id:' + taskId);這樣的有效信息。
也許當(dāng)時(shí)你調(diào)試的時(shí)候,在你看來這些奇怪的字符串是有意義的,但是在其他人看來,這些就是天書。運(yùn)維的背鍋俠會提刀過來砍你的!另外像'-------開始執(zhí)行--------'這種對運(yùn)行期間定位問題沒有半點(diǎn)好處的日志就不要輸出了!自己用可以,提交代碼前一定要刪掉。
3. 日志中帶上上下文信息。
孤立的一句錯(cuò)誤日志通常沒有什么實(shí)際作用。比如上面的例子中,如果在找不到指定的模板文件的時(shí)候未將發(fā)送郵件時(shí)指定的模板文件名輸出,那么排錯(cuò)的時(shí)候無法知道到底是少了哪個(gè)模板文件。
4. 不要在日志中輸出用戶的敏感信息。
千萬不要在日志中輸出像用戶密碼、郵件內(nèi)容之類的涉及用戶隱私的敏感信息,也不要去輸出像驗(yàn)證碼的值之類的敏感信息。
在你對外公開的方法前先插入一些檢查參數(shù)的代碼,以確保方法被“正確的姿勢”調(diào)用。比如:
參數(shù)校驗(yàn)的作用
如果在對外公開的重要方法開始的位置不插入校驗(yàn)參數(shù)的代碼,有時(shí)恐怕方法需要運(yùn)行到方法內(nèi)部比較深的位置才會拋出一個(gè)異常來。而且那種情況下,拋出的異常可能就會有各種各樣的了。比如空指針、除零異常等。
這種情況下,很難一眼看出引發(fā)這個(gè)異常的根源是參數(shù)傳錯(cuò)了。需要對你的代碼進(jìn)行一番調(diào)試才行!如果一開始就在代碼的入口插入了校驗(yàn)參數(shù)的代碼,那么調(diào)用的時(shí)候,一眼就能看出來是參數(shù)傳錯(cuò)了導(dǎo)致了一個(gè)異常。這樣其他程序員看到這個(gè)異常之后就會去看一下你的方法注釋。他一看,哦!原來分頁索引號是從1開始計(jì)數(shù)的,那么這個(gè)問題就會就此打住,給團(tuán)隊(duì)節(jié)省了時(shí)間。
參數(shù)校驗(yàn)問題是會影響團(tuán)隊(duì)運(yùn)行效率的一個(gè)很關(guān)鍵的因素。所以,請同學(xué)們重視起這個(gè)問題來。我們都是工程師,團(tuán)隊(duì)作戰(zhàn)的,自己寫代碼快不叫快,整個(gè)團(tuán)隊(duì)快起來才叫真的快!用好斷言,可以讓你的代碼更健壯。
tips: Java中默認(rèn)斷言是不開啟的,所以建議無視Java語言的斷言,自己處理。
什么時(shí)候需要進(jìn)行參數(shù)校驗(yàn)
我認(rèn)為一個(gè)方法或者函數(shù)在滿足以下條件時(shí)有必要進(jìn)行參數(shù)校驗(yàn):
方法或者函數(shù)是對外公開的,不是私有的。
參數(shù)有可能為空指針的時(shí)候。
參數(shù)的合理值無法通過方法名、參數(shù)名、參數(shù)類型一眼看出來的時(shí)候!比如上面那個(gè)pageIndex是從1開始計(jì)數(shù)的,但別人并不知道你是從1開始計(jì)數(shù)的。
如果對每一方法都進(jìn)行校驗(yàn)的話,其實(shí)挺麻煩的。程序員的時(shí)間是很寶貴的,沒這么多閑工夫。不過在滿足上面條件的情況下,最好還是校驗(yàn)一下。因?yàn)樽隽诉@個(gè)校驗(yàn),你自己是會稍微浪費(fèi)幾分鐘的時(shí)間,不過從團(tuán)隊(duì)整體來看,總的調(diào)試損耗的時(shí)間卻降下來了。要記住方法/函數(shù)寫出來就是給別人調(diào)用的!
參數(shù)校驗(yàn)需要做到什么程度
我有一個(gè)標(biāo)準(zhǔn),就是把自己當(dāng)成調(diào)用這些代碼的那個(gè)人,把自己想象成有可能以任何“姿勢”調(diào)用的菜鳥(實(shí)際上也有可能是不了解你的代碼的大牛)。如果這個(gè)時(shí)候自己也有可能會犯某些錯(cuò)(比如沒注意邊界值,沒注意是否可空),那么這個(gè)時(shí)候是必須要做校驗(yàn)的。對于一些已經(jīng)在其他層做過處理不太可能有錯(cuò)誤的值的情況,可以不做校驗(yàn)。比如你的UserService中有一個(gè)簽名為public void register(User user)的方法,用于注冊一個(gè)用戶。這種情況下,可以只校驗(yàn)一下user參數(shù)是否為空,而不用對user的username、password屬性進(jìn)行校驗(yàn)(用戶名密碼長度是否合法等)。因?yàn)槟阍谏弦粚涌刂破鲗幽P徒壎ǖ臅r(shí)候已經(jīng)做過非常嚴(yán)謹(jǐn)?shù)男r?yàn)了。當(dāng)然,這里如果你有充足的時(shí)間,也可以校驗(yàn)一下。具體做到什么程度,還需要你根據(jù)情況去自己把握。
編碼規(guī)范就是用來約束別人的!
o(∩_∩)o 哈哈!開玩笑的啦!
其實(shí)很多時(shí)候,出于各種原因,如“項(xiàng)目周期緊”、“項(xiàng)目還在探索階段可行性未知,先實(shí)現(xiàn)了再說”、“項(xiàng)目中其他代碼已經(jīng)這樣了,破罐子破摔”等,最終導(dǎo)致的結(jié)果可能就是我們這些自稱“有經(jīng)驗(yàn)”的程序員自己也不一定能寫出完全符合這些理念的代碼來。或許是吧!
ㄟ(▔ ,▔)ㄏ
我承認(rèn),我也寫過奇葩代碼。
但是,這好像并不是你這個(gè)作為未來優(yōu)秀程序員的人不思進(jìn)取的理由。
小時(shí)候,老師教我們要誠實(shí),但是老師自己也不見得能完全做到。我們可以因?yàn)檫@個(gè)鄙視他。
長大后,體驗(yàn)過了生活中會有很多的無奈,不再鄙視“不誠實(shí)”的老師。甚至低下了高貴的頭,自己也變得那般模樣。
未來,你還會教育你的后代“要誠實(shí)”嗎?
恐怕會! 因?yàn)椋瑑?yōu)秀的理念,不管結(jié)局如何,都應(yīng)該去提倡!
本文的觀點(diǎn)僅代表現(xiàn)在的我,人是會成長的,明天的我或許又會有新的見解! 如果你不認(rèn)同部分觀點(diǎn)或者還有其他的優(yōu)秀理念,可以給我留言。我們一起成長!
原作者為Bug輝(Elvin Zeng、zenghui、曾輝也行)
原文鏈接:https://www.bughui.com/2017/08/21/how-to-write-code/