如何寫代碼 —— 編程內功心法

寫代碼就是學一門語言而後開始擼代碼嗎?看完了個人《GoF設計模式》系列文章的同窗或者自己已經就是老鳥的同窗顯然不會這麼認爲。編程是一項很是嚴謹的工做!雖然咱們自嘲爲碼農,可是這工做畢竟不是真正的搬磚,咱們是軟件工程師。編程須要關注的問題太多,不只僅有語言,還有算法、數據結構、編程技巧、編碼風格、設計、架構、工程化、開發工具、團隊協做等方方面面,涉及到不少層面的問題。本文將分享一下根據我這幾年來的編程經驗總結出的一些關於如何寫代碼的我的看法。css

因爲「跟我混」的一些小夥伴編程功底相對來講比較薄弱,因此在此總結一篇「編程內功心法」幫助他們渡過職業生涯的第一個瓶頸期。順便,也造福一下路過的有緣的同窗!因而有了此文。html

前言

首先,思考一個問題,何謂編程?編程就是寫代碼嗎?java

所謂的編程,其實就是不斷的對這個現實世界中的問題創建模型並將其固化爲代碼自動化執行的過程。
~ Bug輝GoF設計模式 - 解釋器模式python

在對問題創建模型的過程當中,咱們會遇到很是多不一樣層面的問題,因此咱們須要不少領域的知識去解決這些問題。git

  • 咱們須要管理被操做的數據,由於數據與數據以前是相互有關聯的。將數據結構化,一般是編程的第一步。關於結構化數據的相關理論以及實踐,須要有一個專門的學科分支或者說課題去研究——數據結構
  • 咱們須要解決一個具體的問題,這個具體的問題如何一步步去解決,過程是怎麼樣子的——算法
  • 咱們須要將解決方案進行自動化,並以代碼的形式進行交付——編程語言
  • 若是將一個抽象的模型進行編碼實現,如何實現「這個功能」,如何實現「那個功能」——編程技巧
  • 問題的規模大了,衆多代碼糅合在一塊兒,連程序員本身都看不懂了!怎麼來拆分、模塊化這些代碼——設計
  • 代碼量已經到了一我的沒法完成的地步了,須要團隊分工合做才能完成了——工程化
  • 你寫的代碼我看不懂,無法調用或者很難調用,我寫的代碼你也看不懂,或者很難看懂。還怎麼愉快的玩耍——編碼風格/編碼規範
  • 問題的規模繼續擴大,到了系統工程的規模了,以前學的套路已經無論用了!怎麼來構建這個系統才能實現正確、安全、高性能、高可用——架構

然而這些也只是一個系統工程中的冰山一角!這是一個龐大的體系。也正是由於軟件開發須要考慮到的問題太多且團隊成員水品良莠不齊,因此團隊開發中並非每一個程序員作的事情都是同樣的。每一個人都有本身的角色、初級工程師、中級工程師、高級工程師、架構師、CTO。。。程序員

因此編程不只僅只是堆砌代碼!github

說到這裏,我想起來了一件事情——爲啥業界廣泛鄙視培訓出來半道出家的新人?人與人的區別是很大的!我見過培訓出來也很牛的。其實,說到底,被鄙視的並非全部人。而是那些培訓了幾個月以後發現隨便找個工做也能拿「高薪」而後還自認爲編程很簡單的新人。由於這種經歷給了他們一種錯覺——編程如此簡單,我培訓幾個月也會嘛!有點像剛學會開車的新司機,很囂張的對老司機說「開車很簡單嘛!你看我也會啊!」。語言和開發工具只是招式,這是外功。而編程思想、經驗是內功。這些內功並非靠短短几個月的培訓可以掌握的,這一點有點像中國製造業和日本製造業的區別。動不動趕英超美可很差。。。算法

編程並不簡單!這是一件很嚴肅的事情。不過今天,我沒有辦法介紹完全部的方面!或者說,到今天爲止,我也並沒能掌握全部領域的知識。因此今天我只是分享一些關於編碼自己的一些經驗。數據庫

另外,本文主要分享如何寫代碼,並非如何用Java寫代碼。因此文章中各類語言都有可能出現。編程

編碼風格

先來一個圈內的段子。
大部分程序員在工做中都很討厭這四件事情:

  1. 寫註釋
  2. 寫文檔
  3. 別人不寫註釋
  4. 別人不寫文檔

o(∩_∩)o 哈哈。。中槍了沒!

