跳轉到內容

如何編寫程式/收集真實經驗

來自華夏公益教科書,開放世界開放書籍

為什麼是這本書

[編輯 | 編輯原始碼]

良好的實踐和生活經驗從未在書籍中被記錄下來。大多數計算機科學書籍都是 API 遍歷。這本書是與眾不同的!

構建指令碼

[編輯 | 編輯原始碼]

嘗試設定 "一鍵測試"。這使得在輸入少量程式碼後點擊該按鈕變得更加方便,該按鈕可以

  • 儲存你剛剛編輯的檔案,
  • 使用所有適當的選項(如果需要,例如“-fno-emit-frame”)編譯應用程式,以及
  • 執行一些快速測試,以確保你的“改進”不會意外地破壞其他東西。

花一個小時來編寫一些測試並設定一鍵測試可能 *看起來* 比實際麻煩。手動編譯檔案,並手動執行應用程式的各個部分以確保它們正常工作,可能花費不到一個小時。但是相信我,你將要編輯、編譯和測試的大多數程式都會執行很多很多次。一年後,當你只進行了一點小小的改動時,你是否寧願按下按鈕然後完成它,而不是

  • 手動編譯檔案
  • 手動執行應用程式並發現它突然不再工作
  • 扯頭髮直到
  • 幾個小時後,你才想起你需要包含“-fno-emit-frame”
  • 手動重新編譯檔案,這次使用“-fno-emit-frame”
  • 從頭開始測試所有內容。

有很多方法可以設定 自動構建系統

一鍵測試只是某些程式設計師推薦的持續整合的一部分。

即使律師也能看到自動化構建指令碼的優勢。 [1]

指令碼編寫概述

[編輯 | 編輯原始碼]

這不僅適用於構建指令碼。如果你發現自己重複了幾行程式碼,就把它放到指令碼或批處理檔案中。

如果你是一個 Windows 程式設計師,學習使用 Cygwin[2] bash 指令碼通常是值得的。它們比 Windows 批處理檔案靈活得多。還可以看看 AutoHotKey[3]

同樣,不要害怕構建任何形式的工具!你是一名程式設計師,如果你處理大量元資料,那就製作一個元資料編輯器。如果你的構建檔案選項太多,而且它們經常改變,那就為它編寫一個 GUI。你可以跳過的每一步都是你不會出錯的一步——而且一次錯誤可能花費的時間與構建指令碼或工具的時間一樣長。

指令碼和工具的另一個原因是知識保留/交流。你可以告訴同事如何進行構建 4 次,直到他記住每個步驟,或者你可以將你使用的指令碼交給他。Ftorkou besaha

資料庫訪問

[編輯 | 編輯原始碼]

併發問題

[編輯 | 編輯原始碼]

不要使用表鎖定!它有缺陷!使用事務,在 JDBC 中設定連線的隔離級別。

錯誤處理

[編輯 | 編輯原始碼]

始終記得使用 finally 塊來關閉資源!否則,丟擲異常可能會導致許多開啟的連線。

小心空的錯誤處理程式... 錯誤處理程式永遠不應該為空,但這在 Java 中很難強制執行,因為存在受檢異常。例如,Java 的 sleep 方法會丟擲一個受檢異常,因此你必須捕獲它。很少有人真正使用它,也不在乎,因此他們用一個空處理程式將其包裝起來。因此,受檢異常被認為是一種反模式或只是存在缺陷。

當堆疊跟蹤被隱藏,因為有人認為它們“不重要”或很混亂,或者其他人捕獲了範圍過大的異常,並隱藏了本應該讓你的錯誤修復變得輕而易舉的跟蹤資訊時,追蹤錯誤變得極其困難。

錯誤跟蹤

[編輯 | 編輯原始碼]

每當你輸出任何內容作為錯誤訊息時,請在它前面加上例如 new Date() 或類似的內容,以便你以後能夠跟蹤回去。將模組名稱新增到錯誤訊息中也很有用。嘗試將錯誤處理作為可測試性的一部分構建。不要只依賴偵錯程式來查詢程式碼錯誤。

