1 頁 (共 1 頁)

PHP 程式設計標準

發表於 : 2005-05-30 15:02:49
yehlu
http://twpug.net/docs/php_coding_standard.html

PHP 程式設計標準
最後更新: 2003-02-17
PHP 程式設計標準是經由Todd Hoff授權,基於他的C++ 程式設計標準 改寫成PHP的版本
本文件作者為 Fredrik Kristiansen / DB Medialab, Oslo 2000-2003.

使用這份文件時. 您可以任意的在自己的電腦上保留本文件的備份,這也是我們製作這份文件的目的!如果您發現任何錯誤或是需要調整的地方,請 e-mail 給我 您建議的內容,這樣一來我才能夠將它加入本文件的最新版本中

目錄

* 介紹
o 標準化的重要性
o 解釋
o 認同觀點
* 命名規則
o 合適的命名
o 縮寫詞不要全部使用大寫字母
o 物件(Class)命名
o 物件庫(Class Library)命名
o 方法(Method)命名
o 物件屬性(Class Attribute)命名
o 方法參數(Method Argument)命名
o 變數(Variable)命名
o 陣列元件(Array Element)
o 參照變數(Reference Variables)與函數傳回參照( Functions Returning References)
o 全域變數(Global Variables)
o 定義(Define)命名與全域常數( Global Constants)
o 靜態變數
o 函數(Function)命名
* 文件規則
o 註解中的註解
o 註解應該像是說個故事
o 寫下為什麼這麼做
o 使用標頭( Headers)
o 明確的抓住重點
o 介面與配置文件
o 目錄文件
* 複雜性管理規則
o 階層
o 開啟/關閉原則
* 物件規則
o 不同的存取者風格
o 別在物件架構期做實際的工作
o 輕巧相對於龐大的物件介面
o 短方法
* 處理規則
o 程式碼檢討
o 提早且不是經常建立原始碼控制系統
o 提早且不是經常建立錯誤追蹤系統
o 遵守責任區分
* 格式化
o 大括弧 {} 規則
o 縮排/跳位/空格 規則
o 小括弧()的關鍵字與函數規則
o If Then Else 格式
o switch 格式
o continue,break 與?:的使用時機
o 一行一個說明
o 聲明區塊的對齊
* 伺服器設定
o HTTP_*_VARS
o PHP檔案副檔名
* 其他
o PHP程式碼標籤
o 避免使用不可思議的數字
o 錯誤返回檢測規則
o 如果測試值不為零則不要使用預設值
o 自相矛盾的布林類型
o 避免嵌入式工作
o 重複使用您以及其他人努力的成果
o 使用 if (0) 來註解程式碼區塊

介紹
標準化的重要性
如果在某些層面的標準造成了每個人的困擾,標準化可以讓他們有個共同的遊戲規則。
這份文件歷經了許多專案、公司的驗證,正確的說法應該是經過了數週的爭執。
這份文件避免了特殊的個人風格而且可以隨時視情況調整。
正確的觀點
當一個專案嘗試著遵守公用的標準時,會有以下好處:

* 程式設計人員可以瞭解任何程式碼,弄清楚程式的狀況
* 新人可以很快的進入狀況
* 防止新接觸php的人基於節省時間創造出一套自己的風格終其一生
* 防止新接觸php的人一次次的犯同樣的錯誤
* 在一致的環境下,人們可以減少犯錯的機會
* 程式設計人員有了共同的敵人 :-)

錯誤的觀點
現在輪到錯誤的觀點了:

* 標準通常很愚蠢,因為它是不懂PHP的人所制定的
* 標準通常很愚蠢,因為它跟我做的不一樣
* 標準降低了創造力
* 標準在長期互相合作的人群中是沒有必要的
* 標準強調了太多結構
* 總之人們忽視標準

討論
從許多專案的經驗中得知,採用程式設計標準可以讓專案進行的順利些。
不過標準是成功的必要條件嗎?當然不是!
但他們是有幫助的,我們需要任何可以取得的幫助!
老實說,大部分對於特定標準的爭執多來自於自負不已的人。
在合理的標準中很少有所謂的技術缺陷,只是習慣的問題罷了。
所以腦子活一點,控制那樣的自負思想,並且記得任何專案都是基於一個團隊的努力。

解釋
慣例
在本文檔中使用“要”字所指的是使用本規範的所有專案需要遵守規定的標準。

使用“應該”一詞的作用是引導個別專案標準的組成,請依照詞義適當的包含、排除或組合需求。

“也許”一詞相近於“應該”,通常指的是次要的需求。

標準的執行
首先應該在開發小組的內部找出所有的最重要的元素,也許標準對您的狀況還不夠恰當。它可能已經概括了重要的問題,也可能還有人對其中的某些問題表示強烈的反對。 :-)

無論如何,只要最後順利的話,人們將成熟的明白這個標準是合理的,然後其他的程式設計人員也會發現它的合理性,並覺得帶著一些疑問去遵循這個標準是值得的。

如果沒有配合的意願,可以把這個標準改為要求,所有的程式碼都必須基於這個編準做檢驗。

如果沒有經過檢驗,唯一的解決方案會困擾不斷。

認同觀點

1. 不可能
2. 也許可能,但是相當的薄弱與無趣
3. 這是事實,我也已經告訴過你了
4. 這是我先想到的
5. 本來就應該這樣

如果您帶著否定的成見而來看待事物的話,請您保持開放的思想。
你仍可以做出它是廢話的結論,但是做 出結論的方法就是你必須要能夠接受不同的思想。請您給自己一點時間去做到它。
命名規則
合適的命名

命名是程式規劃的核心。古人相信只要知道一個人真正的名字就會獲得淩駕於那個人之上不可思議的力量。
只要你為事物想到正確的名字,將會為你自己以及後來的人帶來比程式碼更強的力量。別笑!

一個名稱是事物在它所處的生態環境中一個長久而深遠的結果,只有了解系統的程式設計人員知道如何建立"適合"這個系統的名稱。如果名稱恰當,許多事情就自然而然結合在一起,關係因此明確,意義因此容易傳達,可以預期一般人心中所推論的結果。

如果您發現自己所有的命名都是東西與命令,您也許需要重新檢視自己的設計。