這個段子其實反映出來一個問題,即大部分代碼都須要經過大量註釋和文檔來講明才能將意圖傳達給維護這些代碼的程序員!然而,就像上面的段子說的那樣,寫大量的註釋和文檔實際上是一件很麻煩的事情。因此不少時候,因爲嫌麻煩,註釋和文檔就沒寫,致使維護代碼的人至關的痛苦。這個苦同窗們確定都是體會過的!至關於給你個精密儀器要你維護還不給說明書。

其實,打破上面那個段子描述的那個怪圈的一個頗有效的手段就是統一編碼風格。優秀的代碼能夠實現代碼即註釋,代碼自己就能夠很是清晰的體現出它的意圖來,讓別人能夠很容易讀懂。這就是所謂的可讀性!

命名

計算機科學領域中最難的兩件事是命名和緩存失效!命名並不簡單,很複雜。好的名字能夠見名知意,很是容易理解。之因此說命名難是由於命名的過程同時也是概念提取的過程!對問題創建模型,須要提取概念並賦予其「術語」。這個過程實際上是「萬里長征」中最難的一步。畢竟,設計也好,架構也罷,都有成熟的套路能夠參考。惟獨這個過程,是須要程序設計者本身進行充分的思考的創造性工做!

如下是總結出來的一些命名經驗:

  • 一個類是某物、某事、某人的抽象,是數據與行爲的集合體。這剛好符合名詞的定義,所以 類名 是一個名詞!
  • 方法名 或者說 函數名 是某操做或者某過程的抽象,是一個動做。這剛好符合動詞的定義,所以函數名一般是一個動詞。
  • 變量名寧肯長一些說明清楚用途也不要用abc之類的無心義的名稱,除非是循環計數器中用ijk等約定俗成的一些變量名。好比pageIndexpageSize就要比取名成is好!取成這種和用混淆器混淆事後的代碼同樣的名稱沒有什麼好處,若是算法比較複雜的話,過一段時間恐怕本身都會看不懂。
  • 變量名最好包含變量自己的業務含義。好比List<Student> studentList = new ArrayList<>();就比List<Student> list = new ArrayList<>();好不少。若是同一段代碼裏再出現一個List<Teacher>的話,這樣就能夠很方便的取名爲teacherList或者teachers而不是list1list2這樣的毫無心義的名稱!

英文很差怎麼辦

這個問題怎麼說呢。。
做爲一名程序員吧,基礎的英文仍是要懂的。要否則發展也容易遇到天花板,學很差編程的。畢竟,最新的技術、解決方案、工具都是從國外傳過來的。若是是解決一些基礎性的問題,天天只作作CRUD,好像英文確實不怎麼用得上。可是一旦遇到一些實質性問題,恐怕只能到英文網站上找嘍!ㄟ(▔ ,▔)ㄏ 不要跟我說你編程能夠不須要Stack Overflowcopying and pasting from stackoverflow 但是終極編程大法!o(∩_∩)o 這句話但是編程的真諦啊!(若是你看不懂這個梗那你有多是僞程序員)

其實,話說回來,實在不方便用英文的時候,我認爲也能夠用拼音命名。這個問題上能夠務實一點,量力而行。可是,拼音和英語混用的作法就不太好了。最好別這樣!逼格不高。

註釋

怎麼添加代碼註釋

關於註釋,咱們須要解決的第一個問題是如何添加代碼註釋。

對於Java、C#之類的語言,有專用的文檔註釋語法,很好處理。對於C/C++,則按約定的格式說明一下類和函數、代碼片斷的做用和意圖便可,至少編譯器會進行靜態檢查。在Python中,有更牛逼的文檔字符串這樣的語言級特性支持,看註釋用help()很方便。不過對於Lua這樣的弱類型解釋型語言,註釋就比較難處理了。這裏以Lua爲例給出一種註釋的解決方案。

借用Java語言文檔註釋的風格。

文件註釋,或者說類/模塊註釋。

--[[
    Object-oriented helper functions for Lua
    @author: Elvin Zeng
    @date: 2017-8-21
--]]

函數註釋

--[[
    create a class with specified super class.
    if number of parameters is zero, derived class will extends from {}.
    @param superClass super class of target class
    @return derived class
--]]
local function createClass(superClass)
    local derivedClass = {}

    --  省略一堆代碼

    return derivedClass
end
--[[
    register a new account
    @param user
      {
        username = "username",
        password = "password"
      }
    @return registered user
--]]
local function register(user)

end

tips: Lua中能夠經過metatable機制實現類和繼承,這一點與Javascript經過原型機制來實現類和繼承有點相似。

註釋裏該寫些什麼

咱們首先來看個反例。

/**
 * 查詢
 */
public List<Article> queryPage(int pageSize, int pageIndex) throws PageIndexOutOfBoundsException {
    //  定義一個整型變量
    int offset;

    //  省略一堆代碼
}