你的時間在設定 Log4J 等日誌記錄框架上花費得很好。請注意,某些日誌記錄軟體配置由於日誌記錄功能期間的效率損失而不太適合生產程式碼。對各種設定組合進行一些迭代測試,以瞭解速度損失,然後決定使用最佳組合。

許多程式設計師都非常糟糕地估計向程式新增某些功能需要多長時間。練習,練習,再練習。即使沒有人問你需要多長時間,也要記錄何時開始新增該功能,何時(估計)你會完成以及何時(實際)完成。即使你完全沒有概念,至少也要記錄何時開始和何時停止。每次,你都會學習有關完成此類任務需要多長時間的新知識。

註釋!

[編輯 | 編輯原始碼]

令人驚奇的是,有多少程式設計師就是不願意寫註釋,無論有多少不同的編碼規範建議(甚至強制)程式碼要寫好註釋。

註釋對於軟體專案的維護階段非常重要。任何曾經被要求分析、修改或維護其他人程式碼的人都會證明,如果沒有註釋,這項工作會比在有良好註釋的程式碼上完成相同任務需要更多努力、時間和金錢。別忘了,你可能最終會自己負責這項任務,而且是在很久以後。

不寫過多的註釋與不寫過少的註釋一樣重要。在每一行程式碼的末尾添加註釋是完全多餘的。相反,假設讀者對該語言有一定的瞭解,並將註釋限制在對程式碼塊功能的描述上。

無論何時你檢視程式碼(即使是你自己的程式碼),如果你對它有一點理解上的困難,就需要添加註釋。如果你難以理解它,就需要重構它。

你可能會發現,在完成編碼方法後添加註釋會更舒服——回到頂部,逐一瀏覽每個步驟。這也能讓你對程式碼進行一次小的審查。在編碼之前添加註釋也是一個好習慣——先在註釋中建立一個基本流程,然後在註釋之間插入程式碼。只是別忘了在完成編碼後檢查一下注釋的正確性,我經常會在完成編碼後只留下幾個原始註釋。

另一個重要的概念是“自文件化程式碼”。這意味著你使用函式、變數、類和方法的名稱來傳達你的程式碼的思想,這樣你就不必寫很多註釋。如果你使用一個與它執行的動作相對應的名稱來命名一個方法,那麼讀者就能清楚地知道它將要做什麼;例如,使用一個名為determineIfStackIsEmpty()的方法,而不是僅僅進行一個比較,比如if( size == 0 )

不要忽視程式碼模組中的檔案頭。這些檔案頭很重要,因為它們提供了對程式碼操作的概要,以及作者和許可資訊。大多數編輯器程式在螢幕頂部開啟程式碼模組,第一行程式碼位於最上面;在檔案的最上面部分包含一個塊註釋,該註釋指定該模組的功能,這在維護期間非常有用。

寫註釋並不是一項難以掌握的技能;事實上,很容易為幾種語言建立一組模板,其中包含標準註釋形式,然後在開始一個新模組時複製模板,新增程式碼和相應的註釋細節。一些整合開發環境工具,比如EclipseX-Code,可以配置為在建立新原始檔時自動執行此操作。

最後,在中大型專案中,使用自動文件工具,比如DoxygenJavaDocsAutodoc,為專案中的模組建立超文字連結文件,可能會很有幫助。這樣就可以為專案的所有成員提供相同的標準文件,以便每個人都能瞭解專案中所有程式碼模組的可用性和操作細節。

要了解一些可用的基於程式碼的文件,可以檢視庫中的Java原始碼。每個類都有一個註釋塊,顯示一般類的用法,每個外部方法都有一個註釋塊,描述用法和注意事項,每個非平凡的方法都有內聯(非javadoc)註釋,描述每個程式碼塊(通常是每行程式碼)正在進行的操作。總的來說,可能每行程式碼有5-10行註釋,但方法註釋比需要更詳細,除非你是為公開使用而構建一個庫。

(在閱讀Sun的原始碼時,你會發現大多數方法只有1-3行。好的面向物件設計看起來像“魔法”——你認為一定有一個巨大的例程存在於某個地方,做著所有的“實際工作”,但你永遠看不到它,它只是這些小型的1-3行例程,一直到中心。)