物件名稱

* 命名前記得確定它的用途,如果您無法想出它的用途,這表示您在這個設計中考慮的還不夠
* 名稱如果使用超過三個英文單字以上的組合字,這表示在您的系統中使用了令人困惑的變數元件。請重新檢視您的設計,試著用一個CRC連線來確認您的物件是否比它們應該有的可信度更高。
* 避免被誘導為物件帶來包含來源物件名稱的命名,物件應該是獨立的,不管它來自於哪裡。後置字元(Suffixes)在這種情況是非常有用的。例如,如果您的系統使用了 agents 這個詞,能夠換成像是 DownloadAgent 之類的詞將會傳達更多確定的資訊。

方法和函數命名

* 通常每個方法與函數都會執行一個動作,所以名稱應該清楚的表達它會做什麼:用 CheckForErrors() 取代 ErrorCheck(), DumpDataToFile() 取代 DataFile()。這也會讓函數與資料物件更容易辨別。
* 後置字元在下面情況是很好用的:
o Max - 用來表示某件事物可以使用的最大值
o Cnt - 用來計算數值的變數目前的數字
o Key - 關鍵值

例如: RetryMax表示最大重試次數, RetryCnt 表示目前重試次數

* 前置字元在下面情況是很好用的:
o Is - 詢問一個關於某事物的問題,這樣一來其他人看到Is 就會知道這是個問題
o Get - 取得一個數值
o Set - 設定一個數值

例如: IsHitRetryLimit.

縮寫詞不要全部使用大寫字母

* 當遇到使用全部大寫的英文單字比使用大小寫混用的單字更能夠表達意義時,別理它!

使用: GetHtmlStatistic.
而不是: GetHTMLStatistic.

解釋

* 當名稱中包含縮寫字時人們似乎有著非常不一樣的直覺,最好只選定一個策略來讓名稱完全在人們的預料之中。
* 例如 NetworkABCKey ,要分辨C是來自於 ABC 而且 K 是來自於 key 容易造成困擾。有些人不在意,而其他人則非常討厭這麼命名,所以您將會發現在不同的程式碼中有著不同的規則,您將會因此永遠不知道如何稱呼某個事物。

範例

class FluidOz // 不要用 FluidOZ
class GetHtmlStatistic // 不要用 GetHTMLStatistic

物件(Class)命名

* 使用首字大寫的單字來區隔組合字,單字的其他字母則為小寫
* 名稱的第一個字是大寫
* 不要使用底線 ('_')

解釋

* 在所有不同的命名策略中,許多人發現這樣子可以讓大部分人妥協

範例

class NameOneTwo
class Name

物件庫(Class Library)命名

* 現在名稱空間變成廣為使用的工具,名稱空間用來避免物件在不同的廠商與團體間發生名稱衝突的情形
* 當您未使用名稱空間時,通常在物件名稱前會加上唯一的前置字元,兩個字元就夠了,更長的也可

範例
John Johnson的完整資料結構庫可以使用前置字元JJ ,所以物件變成:

class JjLinkList
{
}

方法(Method)命名

* 使用與物件一樣的規則

解釋

* 在所有不同的命名策略中,許多人發現這樣子可以讓大部分人妥協

範例

class NameOneTwo
{
function DoIt() {};
function HandleError() {};
}

物件屬性(Class Attribute)命名

* 物件成員屬性名稱應該以'm'開頭
* 在'm'之後使用與物件一樣的命名規則
* 'm' 通常出現在任何前置字元之前,像是代表參照的 'r'

解釋

* 開頭加個 'm' 可以避免與方法的名稱衝突,方法與屬性的名稱經常很相近,特別是對於存取者

範例

class NameOneTwo
{
function VarAbc() {};
function ErrorNumber() {};
var $mVarAbc;
var $mErrorNumber;
var $mrName;
}

方法參數(Method Argument)命名

* 第一個字元必須用小寫
* 在第一個字元後的所有單字應該像物件名稱一樣使用首字大寫

解釋

* 您可以輕易的辨識出哪個變數在變數之間傳遞

範例

class NameOneTwo
{
function StartYourEngines(&$someEngine, &$anotherEngine) {
$this->mSomeEngine = $someEngine;
$this->mAnotherEngine = $anotherEngine;
}

var $mSomeEngine;
var $mAnotherEngine;
}

變數(Variable)命名

* 全部使用小寫字母
* 使用底線 '_' 區隔不同的單字

解釋

* 這樣一來程式碼中的變數將清楚明瞭
* 所有的變數看起來都不一樣且在程式碼中可以辨識

範例

function HandleError($errorNumber)
{
$error = new OsError;
$time_of_error = $error->GetTimeOfError();
$error_processor = $error->GetErrorProcessor();
}

陣列元件(Array Element)

陣列元件名稱依循變數的規則

* 使用底線 '_' 區隔不同的單字
* 不要使用減號 '-' 做為單字的區隔

解釋

* 如果使用減號 '-' 做為單字區隔,程式會產生使用跳脫字元的警告訊息

範例

$myarr['foo_bar'] = 'Hello';
print "$myarr[foo_bar] world"; // 將會輸出: Hello world
$myarr['foo-bar'] = 'Hello';
print "$myarr[foo-bar] world"; // 警告訊息

單引號或雙引號

* 使用單引號或雙引號來存取陣列的元件
* 不要使用包含跳脫字元的引號

解釋

* 除了神奇引號外,如果陣列沒有使用引號,一些 PHP 的設定會輸出警告訊息

範例

$myarr['foo_bar'] = 'Hello';
$element_name = 'foo_bar';
print "$myarr[foo_bar] world"; // 將會輸出: Hello world
print "$myarr[$element_name] world"; // 將會輸出: Hello world
print "$myarr['$element_name'] world"; // 語法錯誤
print "$myarr["$element_name"] world"; // 語法錯誤

參照變數(Reference Variables)與函數傳回參照( Functions Returning References)

* 參照應該以字元'r'開頭.

解釋

* 這樣一來在變數種類之中容易區分
* 它可以確定哪個方法返回可更改物件,哪個方法返回不可更改物件

範例

class Test
{
var $mrStatus;
function DoSomething(&$rStatus) {};
function &rStatus() {};
}