首先這個方法名自己就取得很差,這個暫且不說,先說註釋問題。這裏的註釋犯了幾個錯:

  1. 方法註釋爲「查詢」,這簡直就是廢話!方法名已經告訴別人這是查詢方法了,還在這個註釋裏寫這兩個字有什麼意義呢?並且到底查詢些什麼這裏也沒說!
  2. 參數沒有註釋。沒有描述每個參數的意義以及取值範圍等!
  3. 什麼狀況下會拋出PageIndexOutOfBoundsException沒有描述清楚。
  4. 「定義一個整型變量」這種垃圾註釋就不要寫了,這麼簡單的語句誰看不懂啊!若是要註釋,也是寫上這個變量的含義。

這裏咱們先不考慮設計問題(分頁索引號最好作成能夠本身調整成合理值),下面再來看改善註釋以後的代碼。

/**
 * 列出指定分頁的文章
 * @param pageSize 分頁大小。若是等於0則表示查詢出全部文章。
 * @param pageIndex 分頁索引號。必須爲一個大於0的整數,第一頁索引爲1。
 * @return 指定分頁的文章列表
 * @throws PageIndexOutOfBoundsException 當分頁索引號超出正常範圍時拋出,即pageIndex小於0或大於最大頁索引時。
 */
public List<Article> listArticle(int pageSize, int pageIndex) throws PageIndexOutOfBoundsException{
    //  第一條文章記錄在MySql數據庫中的偏移量
    int offset;

    //  省略一堆代碼
}

改完以後的註釋有沒有感受信息更全不少!雖說代碼自己就是最好的註釋,可是必要的註釋仍是得寫上去,畢竟調用的時候別人無法猜想你的索引號到底從0仍是從1開始。另外,若是函數內算法比較複雜,能夠在代碼塊內註釋,也能夠在函數註釋上直接寫清楚這個函數內部的大概算法/邏輯。代碼寫出來就是給別人調用的,若是沒有基本的註釋信息,那麼每次調用你的代碼的時候,都得去看一下你的函數內具體邏輯才能知道怎麼調用。這顯然是很是低效的!

命名與註釋這兩個基本方面沒作好的話,會影響到整個團隊的運做。也就是說,你封裝的東西並無給隊友節省什麼時間,別人用到你的代碼的時候,又須要花上一些時間去讀你的代碼。若是團隊裏每一個人都這樣,那整個團隊都會極其低效。我我的是很是不肯意與這種代碼風格惡劣的人合做的。

參考規範

關於編碼風格的問題,本文只說命名和註釋這兩個方面。關於縮進、空格、斷行、空行等其餘方面的問題,能夠參考本節給出的參考規範。

不一樣的企業會有不一樣的編碼規範,因此這裏沒有辦法給出一個符合全部公司的規範。不過制定本身團隊的規範的時候,能夠參考一些大企業的作法。如下是世界上最大的互聯網公司谷歌的編碼規範,同窗們能夠參考這個。

異常處理

異常與返回值有什麼不一樣

在C語言中,咱們的函數一般會返回一個整型值做爲狀態碼用於通知客戶端調用的結果。好比0表示成功,非0表示失敗。而且能夠經過不一樣的數值來表示不一樣緣由致使的失敗。然而在Java、C#、C++一類面嚮對象語言中,通常不會用返回值來表示狀態。返回值通常用於表示返回的業務值,而異經常使用於通知客戶端程序運行狀態改變了。

何時須要拋出異常

關於這個問題,我想到了一句極其精煉的話:當函數沒法完成宣稱的任務的時候拋出異常!
好比上面的那個日子,當listArticle方法因爲種種緣由沒法查詢出文章列表的時候,則拋出異常。

拋出異常在這種場景下是很是有必要的,由於這樣其餘人調用你的代碼時能夠很是放心的去調用,只要調用了你的方法,就會返回文章列表。若是沒法返回文章列表,則會拋出異常。徹底不用在調用這個函數的時候去懷疑是否執行成功了。

再來一句至理名言:

寧願終止程序也不要帶錯運行下去。

也就是說,遇到錯誤的時候,寧願拋出異常終止程序,也不要帶着錯運行下去。這是在掩耳盜鈴!

異常須要攜帶什麼信息

首先,異常的類型自己會帶有異常種類信息。其次,異常的message屬性能夠帶上更詳細一些的信息。這裏須要注意,千萬不要像下面這麼作。

throw new PageIndexOutOfBoundsException("失敗!");

拋出異常了確定是執行失敗了呀!帶上這種信息有什麼用,不是帶了一句廢話嘛!
應該是下面這樣