如果你被鍵入註釋(即使是每行程式碼3行註釋)所拖累,那麼你要麼非常需要一個打字課,要麼你打字速度快於你的思考速度!請花時間寫你的程式碼——不要只是草率地寫完,檢查它,然後轉到下一個。在你重構你的第一遍程式碼時,審查和重寫註釋。你可能認為你正在做你的老闆想要的事情,因為你儘可能快地打出程式碼更改和錯誤修復,但實際上你正在做的是(根據我的經驗)讓你的整個團隊因為嚴重的錯誤數量和可維護性問題而被解僱。

配置管理

[edit | edit source]

配置管理,也稱為“CM”(有時也稱為“SCM”,表示“軟體CM”),是一個被誤解的主題。雖然最近態度有所改變,但普遍的看法似乎是CM是一個“必要的邪惡”,應該容忍,但不應該積極參與,至少不要超過絕對必要。

當計算機軟體被構建時,變化是不可避免的,而變化會增加在專案中工作的開發人員之間的混亂程度。當修改在執行之前沒有被分析、在進行之前沒有被記錄下來、沒有以一種提高質量、減少錯誤的方式進行控制,或者沒有適當地嚮應該知道的人員報告時,就會出現混亂。

需要更改的原因有很多。首先,客戶可能會提出一個或多個新的需求,或者可能會提出更改現有需求的請求。這可能是由設計評審、應用程式的重新工程,甚至是進度和預算限制驅動的。其次,應用程式的業務條件或市場變化可能會導致需要更改。第三,業務環境可能會增長或變化,這可能會改變專案的優先順序或客戶或供應商工程團隊的結構。要記住的是,隨著時間的推移,所有利益相關者都會對系統有更深入的瞭解;這種知識的增長是導致絕大多數軟體修改請求/要求的原因。因此,這是一個事實,許多軟體工程師和專案經理很難接受,即大多數軟體更改都是合理的

配置管理處理軟體工程專案的多版本功能。CM是一組被開發出來的活動,用於幫助管理軟體專案生命週期中發生的更改。在一個典型的專案中,可交付的系統包含許多不同的檔案和目錄,它們之間可能會發展出許多複雜的關係。在開發過程中需要頻繁修改,這個問題被進一步加劇。糟糕的CM的最終結果包括

  • 多次查詢/修復同一個錯誤(版本之間不一致)
  • 文件和程式碼之間不一致
  • 文件丟失
  • 程式碼丟失

在一個軟體系統中,配置包括與系統相關的所有“工作產品”。這不僅意味著實際交付給客戶的系統,還包括開發人員維護的文件和支援程式碼。配置還可以包括針對不同平臺的不同版本的可執行檔案,並且可能包含相關的缺陷和任務,這些缺陷和任務管理著模組分配給程式設計團隊的成員。

要了解與CM相關的問題,重要的是要了解作為典型軟體專案一部分的大量物件。即使是一個小型系統也可能包含超過一百個檔案。對於現實大小的系統,這個數字可能會增長到數千甚至數萬。與CM相關的問題在範圍上可能類似於現代圖書館所面臨的問題。如果沒有仔細的庫存控制,圖書館中的許多書籍將會被放錯地方、放錯架子、被盜,甚至丟失。

另一件需要記住的事情是,有效的CM計劃是任何聲稱堅持能力成熟度模型®整合(CMMI)原則的軟體開發工作的一部分。這個系統是由卡內基梅隆大學軟體工程研究所(SEI)建立的,它為軟體開發提供了一種流程改進方法。它有5個級別,其中前三個級別都包含某種形式的已記錄的CM流程。

CVS/Subversion

[edit | edit source]

Subversion (SVN) 似乎在功能上已經超越了CVS(參見 TortoiseSVN)。但是,CVS 有一個非常不錯的介面 WinCVS。

最佳化

[edit | edit source]

你可能聽說過“過早最佳化是萬惡之源”。這是真的。我建議你遵循以下準則。

  • 首先總是編寫未最佳化的程式碼。
  • 如果你的應用程式太慢,執行一個分析器來找出到底是哪裡。
  • 重寫指示的程式碼以使其更加最佳化,但將舊程式碼保留在註釋中。
  • 重新測試,如果你的應用程式沒有明顯更快,就刪除最佳化。