全域變數(Global Variables)

* 全域變數應該使用前置字元 'g'

解釋

* 知道變數的作用範圍是很重要的

範例

global $gLog;
global &$grLog;

定義(Define)命名與全域常數( Global Constants)

* 全域常數應該使用全部大寫的組合字,並且用底線 '_' 區隔

解釋
全域常數一直以來都是用這個方式命名,您必須要小心避免與系統預設常數衝突
範例


define("A_GLOBAL_CONSTANT", "Hello world!");

靜態變數

* 靜態變數也許可以用前置字元 's'

解釋

* 知道變數的作用範圍是很重要的

範例

function test()
{
static $msStatus = 0;
}

函數(Function)命名

* 在C GNU大會上討論的結果,PHP的函數命名全部使用小寫字母且以底線 '_' 做為區隔

解釋

* 這樣一來函數就會跟物件相關名稱有很大的差異

範例

function some_bloody_function()
{
}

錯誤返回檢測規則

* 檢查所有系統呼叫傳回的錯誤訊息,除非您要忽略錯誤。
* 每個系統錯誤訊息都包含系統錯誤文字

大括弧 {} 規則
在三種主要的大括弧配置策略中,有兩種是比較被接受的,而下面的第一種通常是比較多人採用的:

* 將開始的大括弧放在關鍵字的下一行,而結束的大括弧放在最後且獨立的一行中:

if ($condition) while ($condition)
{ {
... ...
} }

* 傳統的UNIX程式碼會把開始的大括弧與關鍵字放在同一行,而結束的大括弧放在最後且獨立的一行中:

if ($condition) { while ($condition) {
... ...
} }

解釋

* 雖然兩種格式都可以被接受,但是許多人還是覺得第一種比較舒服;至於為什麼,那就是心理學家研究的範圍了

大家寧可選擇第一種格式的理由比心理學家還多,如果您使用的文字編輯器(例如 VI)支援大括弧的對稱顯示,第一種會比較好。理由是,當您想知道一個大區塊中程式碼結束的位置,您可以移到開始的括弧點一下,編輯器會自動找到結束的括弧。例如:

if ($very_long_condition && $second_very_long_condition)
{
...
}
else if (...)
{
...
}

在區塊間移動您只需要在開始的括弧點一下,您不需要捲動畫面就可以讓游標很快的在區塊間來回。

縮排/跳位/空格 規則

* 在每一層用四個空白當縮排
* 不要使用跳位鍵( tabs ),使用空白;大部分的編輯器都可以將跳位轉換為空白
* 使用剛好的縮排即可,不要濫用。沒有明確的規則限制最多可以使用幾層縮排,不過如果縮排的層次大於4或5層,您也許需要將程式碼作適當的切割

解釋

* 當其他人使用了不同的跳位鍵設定,就會因此無法(難以)閱讀或列印,因此空白會比跳位鍵好
* 大部分的PHP應用程式使用4個空白
* 大部分的編輯器預設也是4個空白
* 許多人嘗試要去限制最多可以使用的縮排階層數,但是通常無法落實。我們相信程式設計人員會聰明的選擇巢狀程式碼的深度

範例

function func()
{
if (something bad)
{
if (another thing bad)
{
while (more input)
{
}
}
}
}

小括弧()的關鍵字與函數規則

* 不要將小括弧放在關鍵字的旁邊,記得加個空白
* 將小括弧放在函數名稱的旁邊
* 非必要時別在傳回值放上小括弧

解釋

* 關鍵字不是函數,如果將小括弧放在關鍵字的旁邊(不加空白),關鍵字跟函數會容易造成渾淆

範例

if (condition)
{
}

while (condition)
{
}

strcmp($s, $s1);

return 1;

別在物件架構期做實際的工作
別在物件的建構子中執行實際的工作,建構子應該只有拿來組態變數且/或執行不能夠發生錯誤的工作

例如在完成組態的物件中建立一個方法 Open() 時,Open() 應該在物件完成組態後才去呼叫它
解釋

* 建構子無法傳回錯誤訊息

範例

class Device
{
function Device() { /* initialize and other stuff */ }
function Open() { return FAIL; }
};

$dev = new Device;
if (FAIL == $dev->Open()) exit(1);

讓函數重複執行
函數不應該保留靜態變數,以避免函數被重覆執行

If Then Else 格式
配置
這是任由程式設計人員決定的,不同的括弧配置看起來只會有些許的差異;通常會像這樣:

if (condition) // Comment
{
}
else if (condition) // Comment
{
}
else // Comment
{
}

如果您的程式碼有else if ,加個 else 的區塊來找到沒有處理過的情況,或許可以在此放個記錄,即使它不需要任何矯正的動作

條件格式
盡可能將常數放在運算子左邊,例如:

if ( 6 == $errorNum ) ...

其中一個理由是當您少了一個等號時系統會產生錯誤訊息;第二個理由是當您希望尋找變數中放了什麼數值時,您可以在開始的地方找到,而不是在結束的地方。這需要一點時間適應,但是這真的很有幫助!

switch 格式

* 如果一個條件的程式會同時執行下一個條件的程式(注意看範例的第一個case沒有break),記得加個備註
* default 不管怎麼樣都應該保留,如果不應該有預設值記得在裡面放入錯誤訊息
* 如果您需要建立變數,將程式碼放在區塊中

範例

switch (...)
{
case 1:
...
// FALL THROUGH

case 2:
{
$v = get_week_number();
...
}
break;

default:
}

continue,break 與?:的使用時機
Continue 與 Break
Continue 與 break 就像是 gotos 一樣,所以放在這邊

Continue 與 break 跟 goto 一樣盡可能不要去用它

使用 continue 有兩個比較主要的問題:

* 它也許會略過測試的條件
* 它也許會略過增加或減少的程式碼

下面這個例子中兩個問題都會發生:

while (TRUE)
{
...
// A lot of code
...
if (/* some condition */) {
continue;
}
...
// A lot of code
...
if ( $i++ > STOP_VALUE) break;
}

注意:要讓程式設計人員無法輕易的找出問題 ,"很多的程式碼"是必要的