throw new PageIndexOutOfBoundsException("參數分頁索引號pageIndex不能大於分頁總數");

此外,異常堆棧也會攜帶不少信息。

日誌

談到日誌,首先要搞清楚一個問題,日誌是幹嗎用的?
用來記錄運行時的錯誤信息啊!
是啊。好像你們都知道日誌是幹什麼用的,可是爲何寫起代碼來就會忘記初衷呢!
來看看代碼:

/**
 * 異步發送通知郵件。
 * @param templateFile 郵件模板文件路徑,相對於classpath。
 * @param modelMap 模型對象
 */
public void sendEmail(String templateFile, Map<String, String> modelMap){
    //  這裏省略一些代碼
    System.out.println("1");
    //  這裏省略一些代碼
    System.out.println("2");
    //  這裏省略一些代碼
}

這裏的代碼是什麼意思呢?程序員們應該都能明白的!很顯然,這位程序員是想借助這些標記來調試,想知道代碼到底執行到哪一行了。可是,這裏很明顯地犯了兩個錯。

  1. 爲何是System.out.println("");而不是logger.debug("");?
  2. 爲何是12而不是一些更明確的文字信息呢?

在這裏,合理的方式是下面這樣。

/**
 * 異步發送通知郵件。
 * @param templateFile 郵件模板文件路徑,相對於classpath。
 * @param modelMap 模型對象
 * @throws ServiceException 當郵件模板文件不存在或者modelMap中缺乏必須的字段。
 */
 public void sendEmail(String templateFile, Map<String, String> modelMap) throws ServiceException{
     //  這裏省略一些代碼
     if (isTemplateExists){
         logger.debug("模板文件存在");
         //  這裏省略一些代碼
         logger.debug("郵件發送任務成功入隊。任務id:" + taskId);
         //  這裏省略一些代碼
     }else{
         logger.error("指定的模板文件[" + templateFile + "]不存在,郵件發送失敗。");
         //  拋出異常
     }
 }

我想給正在犯上面的錯的同窗提個醒:

  1. 使用日誌框架,並用合適的級別輸出日誌很是重要。
    好多程序員歷來不負責也不參與運維相關的工做,甚至是作了好幾年的Web都歷來沒有本身發佈過網站。因此壓根沒有後期維護的意識!
    若是沒有這些日誌,當項目上線以後,運維的背鍋俠兄弟發現網站掛了以後只能直接重啓,而後看成什麼也沒看到。由於沒有排錯的線索。

  2. 輸出有效信息。
    不要去輸出一些像123成功失敗hello這樣的毫無心義的日誌,要輸出logger.debug("郵件發送任務成功入隊。任務id:" + taskId);這樣的有效信息。
    也許當時你調試的時候,在你看來這些奇怪的字符串是有意義的,可是在其餘人看來,這些就是天書。運維的背鍋俠會提刀過來砍你的!另外像"-------開始執行--------"這種對運行期間定位問題沒有半點好處的日誌就不要輸出了!本身用能夠,提交代碼前必定要刪掉。

  3. 日誌中帶上上下文信息。
    孤立的一句錯誤日誌一般沒有什麼實際做用。好比上面的例子中,若是在找不到指定的模板文件的時候未將發送郵件時指定的模板文件名輸出,那麼排錯的時候沒法知道究竟是少了哪一個模板文件。

  4. 不要在日誌中輸出用戶的敏感信息。
    千萬不要在日誌中輸出像用戶密碼、郵件內容之類的涉及用戶隱私的敏感信息,也不要去輸出像驗證碼的值之類的敏感信息。

參數校驗

在你對外公開的方法前先插入一些檢查參數的代碼,以確保方法被「正確的姿式」調用。好比:

/**
 * 列出指定分頁的文章
 * @param pageSize 分頁大小。若是等於0則表示查詢出全部文章。
 * @param pageIndex 分頁索引號。必須爲一個大於0的整數,第一頁索引爲1。
 * @return 指定分頁的文章列表
 * @throws PageIndexOutOfBoundsException 當分頁索引號超出正常範圍時拋出,即pageIndex小於0或大於最大頁索引時。
 */
public List<Article> listArticle(int pageSize, int pageIndex) throws PageIndexOutOfBoundsException{
    if (pageSize < 0){
        throw new IllegalArgumentException("pageSize不能小於0");
    }
    if (pageIndex < 1){
        throw new IllegalArgumentException("pageIndex不能小於1");
    }

    //  第一條文章記錄在MySql數據庫中的偏移量
    int offset;

    throw new PageIndexOutOfBoundsException("");

    //  省略一堆代碼
}