注意,這不是對無知的藉口!作為一個程式設計師,你必須理解一些基本概念,比如如果你正在進行插入排序,選擇一個連結串列而不是一個數組!這不是最佳化,而是程式設計!

有關使程式執行更快的多種方法,請參見最佳化程式碼以提高速度

避免程式碼重複

[edit | edit source]

我所有規則中最重要的規則就是永遠不要重複程式碼。即使只有一兩行類似的程式碼也可能導致問題。永遠不要複製和貼上。一套絕對沒有冗餘的程式碼稱為“完全分解”。完全分解的程式碼比使用過多的複製和貼上建立的程式碼容易處理得多。

這也稱為“DRY”原則:不要重複自己。

我敢說,你可以透過語言避免重複程式碼的難易程度來區分它們。

識別重複程式碼

[edit | edit source]

複製貼上者通常會複製一個程式碼塊並貼上多次,然後更改一些常量以適應新程式碼塊的需求。這些很容易識別,只需掃描程式碼中重複的行長度模式即可。

從重複的程式碼塊中提取唯一的資料

[edit | edit source]

通常的解決方案是將常量提取到陣列中,刪除除一個之外的所有重複程式碼,然後遍歷剩餘的程式碼副本。

這通常會導致程式碼減少的其他機會。例如,當代碼中變化的“常量”是函式/方法呼叫(在每個重複程式碼集中,同一位置的不同方法名)時,您可能會發現被呼叫的方法存在很多冗餘。這通常還會導致建立以前沒有的新物件和可重用的資料結構。良好的最佳化通常會引發一系列重構,您可能會發現自己刪除了如此多的程式碼,以至於令人尷尬。

某些語言結構似乎會吸引程式設計師進行程式碼複製。例如,設定 GUI 元件可能非常重複。這是一個尋找程式碼減少機會的好地方。Java 要求每個 MenuItem 物件都定義一個類。通常你會看到這種實現方式是用一屏程式碼來建立 MenuItem 物件,而實際上它可以用定義一組資料和編寫一個小型例程來建立所有 MenuItem 類來更輕鬆地完成(實際上要容易得多)。此時你會發現,像為每個選單項新增按鈕這樣的操作,在重構之前可能很可怕,現在變得微不足道。

完全分解程式碼通常需要設定一些資料並對其進行迭代。在 Java 中,在程式碼中設定一個數據陣列非常簡潔。之後,陣列資料可以非常輕鬆地外部化,比原來的複製貼上程式碼要容易得多。要在 Java 中設定陣列,請習慣以下簡單語法(其他語言應該有類似的結構)

String[] data = new String[] {"Opt1", "Opt2", "Opt3"};