在上面的例子中,我們可以說:在同一個迴圈中同時使用 continue 與 break 必然會是個災難

?:
這會發生問題的原因在於,人們總是喜歡在? 跟 : 之間塞進大量程式碼;下面有幾個清楚的規則可以遵循:

* 在條件的敘述中使用小括弧,藉此與其他程式碼產生區隔
* 測試的動作最好是單純的函數
* 不要把 then 與 else 的描述放在同一行,儘管它是可以放在一起

範例

(condition) ? funct1() : func2();

or

(condition)
? long statement
: another long statement;

定義區塊的對齊

* 定義的區塊應該要對齊

解釋

* 清楚
* 相近的變數組態區塊應該做同樣的對齊
* 符號 "&" 應該放在靠近變數類型的地方,而不是名稱

範例

var $mDate
var& $mrDate
var& $mrName
var $mName

$mDate = 0;
$mrDate = NULL;
$mrName = 0;
$mName = NULL;

一行一個描述
即使描述之間是相當的接近,還是應該將它分成一行一個描述

短方法

* 方法的程式碼應該限制在一個頁面(畫面)可以看到的數量

解釋

* 因為每個方法代表著完成一個目標所需要的技術
* 大部分沒有使用到的參數最後都會變成 false
* 實際的函數呼叫會比沒有還慢,但是必須要想清楚再決定 (是否要這麼早將程式碼最佳化)

放一個空值( Null )的描述
即使迴圈不需要其他描述,在 for 或 while 的描述中記得放上一個空值( Null )的內容,這樣才能夠清楚的知道程式是刻意這樣子做而不是忘記輸入

while ($dest++ = $src++)
; // VOID

如果測試值不為零則不要做為 if 的預設值
不要將不為零的測試值當預設值,例如:

if (FAIL != f())

會比下面這樣好:

if (f())

即使 FAIL 也等於數值零,會讓PHP判斷為錯誤( false )。如果其他人決定要將錯誤的回傳值定義為 -1 而不是 0 時,一個明確的測試值會很有幫助。又即使比較的數值一定不會改變,明確的比較運算還是必要的!例如 if (!($bufsize % strlen($str))) 應該改寫為 if (0 == ($bufsize % strlen($str))) 來反應測試值的自然數(不是布林數)。經常發生一個錯誤是使用 strcmp 來測試字串是否相等,結果就永遠不會是預設值。

不為零的測試值經常被當做 if 的預設值,而這樣子其他函數或程式就會受到下面這樣的限制:

* 回傳值為 0 才是 false,其餘免談
* 這樣子對於回傳值為 true 的情況太過廣泛,等於呼叫一個 IsValid(), 而非 CheckValid()

自相矛盾的布林類型

不要用1 (TRUE, YES, etc.)來檢查布林數值,將它改為不等於0 (FALSE, NO, etc.)。因為大部分的函數保證會在 false 的情況時傳回 0 ,但是true 的狀況會傳回任何不為零的數值,所以...