參數校驗的做用

若是在對外公開的重要方法開始的位置不插入校驗參數的代碼,有時恐怕方法須要運行到方法內部比較深的位置纔會拋出一個異常來。並且那種狀況下,拋出的異常可能就會有各類各樣的了。好比空指針、除零異常等。
這種狀況下,很難一眼看出引起這個異常的根源是參數傳錯了。須要對你的代碼進行一番調試才行!若是一開始就在代碼的入口插入了校驗參數的代碼,那麼調用的時候,一眼就能看出來是參數傳錯了致使了一個異常。這樣其餘程序員看到這個異常以後就會去看一下你的方法註釋。他一看,哦!原來分頁索引號是從1開始計數的,那麼這個問題就會就此打住,給團隊節省了時間。

參數校驗問題是會影響團隊運行效率的一個很關鍵的因素。因此,請同窗們重視起這個問題來。咱們都是工程師,團隊做戰的,本身寫代碼快不叫快,整個團隊快起來才叫真的快!用好斷言,可讓你的代碼更健壯。

tips: Java中默認斷言是不開啓的,因此建議無視Java語言的斷言,本身處理。

何時須要進行參數校驗

我認爲一個方法或者函數在知足如下條件時有必要進行參數校驗:

  1. 方法或者函數是對外公開的,不是私有的。
  2. 參數有可能爲空指針的時候。
  3. 參數的合理值沒法經過方法名、參數名、參數類型一眼看出來的時候!好比上面那個pageIndex是從1開始計數的,但別人並不知道你是從1開始計數的。

若是對每一方法都進行校驗的話,其實挺麻煩的。程序員的時間是很寶貴的,沒這麼多閒工夫。不過在知足上面條件的狀況下,最好仍是校驗一下。由於作了這個校驗,你本身是會稍微浪費幾分鐘的時間,不過從團隊總體來看,總的調試損耗的時間卻降下來了。要記住方法/函數寫出來就是給別人調用的!

參數校驗須要作到什麼程度

我有一個標準,就是把本身當成調用這些代碼的那我的,把本身想象成有可能以任何「姿式」調用的菜鳥(實際上也有多是不瞭解你的代碼的大牛)。若是這個時候本身也有可能會犯某些錯(好比沒注意邊界值,沒注意是否可空),那麼這個時候是必需要作校驗的。對於一些已經在其餘層作過處理不太可能有錯誤的值的狀況,能夠不作校驗。好比你的UserService中有一個簽名爲public void register(User user)的方法,用於註冊一個用戶。這種狀況下,能夠只校驗一下user參數是否爲空,而不用對userusernamepassword屬性進行校驗(用戶名密碼長度是否合法等)。由於你在上一層控制器層模型綁定的時候已經作過很是嚴謹的校驗了。固然,這裏若是你有充足的時間,也能夠校驗一下。具體作到什麼程度,還須要你根據狀況去本身把握。

後記

編碼規範就是用來約束別人的!
o(∩_∩)o 哈哈!開玩笑的啦!
其實不少時候,出於各類緣由,如「項目週期緊」、「項目還在探索階段可行性未知,先實現了再說」、「項目中其餘代碼已經這樣了,破罐子破摔」等,最終致使的結果可能就是咱們這些自稱「有經驗」的程序員本身也不必定能寫出徹底符合這些理念的代碼來。或許是吧!
ㄟ(▔ ,▔)ㄏ
我認可,我也寫過奇葩代碼。
可是,這好像並非你這個做爲將來優秀程序員的人不思進取的理由。

小時候,老師教咱們要誠實,可是老師本身也不見得能徹底作到。咱們能夠由於這個鄙視他。
長大後,體驗過了生活中會有不少的無奈,再也不鄙視「不誠實」的老師。甚至低下了高貴的頭,本身也變得那般模樣。
將來,你還會教育你的後代「要誠實」嗎?
恐怕會!
由於,優秀的理念,無論結局如何,都應該去提倡!

本文的觀點僅表明如今的我,人是會成長的,明天的我或許又會有新的看法! 若是你不認同部分觀點或者還有其餘的優秀理念,能夠給我留言。咱們一塊兒成長!

做者:Bug輝 本文是個人原創文章。獨立博客連接:https://www.bughui.com/2017/08/21/how-to-write-code/ 版權聲明:《如何寫代碼 —— 編程內功心法》 由 Bug輝 採用 知識共享 署名-非商業性使用-禁止演繹 4.0 國際 許可協議 進行許可。轉載或引用文章時請註明原做者並帶上原文連接。

相關文章
相關標籤/搜索