字串很常見,但您也可以使用 int 或 long 陣列 - 語法相同。我經常使用 Object 陣列將整數與字串配對:new Object[] {"One", 1, "Two", 2}...(這將在 Java 5 中有效,在 Java 5 之前,您最好使用兩個陣列。這些陣列的訣竅是保持語法簡單和簡潔。

通常在資料中還需要控制訊號。不要害怕在程式碼中包含所需的訊號。例如,如果您要自動生成一組選單,則需要識別哪些選單是頂級選單,哪些是子項。像這樣的列表應該是您需要的全部資料

"^", "File", "Load", "Save", "^", "Edit", "Copy", "Paste"

這些資料可以被輸入到一個簡單的迴圈中(最多幾行)來建立您的整個選單結構。

下一步,建立一個新類!

[edit | edit source]

從重構中建立的這些資料結構通常最終會包含資料對、三元組或更糟的情況。任何時候您都有想要分組在一起的資料對或集,您都應該定義一個自定義物件來儲存它們。我知道這聽起來很極端,但嘗試一下。您會突然發現您一直都需要那個物件。您會很快發現自己將以前是靜態實用方法(不好的程式碼氣味)的程式碼移動到您的新類中,它將具有完美的程式碼氣味。

您可以建立一個字串或物件陣列並對其進行迭代以建立自定義物件,但更好的方法是直接在陣列中建立物件

這裡有一個我在過去使用過的方法,有點棘手

  MyClass[] primaryData=new MyClass[] {
    new MyClass("File", top),
    new MyClass("Save", "Save the file", "saveFunc"),
    new MyClass("Load", "Load the file", "loadFunc"),
    new MyClass("Edit", top),
    new MyClass("Copy", "Copy the selection", "copyFunc", "isTextSelectedFunc"),
  };

這使用了幾個技巧。首先,它有多個建構函式。最上面的一個是(string,int),因此它與眾不同,系統知道要從中建立一個新的頂級選單項。之後新增的所有(沒有 int "top")都將成為該選單的成員。其餘的建構函式接受字串 - 一個字串建構函式接受 3 個字串,另一個接受 4 個。

第一個字串顯然是選單名稱。第二個是工具提示,第三個是呼叫的函式名稱(在原始物件中)(這是透過反射完成的)。第四個(如果存在)被認為是一個返回 T/F 的布林方法名稱,可以根據返回值停用該選單項。

這段程式碼並不準確,它只是一個示例。為了真正實現它,MyClass 需要訪問一些其他東西(例如呼叫物件),但這可以給你一個想法。

使用這種結構來實現一個典型的包含 20 或 30 個條目的選單可以替換許多螢幕的複製貼上程式碼。此外,您不必處理難看的“內部類”語法。您確實需要處理難看的反射語法,但這被埋藏在 MyClass 物件中,只需要處理一次(永遠)。

您可能還會注意到,現在您所有的字串都在一個地方。這意味著用資料驅動的例程替換這 few 行程式碼會非常容易。這可能是下一步,也可能不是。我不建議嘗試直接跳轉到資料驅動。

關於程式碼示例的說明

[edit | edit source]

我以這種方式編寫程式碼是為了說明將類儲存在陣列中的要點。如果你要真正這樣做,我建議建立一個 MyClassHolder 例項並傳入呼叫類,以便 MyClassHolder 可以對其進行反射呼叫 - 然後,對於每個後續的 MyClass 行,不要使用 "new",而是呼叫 MyClassHolder 中的一個方法,該方法為你建立 MyClass 並將例項儲存在自己內部。如果你想看到真正的實現,請在討論標籤中留言。此外,我本可以使用只有一個引數的事實來區分“頂級”專案,但我試圖展示一種在您無法僅透過引數來區分每個方法時可以使用的方法。

警告:不要依賴可見字串

[edit | edit source]

注意,上面的示例中資料有一些冗餘。

new MyClass("Save", "Save the file", "saveFunc"),

例如,可以從 Save 中計算出 saveFunc。例如,您可以簡單地說儲存函式必須命名為 "doXFunc"。因此,建立一個 "Save" 選項將自動呼叫 doSaveFunc。

這通常在一開始有效,並且似乎是一個很好的技巧,但根據我的經驗 - 我總是後悔將我的內部名稱繫結到外部顯示的文字,或使用外部顯示的文字作為任何型別的“金鑰”。最終,有人會想用不同的語言實現它,或者其他什麼。我贊成簡潔,但在這個情況下,它不值得。

警告:小心使用資料

[edit | edit source]

每當你切換到資料驅動程式碼時,你生活中的大部分都會變得容易得多,但是某些型別的除錯會變得困難得多 - 請記住,現在是你檢查資料的工作。

  • 明確定義資料,以便可以更改它。
  • 對資料非常挑剔,只允許您定義的結構和值
  • 檢查資料中的引用(如果存在)。
  • 用清晰的錯誤說明大聲地失敗。
  • 當用戶忽略你的解釋並說這是你的程式碼錯誤而沒有閱讀它時,問問他們你應該怎麼做才能讓他們閱讀資訊並修復他們的問題。把你學到的所有東西都放回程式碼中。
  • 如果資料足夠複雜,請考慮實現一個編輯器。
  • 資料可以很容易地儲存在 .properties 檔案(煩人)、XML 或資料庫中 - 或任何其他方式,選擇對你最簡單的方式 - .properties 檔案使得關聯資料變得困難,而資料庫幾乎需要一個編輯器。XML 是一個很好的平衡。

一定要驗證資料與程式其餘部分之間的界限。例如,在上面的示例中,在載入時檢查每個反射字串。事實上,你應該在載入 MyClass 物件時例項化 Method 物件,並存儲 Method 物件,而不是每次都進行反射。

請記住,如果你編寫了像這樣的錯誤檢查程式碼,那麼資料中的拼寫錯誤很容易檢測到 - 否則幾乎不可能檢測到。

匿名內部類

[edit | edit source]

匿名內部類是導致複製貼上的另一個情況。例如,每當控制元件的值發生更改時,您可能希望驗證表單。最糟糕的情況:整個驗證方法被複制貼上到每個匿名監聽器中。更好的情況:每個監聽器都呼叫一個“驗證方法”。

所有匿名監聽器應該相似,如果不是完全相同。

建立一個內部類(非匿名),繼承您的基本監聽器型別(ActionListener 等)。將來自匿名內部類之一的程式碼放入這裡。如果所有內部類都相同,您可以將該類設為無狀態(沒有內部變數)。

如果它是無狀態的,建立一個單例並將其傳遞到每個建立監聽器的位置。您就完成了。根據其他正在進行的操作,您可能將檔案大小減少了 1/3 到 2/3。您可能還在此過程中消除了 2 到 3 個與打字錯誤相關的錯誤。

如果您的匿名內部類略有不同,您應該可以在例項化內部類時傳遞一個變數,並使用該變數來控制差異。如果您這樣做,您將不得不建立多個類例項——每個不同型別一個。我仍然建議您將它們設為不可變的,否則您需要為每個監聽器建立一個例項。

如果它們有一些差異,您可以建立一個基類監聽器並繼承到幾個子類——就像您對任何面向物件的程式碼一樣。沒有理由僅僅因為您幾乎總是將它們視為匿名類而對監聽器進行不同的處理。

最後,如果它們非常不同,只需建立 2 或 3 個不同的類。您可以將任何唯一的匿名內部類保留下來,但是如果監聽器中的程式碼相同或非常相似,請嘗試將它們合併。

函式/方法大小

[編輯 | 編輯原始碼]

嘗試將函式/方法限制在一個螢幕內。您會經常失敗,但這是一個很好的目標。如果看到一個函式長度超過 2 到 3 個螢幕,您應該開始感到很不舒服。

處理一堆小函式比處理一個大函式要容易得多。

請不要透過級聯來實現這一點。僅僅因為函式太長而將其在中間斷開,這更糟糕。

正如我在註釋部分所說,好的面向物件程式碼看起來像是沒有做任何事情——任何地方。我認為我從未見過 Java 類中的方法比一個螢幕更長(刪除註釋後),而且絕大多數只有 1-3 行程式碼。我剛剛掃描了 Hashtable。沒有一個方法超過一個螢幕(比如 25 行左右),大多數更小——所有方法都非常小且專注於解決一個問題。

每個方法或函式都應該只做一件事。這種功能分解使管理變得更容易,並且可以提供在計劃過程中可能沒有意識到的抽象級別。一般來說,如果程式碼執行超過 40 行,您可能在一個步驟中做了太多的事情。如果您覺得該功能不應該公開給其他開發人員,請將該方法設為私有。

公共變數/變數範圍

[編輯 | 編輯原始碼]

我已經得出結論,每個變數都應該是私有的。始終如此。有時我會實現一個簡單的類——矩形甚至“Pair”,並將變數設為公共的,這樣我就不必編寫 setter/getter。但我最終總是後悔這樣做。

即使受保護的變數也很煩人。如果您必須這樣做,請編寫一個簡單的受保護的 getter。(getter 和 setter 也可能是一個壞主意——在 Google 上搜索“Getter 和 Setter 很糟糕”以獲取一篇很棒的文章。)

傳遞一個可變物件就像傳遞一個全域性變數一樣糟糕。對可變物件要非常小心(大多數 Java 物件像 String 都是不可變的,因此更安全)

進一步閱讀

[編輯 | 編輯原始碼]
  • 版本控制 華夏公益教科書討論了配置管理
  • 計算機程式設計 華夏公益教科書討論了各種程式語言
  • "改進開發者大學" 討論了我們希望在學校學到的工具:版本控制、錯誤跟蹤、夜間構建、溝通技巧、時間估計和管理、矛盾的現實。
華夏公益教科書