if (TRUE == func()) { ...

應該要改寫為

if (FALSE != func()) { ...

避免嵌入式工作
嵌入式工作可以節省時間與空間,但是某些人認為這樣子除了讓程式碼變的龐大且難以閱讀外沒有更好的優點。

while ($a != ($c = getchar()))
{
process the character
}

++ 與 -- 兩個運算子是使用指派描述來完成運算,所以在很多情況下執行函式會有些副作用;使用嵌入式指派描述也許會增加執行效能,但是你必須同時考慮加速與不容易維護兩種結果,例如:

$a = $b + $c;
$d = $a + $r;

不應該寫成

$d = ($a = $b + $c) + $r;

即使後者可以節省一個週期,在長時間的執行中,加速器的成熟會讓兩者的差異會越來越小;不過後者在維護上會較困難

重複使用您以及其他人努力的成果
如果沒有適當的基礎架構,在不同的專案之間重複使用程式碼幾乎是不可能的;而物件通常也只符合所需的服務,不同的專案有著不同的服務環境,使得重複使用物件相當困難。

發展一個基礎架構可以讓設計的工作輕鬆許多,不管怎麼樣,在還沒開始之前,下面有幾個技巧可以用來鼓勵重複使用程式碼:
別擔心函式庫太小
重複使用程式碼最大的敵人就是人們不習慣將函式庫脫離他的程式碼,一個可以重複使用的物件也許藏在一個程式的資料夾中而且不易與人分享,因為程式設計人員不會將物件分解或是將物件放入函式庫中

其中一個理由是人們不喜歡製作小的函式庫,總覺得這樣怪怪的;別鬧了,電腦不會在乎你使用了多少函式庫

如果你有可以重複使用的程式碼而且無法放在既有的函式庫,麻煩就建立一個新的吧!如果人們真的去思考重複使用的好處,這個函式庫不會一直這麼小的!

如果你擔心在函式庫異動或新增的時候必須去更新 makefiles ,那就不要在 makefiles 中引入(include)函式庫,試著去引入想法或是服務 。 基礎層級的 makefiles 定義了服務是由一堆函式庫所組成,更高層級的 makefiles 指定它們需要的服務,當一個服務的函式庫有了異動,你只需要去修改較低層級的 makefiles 。

維持一個資料庫
大部分的公司並不知道他們到底有哪些程式碼,而且大部分的程式設計人員仍然不會告訴其他人他完成了什麼或是詢問其他人什麼已經做好了。而解決的方式就是去維護一個程式資源的資料庫。

在理想的狀況下,程式設計人員可以從網頁瀏覽或搜尋包裝好的函式庫清單,並且取得他們需要的部份。如果你能建立這樣一個讓程式設計人員自願維護的系統,那就太棒了;甚至如果能夠記錄重複使用的狀況更好。

另外一個方法就是自動從程式碼中產生一個資料庫,這是基於使用通用的物件、函數、函式庫與子系統標頭,這樣子同時可以完成說明頁面與資料庫。

註解中的註解
註解應該像是說個故事
讓你的註解像是描述這個系統的一個故事,預期你的註解會被自動程式從程式碼中取出然後格式化成說明頁面;而物件的註解就是這個故事的一部分,函式註解、函式參數與函式的配置則是故事的另外一個部份。所有的內容應該編織在一起,並且在另一個時間點告訴某人你做了些什麼以及為什麼這麼做
寫下為什麼這麼做
註解應該說明你的決定,在任何一個你必須做出決定的地方留下註解說明你做了什麼決定以及為什麼,考古學家會從裡面找到最有用的資訊
使用標頭( Headers)
使用像是 ccdoc 的文件抽離系統,文件中的其他部分說明如何使用 ccdoc 來完成一個物件與函式的文件。

這些標頭使用同樣的規則結構化,這樣一來它們可以被分析與分離。他們不像是一般的標頭,所以花點時間去完成填寫。如果你一次作對,你就不需要另外製作文件了!

註解配置
專案的每個部份都該有個特別的註解配置方式
明確的抓住重點
明確的對以下事情加以註解:在正常的控制流程之外所發生的變數變化; 在維護時很可能會被弄壞的程式碼。使用內嵌關鍵字來指出程式要點與潛在問題。 想像有一個自動程式,它會剖析您的註解以尋找關鍵字,然後取出關鍵字所指的 內容,再製作出一份報告,好讓大家在需要解決的地方予以特別處理。

搜尋關鍵字﹝Gotcha Keywords﹞

* :TODO: 主題
表示這裡還有工作尚待完成,不要忘記了。
* :BUG: [bugid] 主題
表示這裡有一個已知的臭蟲,並加以解釋,或許再給一個臭蟲編號。
* :KLUDGE:
當您很笨拙的完成某件事情時,就明白的說出來,並說明您下次想採取 怎樣不同的方法去做,如果您還有時間的話。
* :TRICKY:
告訴別人以下的程式碼是非常有技巧的,請不要不加思索的修改它。
* :WARNING:
要注意某件事情。
* :PARSER:
偶爾您必須設法解決程式語言剖析器所產生的問題。請加以註明。這個剖析器的 問題終會被修正。
* :ATTRIBUTE: value
這是個一般格式,說明該註解所內含的屬性。您可以創造您自己的屬性,它們 也會被析取出來。

搜尋格式﹝Gotcha Formatting﹞

* 將搜尋關鍵字放在註解的最前面,亦即第一個符號。
* 註解也許會包含好幾行,然而第一行必須是獨立而有意義的簡要說明。
* 作者的名字與註解的日期也應該寫在註解裡面。這兩項資訊雖然也可以 在原始碼的倉儲裝置中找到,但是這或許需要一段時間才能查明這段註解 到底是誰、在什麼時後所加上去的。通常這會浪費許多時間去搜尋。 將日期資訊寫在註解裡面,可以讓其他程式設計師在做決定時有所依據。 將作者資訊寫在註解裡面,可以讓我們知道應該找誰問問題。

範例

// :TODO: 年月日 960810: 執行效率問題
// 其實我們應該在這裡使用雜湊表,但是現在先使用線性搜尋。

// :KLUDGE: 年月日 960810: 不安全的強制轉型
// 這裡我們需要作一個強制轉型,以回復原本的衍生型別。或許我們應該
// 使用一個 virtual method、或是使用 template。


相關資訊
請參考介面文件與實作文件來了解文件格式的細節。

介面文件與實作文件
文件主要是為兩種人製作的:

* Class 使用者。
* Class 實作者。

只要在撰寫程式時事先考慮到文件的製作,我們就可以從原始程式碼中直接 取出上述的兩種文件。
Class 使用者

Class 使用者需要類別介面的資訊,如果標頭檔的結構正確,這些介面資訊可以直接 從標頭檔中取得。當您在檔案表頭的註解區塊中,為某個 class 撰寫註解時,要記得 使用該 class 的程式設計師只需要知道如何去使用該 class。不要在註解中深入討論 實作上的演算法細節,除非這是該 class 的使用者應該要知道的。把標頭檔中的註解 當作是以後要加入說明書的一部份。
Class 實作者

Class 實作者需要知道該 class 如何實作的深入細節。這些資訊可以在在原始碼檔案中 的注解找到。不要擔心介面的問題。原始碼檔案的表頭註解區塊中,應該涵蓋演算法議題, 與其他設計上的決定。在 method 實作的註解區塊裡,應該有更詳細的討論。

目錄文件
每個目錄裡面都應該一個 README 檔案,其內容包括:

* 建立這個目錄的目的,以及這個目錄裡面包含些什麼。
* 檔案列表,每個檔案都應該有一行說明。這行說明通常是取自該檔案表頭的 NAME 屬性。
* 建置與安裝的指導。
* 將大家引導到相關的資源:
o 原始碼的目錄。
o 線上文件。
o 紙張文件。
o 設計文件。
* 其他有幫助的東西。

假設有某個專案,每位開發成員都離開了,六個月後,有新人進來。這位孤獨害怕的 探索者,就可以藉著遍歷原始碼的目錄樹,閱讀每個 README 檔案、Makefiles、與 原始碼檔案的表頭,來拼湊出該專案的面貌。

開放/封閉原則 ﹝Open/Closed Principle﹞
Software entities should be open for extension, but closed for modification. (Bertrand Meyer)
開放/封閉原則的意思是,一個 class 在擴充方面應該是開放的,而在修改方面是封閉的:

* 所謂的開放,意即我們可以對一個 class 加以擴充。
* 所謂的封閉,意即我們只能對 class 予以擴充,而不能加以修改。其想法是,一旦某個 class 已經被證明是可用的,而已經進入程式碼檢討、單元測試、與其他品管程序,您就不可以 對它進行修改,而只能擴充它。

開放/封閉原則所高唱的是穩定性。擴充一個系統的方法,應該是加入新的程式碼, 而不是對已經可以使用的程式碼加以修改。程式設計師通常不是很喜歡去修改舊的 程式碼,因為它們沒有問題!這個原則只是給您一個學術上的依據,讓您可以安心 :-)

在實際上,開放/封閉原則就是要我們好好利用抽象化﹝abstraction﹞與多型 ﹝polymorphism﹞這兩個老朋友。使用抽象化來釐出通用的過程與想法。 使用繼承來建立出衍生類別必須遵守的程式介面。

伺服器設定
本節包含某些 PHP/Apache 組態的指導原則。

HTTP_*_VARS
HTTP_*_VARS可能是開啟或關閉的。當開啟時,所有變數都必須以$HTTP_*_VARS[key] 的方式來存取。當關閉時,所有變數則可以直接使用 key 的名稱來存取。

* 使用HTTP_*_VARS來存取變數。
* 在PHP組態中,將HTTP_*_VARS設為開啟。

解釋

* 每個PHP組態都會有HTTP_*_VARS。
* 使用HTTP_*_VARS來存取變數不會與程式碼中的變數名稱相抵觸。
* 使用者無法以給值的方式來更改變數的值。

PHP檔案副檔名
PHP檔案有許多不同的副檔名﹝.html,.php,.php3,.php4,.phtml, .inc,.class...﹞。

* 永遠使用 .php 這個副檔名。
* 永遠使用 .php 這個副檔名來命名您的類別與函式庫檔案。

解釋

* 使用 .php 可以讓我們對其它不以 .php 做副檔名的檔案開啟快取。
* 使用 .inc 與 .class 可能會產生安全性的問題。在大多數伺服器的設定中, 這些副檔名並不是可執行的。因此,當遠端的使用者存取這些檔案時,這些檔案 不會被送給剖析器去執行,而是原原本本的顯示在瀏覽器中。

其他
本節要說明哪些是可以做的、哪些是不能做的。

* 在需要整數的地方,請不要使用浮點數。拿浮點數當作迴圈計數器,等於是拿一把槍 來射自己的腳。在比較浮點數的大小時,請使用 <= 或 >=,絕對不要用剛好等於 或不等於的比較運算子﹝== 或 !=﹞。

* 不要倚賴自動化的程式碼美化工具。使用良好的程式風格,獲益最大的還是程式設計師自己。 特別是在設計階段早期,使用手寫方式來撰寫演算法或虛擬碼的時候,更需要好的程式風格。 美化程式碼的自動工具只能適用於已撰寫完畢、而且語法正確的程式,它不能使用在還需要 對空白字元與縮排字元加以注意的撰寫階段。謹慎的程式設計師都會對程式細節加以注意, 因此,他們為函式或檔案所編排出的格式會更為清楚﹝換句話說,謹慎的程式設計師不會只 依循語法來編排程式碼,它們會在需要注意的地方,刻意編排出突顯的格式,以便在閱讀時 能一目了然,自動美化工具無法知道程式設計師的心思,它會把用心編排出來的格式破壞殆盡。﹞ 草率的程式設計師應該學習如何去當一位謹慎的程式設計師,而不是倚賴自動美化工具來使 他們的程式碼好讀一些。最後要說的是,自動美化工具並不是很簡單的程式,它必須對原始碼 進行剖析,特別設計這麼一個複雜的程式來美化程式碼,實在是不怎麼值得。程式碼美化工具 的最佳用途,是將程式碼產生器所產生的如野草般雜亂的程式碼予以美化。

* 程式設計師偶爾會在邏輯比較運算子中少打一個 ``='',這也是一個問題。 下面的程式碼是很令人困惑的,而且也常會出錯。

if ($abool= $bbool) { ... }

這位程式設計師真的是要寫一個指定式嗎?很可能是,但是通常不是。解決這種問題的方法, 就是不要這樣寫。將指定式與邏輯比較式分開來寫,不要將兩者混在一起。建議的格式是, 先執行指定式,再執行邏輯比較:

$abool= $bbool;
if ($abool) { ... }

使用 if (0) 來註解程式碼區塊
有時候,您需要將很大的程式碼區塊註解起來,以便測試。最好用的方法 是使用 if(0) 區塊:

function example()
{
一堆程式碼

if (0) {
許多程式碼
}

更多程式碼
}

您不能使用 /**/ 的方式來註解一大塊程式碼,因為註解裡面不能再包含 註解,而且很肯定的,一大塊程式碼裡面一定會包含有註解,不是嗎?
不同的存取函式風格
實作存取函式
存取函式的建立有兩種主要風格。
Get/Set

class X
{
function GetAge() { return $this->mAge; }
function SetAge($age) { $this->mAge = $age; }
var $mAge;
};

個人並不欣賞Get/Set風格。它會讓程式碼裡面充斥了一堆 Get 與 Set。

但是它有一個好處,在處理網路訊息時,我們可以在 Set 函式裡面將本機的 位元組順序﹝byte order﹞轉換成網路的位元組順序。
屬性物件﹝Attributes as Objects﹞

class X
{
function Age() { return $this->mAge; }
function Name() { return $this->mName; }

var $mAge;
var $mName;
}

$x = new X;

// 範例 1
$age = $x->Age();
$r_age = &$x->Age(); // 屬性的參考﹝Reference﹞

// 範例 2
$name = $x->Name();
$r_name = &$x->Name(); // 屬性的參考﹝Reference﹞

從函式名稱來看,屬性物件的函式名稱很簡潔。請儘可能使用這種方式來存取屬性。
階層
階層式設計是減低系統複雜度的主要技術。一個系統應該被分割成數個階層。 某個階層可以使用定義良好的介面來與鄰近階層進行溝通。如果一個階層可以 接觸到非鄰近階層時,就違背了階層設計的原則,稱之為「階層違背」。

「階層違背」的發生,就表示有兩個階層之間的互依關係,不是經由良好定義的介面來產生的。 當其中一個階層有所改變時,程式碼可能會無法運作。我們不希望程式碼無法正常運作,所以 要求每個階層只能與鄰近階層進行溝通。

有時候,為了改善執行效率,我們需要跳過幾個階層。這沒有問題,但是我們 應該要知道自己正在違背階層原則,並應該在文件中加以說明。

程式碼檢討﹝Code Review﹞
如果您能夠對程式碼進行一次正式的檢討會議,我向您脫帽致敬。程式碼檢討是非常有幫助的。 不幸的是,它們總是會轉變成吹毛求疵的會議,無止境的在愚蠢的事情上互相爭論。 它們也總是會佔據許多人的時間,而無法產生預期的效果。

天阿,這個人在質疑程式碼檢討的必要性,他一定不是工程師!

也不盡然,我所要質疑的,只是程式碼檢討的會議形式,以及在延期又混亂 的專案中進行程式碼檢討的效果。

首先,程式碼檢討只能在專案後期舉行,它來得太遲了,無法產生任何幫助。 真正需要檢討的,是需求規格與設計規格。這才是您可以獲得更大成效的地方。

將相關人員集合在一個房間。把他們鎖在裡面。完整的審視程式介面設計,以及需求 規格,直到前者的設計令人滿意,後者的需求也都達成。將所有相關人員集中在一個 房間裡面,可以讓這個過程產生良好的效益,因為問題可以被立即回答,觀點可以被 立即探討。通常,這種會議只要舉行兩三次就很夠了。

如果上述的過程圓滿達成,程式碼撰寫工作就不會有什麼問題。如果您在程式碼檢討 中發現問題,而這個問題是某人花了許多時間與精神不停測試所發現的,您最好回去將 程式碼重新寫過一遍。

您仍然會想要做程式碼檢討,那只要在一旁進行就好了。找幾位您信得過的人, 閱讀可能有問題的程式碼,並將建議直接告訴程式設計師。如此,程式設計師與審查者 就可以討論各種觀點,並將它們實行出來。電子郵件與快速犀利的討論是很好的方式。 這種方式符合檢討的目的,也不會一次用掉六個人的時間。

提早且不是經常建立原始碼控制系統
在專案生命週期中,應該安裝一個共用的建置系統與原始碼控制系統,愈早安裝愈好, 最好是在開始撰寫程式碼之前就安裝完畢。原始碼控制系統是讓專案結構保持完整的 黏著劑。如果程式設計師不能輕易的使用他人的成果,您就永遠無法製造出可以重覆 生產的建置版本,也會浪費大家許多時間。將一個失控的建置環境改建成標準系統, 也是很糟糕的事情。每個專案都有權利去建立它自己的客製環境,這個道理似乎是對的, 但是該客製環境卻總是無法運作得那麼對。

某些要點必須牢記在心:

* 一般來說,像 CVS 這種共享原始碼環境,最適用於極大型專案。

* 如果您要使用 CVS,請使用 參考建置樹﹝reference tree﹞ 的方式。這種方式會將各個建置版本儲存在一個主建置樹﹝master build tree﹞中。 程式設計師只從工作中的建置版本中取出原始碼,而且只取出欠缺的部份, 因為 make 系統會使用該建置版本來找出本機上所沒有的檔案。 使用-I 與 -L旗標讓這個系統在設定上容易一些。 先在本機上搜尋檔案與程式庫,然後再到參考建置﹝reference build﹞中搜尋。 這個方式可以節省磁碟空間與建置時間。

* 磁碟空間愈大愈好。現在的磁碟機又大又便宜,實在沒有理由不去保存數份建置版本。

* 讓簡單的事情更簡單。在文件中說明如何進行下列事項,應該是非常簡單的事情:
o 如何取出模組來建置。
o 如何更改檔案。
o 如何將新的模組加入系統中。
o 如何刪除模組與檔案。
o 如何將所做的更改存入系統中。
o 可以用的函式庫與引入檔有哪些。
o 如何取得建置環境,包含所有的編譯器與其它工具。

製作一個網頁、文件、或是其他類似的東西。不要讓新到的程式設計師 到處要求老手透露一下建置的秘訣。
* 每次存入﹝checkin﹞時都做一次日誌註解是很有用的。這些註解應該每晚蒐集起來, 並傳送給相關單位。

相關資訊
如果您的錢夠多,許多專案發現 Clear Case 是個好系統。也有許多完全可以運作的系統是建立在 GNU 的 make 與 CVS 上面。CVS 是一個建立在 RCS 上面的 freeware 建置系統。它與 RCS 之間的 主要差異是,它為建置中的軟體提供了一個共享檔案模式。

提早且不是經常建立錯誤追蹤系統
愈早習慣使用臭蟲追蹤系統愈好。如果您在專案進度3/4的時候才安裝臭蟲追蹤系統, 沒有人會去使用它。您必須早一點安裝臭蟲追蹤系統,讓大家去使用。

程式設計師通常很不喜歡去追蹤臭蟲,然而一旦使用正確,它會對專案有很大的幫助:

* 問題不會被丟下不管。
* 問題會被自動轉到負責人手中。
* 問題的生命週期會有追蹤記錄,讓大家有足夠的資訊可以互相討論。
* 經理可以依據該系統中的臭蟲數量與類型,來擬定重要的進度表與幕僚決策。
* 組態管理﹝configuration management﹞有希望與修補問題的補丁保持一致。
* QA﹝問答集﹞與技術支援和開發人員之間有一個溝通的媒介。

以上各點,雖然不是什麼迷人的事情,但是它們讓專案有良好紮實的進步。

原始程式碼的版本控制應該連結到臭蟲追蹤系統上面來。當專案進行到釋出軟體前的 程式碼凍結階段時,版本控制系統應該只接受帶有合法臭蟲ID的存入動作﹝checkin﹞。 當程式碼的臭蟲被修正完畢後,該臭蟲的ID也應該包含在checkin的注解中。

遵守責任區分
軟體模組的責任是有所區分的。模組的維護可能是某人的責任,或是大家共同的責任。 請遵守這樣的責任區分。不要去修改不屬於您負責的模組。否則,只會引起 錯誤與不愉快的經驗。

請面對這樣的事實,如果某段程式碼不屬於您維護,就請放棄自己去修改它的念頭。 程式碼的來龍去脈是很複雜的,您認為相當合理的事情,可能到頭來完全是錯誤的。 如果您需要修改某個地方,只要請負責該程式碼的人去修改就好了。或是在您進行 修改之前,先詢問他們是否可以做這樣那樣的改變。如果他們說好,那就可以去改, 否則就請不要動您的編輯器。

每個規則都有例外的情形。如果在清晨三點鐘,您需要做一個修改,以釋出一個 可以使用的版本,那麼您就必須改下去。如果負責人正在休假,而且該模組沒有 指定代理人,您也必須改下去。當您修改別人所撰寫的程式碼時,請試著使用他們 所採用的程式碼風格。

某些特別敏感的程式碼在修改之後會對其他地方造成影響,負責的程式設計師必須在這些程式碼 中加入註解。如果修改某個地方的程式碼,也必須對另一地方的程式碼進行修改,那就 要在註解中加以說明。如果資料格式的改變會與儲存的資料產生衝突、或是與送至遠端電腦的訊息 不一致,也要在註解中加以說明。如果您試著減少記憶體的使用量,或是想達成某種目標, 也請在註解中說明。不會有人聰明到可以猜出您心中的想法。

最不可原諒的是,把整個系統的程式碼全部改過一遍,以符合您個人的程式風格。 請保持禮貌。如果某人沒有按照標準來撰寫程式碼,就請他們修正,或是 請您的經理來要求他們。

如果模組是由大家一起維護的,就要特別小心。堅持不做劇烈的修改,以免產生難以解決的 版本衝突。在程式檔中加入註解,說明這個程式檔的擴增方法,好讓每個人都遵循這個規則。 試著在共用的程式檔中使用相同的程式結構,免得其他人必須摸索著去尋找所要的 程式碼,找到以後也不知道如何去修改它。每次修改完程式碼,立即將新版程式碼 儲存進版本控制軟體中,以避免版本衝突的發生。

另外一提,追蹤臭蟲也是一種必須指定的模組責任。
PHP程式碼標籤
PHP 標籤﹝Tags﹞是用來標示出夾雜在 html 中的 PHP 程式碼。有許多標示方法可以使用。 , , , <% %>,以及。某些方法可能在您的 PHP 設定 中被關掉了。

* 使用

解釋

* 在任何的系統與安裝過程中都是可以使用的

範例

// 會列印出 "Hello world"

// 會列印出 "Hello world"

// 會列印出 "Hello world"

<% print "Hello world"; %> // 會列印出 "Hello world"

// 會列印出 $street 這個變數的值

避免使用不可思議的數字
不可思議的數字﹝magic number﹞,就是光溜溜的出現在原始程式碼中的數字。 它們之所以不可思議,是因為沒有人知道這些數字的意思是什麼,甚至連原始作者 也會在三個月後完全忘記它們的意思。例如:

if (22 == $foo) { start_thermo_nuclear_war(); }
else if (19 == $foo) { refund_lotso_money(); }
else if (16 == $foo) { infinite_loop(); }
else { cry_cause_im_lost(); }

在上面的例子中,22 與 19 的意思是什麼?您又怎麼能夠知道,這些數字曾經被修改過, 或是這些數字根本就是錯誤的?

頻繁的使用不可思議的數字,表示該位程式設計師只不過是業餘的。這樣的程式設計師 可能從未在開發團隊的環境中工作過,否則,如果他們需要去維護程式碼,那他們鐵定不會 這樣做。

請絕對不要在程式碼中寫下不可思議的數字,改用有意義的名稱來代表它們。您應該使用 define()。例如:

define("PRESIDENT_WENT_CRAZY", "22");
define("WE_GOOFED", "19");
define("THEY_DIDNT_PAY", "16");

if (PRESIDENT_WENT_CRAZY == $foo) { start_thermo_nuclear_war(); }
else if (WE_GOOFED == $foo) { refund_lotso_money(); }
else if (THEY_DIDNT_PAY == $foo) { infinite_loop(); }
else { happy_days_i_know_why_im_here(); }

這樣不是好多了嗎?

輕巧相對於龐大的物件介面

一個物件應該有多少method?正確的答案當然是剛剛好,我們稱之為「歌蒂拉克程度」 ﹝Goldilocks level,程度適中的意思﹞。但是「歌蒂拉克程度」是什麼樣子?答案是, 它不存在。您必須依據實際情形來拿捏method的數量,這就是為什麼我們需要程式設計師 :-)

