在本系列文章(有關如何在實際情況下開發有效的 PHP 代碼)的第 3 部分中,Amol Hatwar 討論了如何構建最有效的功能型函數,使用這些函數不會犧牲太多性能或可管理性。作者重點闡述了如何編寫可重用函數,並介紹了如何避免與該任務相關的一些最常見問題。 歡迎回來。在本系列文章的第 1 部分中,我討論了一些基本的 PHP 設計規則,並介紹了如何編寫安全、簡單、與平台無關且快速的代碼。在第 2 部分中,我介紹了變量,並討論了它們在 PHP 編碼中的用法 — 好的和壞的實踐。 在本文中,您將了解如何在 PHP 中明智地使用函數。在每一種高級編程語言中,程序員都可以定義函數,PHP 也不例外。唯一的區別在於,您不必擔心函數的返回類型。 深入研究 函數可用於: 將幾行代碼封裝成一條語句。 簡化代碼。 最重要的是,將應用程序作為更小的應用程序相互協調的產物。 對於從編譯語言(如 C/C++)轉到 PHP 的開發人員來說,PHP 的性能級別是令人吃驚的。在使用 CPU 和內存資源方面,用戶定義的函數非常昂貴。這主要是因為 PHP 是解釋型和松散類型的。 包裝與否 有些開發人員僅僅因為不喜歡函數的名稱就把他們使用的每個函數都包裝起來,而另一些開發人員卻根本不喜歡使用包裝。 包裝現有的 PHP 函數而不添加或補充現有的功能,是完全不能接受的。除了會增加大小和執行時間外,這樣的重命名函數有時可能會帶來管理上的惡夢。 代碼中的內聯函數會導致莫名其妙的代碼,甚至是更大的管理災難。這樣做的唯一好處可能就是得到一個更快的代碼。 更明智的方法是,僅在需要多次使用代碼,並且對於您希望實現的任務沒有可用的內置 PHP 函數時才定義函數。您可以選擇重命名或僅當需要時才有限制地使用。 圖 1 中的圖表粗略地顯示了可管理性和速度與使用的函數數量之間的相互關系。(在此我沒標明單位,因為數字取決於個體和團隊的能力;這一關系是重要的可視數據。) 圖 1. 可管理性/速度 Vs. 函數數量 命名函數 正如我在本系列文章的第 2 部分(請參閱參考資料)中提到的,要獲得有效的代碼管理,始終都使用公共的命名約定是必不可少的。 其它兩個需要考慮的實踐是: 選擇的名稱應當能很好地提示函數的功能。 使用表明包或模塊的前綴。 假定您有一個名為 user 的模塊,它包含用戶管理函數,那麼對於檢查用戶當前是否在線的函數而言,諸如 usr_is_online() 和 usrIsOnline() 這樣的函數名稱都是上佳之選。 將上面的名稱與 is_online_checker() 這樣的函數名稱相比較。得到的結論是,使用動詞優於使用名詞,因為函數始終都會做點什麼。 多少參數? 很有可能您將使用已經構造的函數。即使情形並非如此,您可能也希望最大化代碼的使用范圍。要做到這一點,您和其他開發人員應當繼續開發易於使用的函數。沒人喜歡使用那些所帶的參數既晦澀又難於理解的函數,因此請編寫易於使用的函數。 選擇一個能夠說明函數用途的名稱(並減少函數使用的參數數量)是確保易於使用的一個好方法。參數數量的幻數是什麼呢?依我看來,超過三個參數就會使函數難以記憶。使用大量參數的復雜函數幾乎都能被拆分成多個更簡單的函數。 沒人喜歡使用湊合的函數。 編寫優質函數 假定您希望在將 HTML 文檔放到浏覽器之前設置文檔的標題。標題就是
... 標記之間的所有內容。 假定您希望設置 title 和 meta 標記。不使用 setHeader(title, name, value) 函數執行所有工作,而分別使用 setTitle(title) 和 setMeta(name, value) 完成各項工作是一個更佳的解決方案。該方案相互獨立地設置 title 和 meta標記。 進一步考慮,標題可以只包含一個 title 標記,但它可以包含多個 meta 標記。如果需要設置多個 meta 標記,則代碼必須多次調用 setMeta()。在這種情況下,更佳的解決方案是將帶有名稱-值對的兩維數組傳遞給 setMeta(),並且讓函數循環執行該數組 — 同時執行所有操作。 一般來說,象這樣的同時的函數更可取。用函數需要處理的所有數據一次性調用函數,始終優於多次調用函數,並以增量的方式為其提供數據。編寫函數時的主要思想是,盡量減少從其它代碼對其的調用。 據此,setHeader() 解決方案實在不是好方法。顯而易見,我們可以將 setHeader() 重構成 setHeader(title, array),但是也必須考慮到我們失去了相互獨立地設置 title 和 meta 標記的能力。 此外,在實際環境中,標題可能包含多個標記,而不只是 title 和 meta 標記。如果需要添加更多標記,您必須更改 setHeader(),並且改變依賴於它的所有其它代碼。在後一種情形下,只需多編寫一個函數。 下面的等式適用於所有編程語言: 便於記憶的名稱 + 清晰的參數 + 速度和效率 = 在所有編程語言中都適用的優質函數 用分層的方法協調函數 函數很少是獨自存在的。它們與其它函數共同起作用,交換和處理數據以完成任務。編寫可與相同組或模塊中的其它函數良好協作的函數很重要,因為這些函數組或模塊組就是您必須能夠重用的。 讓我們繼續假想的頁面構建示例。這裡,該模塊的職責是用 HTML 構建一個頁面。(現在讓我們先略過細節問題和代碼,因為示例的目的只是為了說明:在提高可重用性要素的同時,如何使函數和函數組方便地相互配合。) 從內置的 PHP 函數開始,您可以構建抽象函數,使用它們創建更多處理基本需求的函數,然後依次使用這些函數構建特定於應用程序的函數。圖 2 可以讓您了解其工作原理。 圖 2. 分層的函數 現在,先在內存中構建頁面,然後將完成的頁面分發給浏覽器。 在內存中構建頁面有兩大好處: 可以用自己的腳本高速緩存已完成的頁面。 如果未能成功構建頁面,可以廢棄完成一半的頁面,並使浏覽器指向出錯頁面。 現在,您的用戶將不會看到頁面中有錯誤消息的報告了。 根據大多數頁面的結構,需要將頁面構建模塊分成執行以下功能的函數: 繪制頂欄 繪制導航欄 顯示內容 添加腳注 還需要執行下述功能的函數: 高速緩存頁面 檢查頁面是否已經被高速緩存 如果頁面已被高速緩存則顯示它 讓我們稱之為頁面構建器(pagebuilder)模塊。 頁面構建器模塊通過查詢數據庫執行其工作。由於該數據庫是 PHP 之外的,所以將使用數據庫抽象模塊,其職責是為 PHP 中各種不同的特定於供應商的數據庫函數提供同類接口。該模塊中的重要函數有:連接數據庫的函數、查詢數據庫的函數以及提供查詢結果的函數。 假定您還希望實現一個站點范圍的搜索引擎。該模塊將負責搜索站點上與某個關鍵字或某組關鍵字相關的文檔,並根據搜索字符串的相關性或出現該字符串次數最多來顯示結果。如果您還希望記錄搜索以便進行審計,該模塊將與數據庫抽象模塊一起使用。 請記住,您將接受來自用戶的輸入。您需要將其顯示在屏幕上,並廢棄那些看上去懷有惡意的內容。這需要另一個模塊,它負責驗證用戶通過表單提交的數據。 至此,您對我正在講述的概念肯定有了大致的了解。必須將最核心的功能分解成邏輯模塊,要執行它們的任務,應用程序必須使用這些模塊提供的函數。 使用這種分層的方法,簡單的頁面構建呈現應用程序可能如圖 3 所示。 圖 3. 分層的頁面構建應用程序 請注意,在本示例中,核心模塊與處理應用程序的模塊之間沒有層次。也就是說,核心模塊可以從下面的抽象模塊或層中聲明的函數調用和聲明函數,但是應用程序代碼可能不能這樣做。如果應用程序代碼中的函數受任何低層函數“污染”或者封裝了任何低層函數,那麼應用程序代碼不應該聲明這些函數。它只能使用低層的函數。這被證實是一個更快的方法。 功能型技術 既然您已經了解了應如何使用和編寫函數,那麼就讓我們研究一些常用的技術。 使用引用 簡單點說,引用就象 C 語言中的指針。唯一的區別在於,在 PHP 中,不需要象在 C 語言中那樣使用 * 運算符來解除引用。您可以將它們看成是變量、數組或對象的別名。無論執行什麼操作,別名都將影響實際的變量。 清單 1 演示了一個示例。 清單 1. 變量引用 當將參數傳遞給函數時,函數接收到參數的副本。只要函數一返回,您對參數所做的任何更改都將丟失。如果您希望直接改變參數,這會是一個問題。清單 2 演示了一個說明該問題的示例。 清單 2. 將參數傳遞給函數時的問題 我們希望直接改變 $myNum;通過將 $myNum 的引用傳遞給 half() 函數可以輕易地完成該工作。但是請記住,這並不是個好實踐。使用您代碼的開發人員必須跟蹤所用的引用。這可能會在不經意間導致錯誤蔓延。它還會影響到的函數的易用性。 更好的實踐是直接在函數聲明中使用引用 — 在我們的例子中,使用 half(&$num) 代替 half($num)。這樣,通過記住引用,您就無須記住要將參數傳遞給函數了。 PHP 處理幕後的一些事情。較新的 PHP 版本(從 4.0 起的後續版本)不贊成在調用時按引用傳遞,並且無論如何都會發出警告。(這裡有一些建議:如果您正在使用針對早期 PHP 版本編寫的代碼,那麼最好更新代碼,而不是通過改變 php.ini 文件來更改 PHP 的行為。) 保留函數調用之間的變量 常常需要維護函數調用之間的變量值。可以使用全局變量,但是變量非常脆弱,並可能被其它函數破壞。我們希望變量對於函數而言是局部變量,並仍然保留其值。 使用 static 關鍵字是一個很好的解決方案。當我希望計算在無法使用調試器的情況下有多少用戶定義的函數被執行時,我常使用這種方法。我只是改變了所有函數(當然是使用自動化的腳本),並在函數體的第一行添加了對執行計數工作的函數的調用。清單 3 描述了該函數。 清單 3. 計數用戶定義的函數 function funcCount() { static $count = 0; return $count++; } 剛好在腳本完成之前通過調用 funcCount() 來收集變