兩種極端的設計是,輕巧類別﹝thin classes﹞與龐大類別﹝thick classes﹞。輕巧類別 是最低限度的類別,它所擁有的method愈少愈好。一般來說,使用者會繼承輕巧類別來衍生 出他們自己的類別,並在衍生類別中加入所需要的method。

輕巧類別或許看起來很「乾淨」,但是實際上並非如此。輕巧類別沒有多大的用途,它的 主要目的只是為了建立起一個型別。正因為輕巧類別沒什麼實際功用,所以在同一個專案 中的每位程式設計師,都會衍生出他們自己的類別,而且他們在衍生類別中所加入的method, 基本上都是一樣的。這就造成了程式碼重複開發的問題,而違背了當初採用物件的方法來 開發程式的目的─讓同一份程式碼可以重覆使用。解決這個問題的方法,就是把新增的method 往上放到基底類別中。如果往上放的method很多,基底類別就會變成龐大類別。

龐大類別有很多method。您想在它裡面加入多少method都可以。這會產生什麼問題嗎?也許不會。 如果要加入的 method 與該類別有直接的關連,那把它們放在該類別裡面就不會有什麼問題。 真正的問題是,有些人會開始懶惰,把有些應該放到別的類別的method,即使與該類別只有一點點關連, 也一併新增到該類別裡面。在這裡,我們需要再次做出正確的判斷。

龐大類別還有其他的問題。愈龐大的類別,愈不容易了解,程式碼之間的互動更不可預期, 因此也更難除錯。而且,如果有一個您從不使用、也從不關心的method被修改了,您還是得 整個類別重新測試,再重新釋出這個類別。

Recent Changes

1. 2004-07-18. Traditional Chinese version done by simula and kiang
2. 2003-02-17. Modified indent rule.
3. 2002-03-04. Some changes in PHP File Extensions section. Only .php extensions is now recommended.
4. 2002-01-23. I've added Array Element.
5. 2001-08-13. The Variable Names example is now compatible with this PHP standard. Added word version of this document submitted by Chris Hubbard.
6. 2001-01-23. Method Argument Names example code fix. Parts of Different Accessor Styles has been deprecated because there was no support in PHP for these.
7. 2000-12-12. HTTP_*_VARS added
8. 2000-12-11. Indentation/Tabs/Space Policy has been changed
PHP Code Tags added
9. 2000-12-05. Method Argument Names has been updated

*由於原始英文網站暫時無法連結,所以文件中有提到原始網站的部分均隱藏,如果有興趣可以透過檢視原始碼取得!