## 關于后端代碼結構統一的約定
## **前言**
在現代軟件工程的廣闊疆域中,代碼質量已牢固確立為衡量項目穩健性、可維護性和團隊協作效能的核心標尺。為鑄就堅實、明晰且易于傳承的代碼寶典,制定一套系統化、高標準的編碼規范與共識顯得尤為迫切。本《代碼結構統一度約定手冊》應運而生,旨在為我司開發團隊鋪設一條規范化的編程實踐之路,引導我們在代碼風格塑造、模塊架構布局、邏輯表述藝術及數據庫藍圖繪制等諸多維度達成一致性與最佳實踐的共鳴。此文檔不僅旨在拋磚引玉,更是誠摯邀請每一位團隊成員積極獻策,共襄代碼美學與工程智慧的盛宴。您的真知灼見,必將點亮我們共筑卓越代碼殿堂的璀璨星光。
## **編程規約**
**命名風格**
強制 文件命名: 文件名稱應遵循“大駝峰”(Upper Camel Case)命名規則。這意味著每個單詞的首字母均大寫,且單詞之間無空格或任何其他分隔符。例如:
```
RequestHepler.php
UserAuthenticationModel.php
DatabaseConnectionConstant.php
```
強制 代碼變量: 在PHP代碼中,變量名稱應采用“小駝峰”(lower camel case)命名法。即首個單詞以小寫字母開始,后續單詞的首字母大寫,其余字母均為小寫。示例:
```
$requestData
$userProfile
$databaseConnection
```
強制 數組鍵名: 數組鍵應當使用全小寫字母,并通過下劃線 \_ 連接單詞,形成所謂的“蛇形命名”(snake\_case)。以下是一些示例:
```
$userInfo = [
'first_name' => 'John',
'last_name' => 'Doe',
'email' => 'john.doe@example.com',
];
$productDetails = [
'product_id' => 123,
'product_name' => 'Example Product',
'price' => 99.99,
];
```
推薦 常量命名:建議采用全大寫命名法,并使用下劃線(\_)進行單詞分隔。
```
const PAGE_SOURCE_LIBRARY = 14;
const PAGE_SOURCE_DISCOVER = 18;
const PAGE_SOURCE_PROFILE = 19;
```
常量文件建議寫入Constant文件中,和表強關聯的常量可以寫入表對應的MODEL文件中(不推薦),RedisKey定義常量文件寫入入RedisKey文件中。
推薦 緩存命名:建議采用“蛇形命名”,變量部分采用:分隔開。前半部分建議和常量名稱保持一致。
```
const ABROAD_GOOGLE_ACCESS_TOKEN = 'abroad_google_access_token:%s';
const ABROAD_GOOGLE_REFRESH_TOKEN = 'abroad_google_refresh_token:%s';
```
強制 杜絕完全不規范的縮寫,避免望文不知義。
反例:AbstractClass“縮寫”命名成 AbsClass;condition“縮寫”命名成 condi,此類隨意縮寫嚴重降低了代碼的可閱讀性
**代碼格式**
強制 方法約定:方法名應該為小駝峰形式,換行格式如下:
```
public function getCreatedAtAttribute(string $value) : string
{
return date('Y-m-d H:i:s', strtotime($value));
}
```
方法外部的大括號均另起一行。
推薦 參數類型聲明:為函數或方法的每個參數明確指定其數據類型。此舉有助于編譯器/解釋器進行類型檢查,提前發現潛在的類型錯誤,同時顯著提升代碼的可讀性與自文檔化特性。
推薦 盡量避免一個方法在不同條件下返回不同類型的值。這種設計容易造成類型推斷困難,增加調用者的使用復雜性,且不利于靜態類型檢查。如有必要,可考慮拆分為多個明確職責的方法。
推薦 為函數或方法聲明其返回值的數據類型。這將進一步強化代碼的契約性,使調用者清楚了解預期的返回結果類型,降低誤解與錯誤使用風險。
推薦 區分數組和MAP的命名,假如一個數組不帶KEY,可以用s\\list結尾,例如 $books, $userList,但是假如數組存在KEY,建議用map結尾,例如 $channelMap。這樣能方便后邊使用的時候判斷,例如存在$books\[$bookId\]的寫法,大概率就是有問題的。假如是一個單獨的對象,如一本書,建議用單數名稱表示,例如 $book或者加上Info,例如 $bookInfo。
強制 大括號的使用約定:在程序中進行結構控制代碼編寫,如if、for、while、switch等結構,如果是大括號內為空,則簡潔地寫成{}即可,不需要換行;如果是非空代碼塊則:
1) 左大括號前不換行。
2) 左大括號后換行。
3) 右大括號前換行。
4) 右大括號后還有 else 等代碼則不換行;表示終止的右大括號后必須換行
強制 左小括號和字符之間不出現空格;同樣,右小括號和字符之間也不出現空格;而左大括號前需要空格。。
反例:if (空格 a == b 空格)
強制 if/for/while/switch/do 等保留字與括號之間都必須加空格。
強制 任何二目、三目運算符的左右兩邊都需要加一個空格。
說明:運算符包括賦值運算符=、邏輯運算符&&、加減乘除符號等。
強制 注釋的雙斜線與注釋內容之間有且僅有一個空格。
<span style="color:green;">**正例:**</span>
```
// 這是示例注釋,請注意在雙斜線之后有一個空格
$a = 1;
```
強制 方法參數在定義和傳入時,多個參數逗號后邊必須加空格。
<span style="color:green;">**正例:**</span>下例中實參的 $recommend,后邊必須要有一個空格。
```
protected function getTopics($recommend, $recommendBooks, $recommendIds): array
{
... 代碼部分省略
}
```
<span style="color:red;">**強制**</span>
IDE 的 text file encoding 設置為 UTF-8; IDE 中文件的換行符使用 Unix 格式,不要使用 Windows 格式。
強制 禁止寫大而全的方法,這樣的功能過于復雜、耦合性高、不易測試、難于維護、可讀性差。為改善這種情況,應遵循“單一職責原則”(SRP)對方法進行拆分重構,將每個獨立的邏輯塊封裝成專門的方法。將方法被拆分為多個職責單一的小方法,每個方法專注于一項具體任務。這樣既提高了代碼的可讀性、可測試性和可維護性,又降低了出錯的概率。
<span style="color:red;">**反例:**</span>
```
public static function handleOrder($orderNum, $grades, $isTest = 0)
{
$order = Order::*selectInfo*([ 'order_num' => $orderNum]);
// 修改訂單狀態
// ... 這里是一堆修改訂單狀態的代碼
// 修改用戶充值狀態
// ... 這里是一堆修改充值狀態的代碼
// 發放充值獎勵
// ... 這里是一堆發放充值獎勵的代碼
// 延長染色時長
// ... 這里是一堆延長染色時長的代碼
// ..其他的代碼
}
```
<span style="color:green;">**推薦**</span>
不同邏輯、不同語義、不同業務的代碼之間插入一個空行分隔開來以提升可讀性。
說明:任何情形,沒有必要插入多個空行進行隔開。也不要每一行都加個空行,就失去了提升的意義。
控制語句
<span style="color:red;">**強制**</span>
在一個 switch 塊內,每個 case 要么通過 continue/break/return 等來終止,要么注釋說明程序將繼續執行到哪一個 case 為止;在一個 switch 塊內,都必須包含一個 default 語句并且放在最后,即使它什么代碼也沒有。
說明:注意 break 是退出 switch 語句塊,而 return 是退出方法體。
<span style="color:red;"> **強制**</span>
在 if/else/for/while/do 語句中必須使用大括號。 說明:即使只有一行代碼,也禁止不采用大括號的編碼方式。
<span style="color:red;"> **反例:**</span>
if (condition) statements;
<span style="color:red;"> **強制 **</span>
在高并發場景中,避免使用”等于”判斷作為中斷或退出的條件。
說明:如果并發控制沒有處理好,容易產生等值判斷被“擊穿”的情況,使用大于或小于的區間判斷條件 來代替。
反例:判斷剩余獎品數量等于 0 時,終止發放獎品,但因為并發處理錯誤導致獎品數量瞬間變成了負數, 這樣的話,活動無法終止。
推薦 當某個方法的代碼總行數超過 10 行時,return / throw 等中斷邏輯的右大括號后均 需要加一個空行。 說明:這樣做邏輯清晰,有利于代碼閱讀時重點關注。
推薦 表達異常的分支時,少用 if-else 方式,這種方式可以改寫成:
```
if ($condition) {
return $obj;
}
// 接著寫 else 的業務邏輯代碼;
```
說明:如果非使用 if()...else if()...else...方式表達邏輯,避免后續代碼維護困難,請勿超過 3 層。 正例:超過 3 層的 if-else 的邏輯判斷代碼可以使用衛語句、策略模式、狀態模式等來實現,其中衛語句 示例如下:
```
function findBoyfriend($man) {
if ($man->isUgly()) {
echo "本姑娘是外貌協會的資深會員\\n";
return;
}
if ($man->isPoor()) {
echo "貧賤夫妻百事哀\\n";
return;
}
if ($man->isBadTemper()) {
echo "銀河有多遠,你就給我滾多遠\\n";
return;
}
echo "可以先交往一段時間看看\\n";
}
```
<span style="color:green;"> 推薦</span>
除常用方法(如 getXxx/isXxx)等外,不要在條件判斷中執行其它復雜的語句,將復 雜邏輯判斷的結果賦值給一個有意義的布爾變量名,以提高可讀性。
說明:很多 if 語句內的邏輯表達式相當復雜,與、或、取反混合運算,甚至各種方法縱深調用,理解成本非常高。如果賦值一個非常好理解的布爾變量名字,則是件令人爽心悅目的事情。
<span style="color:green;">**正例**</span>:
```
if (ControlGroupHelper::*isControlGroup*($userId, 1, [0, 2, 4, 6, 8])) {
return $this->getContinueReadPopUp($userId, 1);
}
```
<span style="color:red;">**反例**</span>:
```
if (!tryAcquire($arg) && acquireQueued(addWaiter(Node::EXCLUSIVE), $arg)) {
selfInterrupt();
}
```
<span style="color:green;">**推薦** </span>
不要在其它表達式(尤其是條件表達式)中,插入賦值語句。 說明:賦值點類似于人體的穴位,對于代碼的理解至關重要,所以賦值語句需要清晰地單獨成為一行。
反例:
```
// 算術表達式中出現賦值操作,容易忽略 $count 值已經被改變
$threshold = ($count = PHP_INT_MAX) - 1;
// 條件表達式中出現賦值操作,容易誤認為是 $sync==$fair
return ($sync = $fair) ? new FairSync() : new NonfairSync();
```
<span style="color:green;">**推薦 **</span>
避免采用取反邏輯運算符。
說明:取反邏輯不利于快速理解,并且取反邏輯寫法一般都存在對應的正向邏輯寫法。
<span style="color:green;">**正例:**</span>
使用 if (x < 628) 來表達 x 小于 628。
<span style="color:green;">**反例:**</span>
使用 if (!(x >= 628)) 來表達 x 小于 628。
參考 下列情形,需要進行參數校驗:
1) 調用頻次低的方法。
2) 執行時間開銷很大的方法。此情形中,參數校驗時間幾乎可以忽略不計,但如果因為參數錯誤導致 中間執行回退,或者錯誤,那得不償失。
3) 需要極高穩定性和可用性的方法。
4) 對外提供的開放接口,不管是 RPC/API/HTTP 接口。
5) 敏感權限入口。
參考 下列情形,不需要進行參數校驗:
1) 極有可能被循環調用的方法。但在方法說明里必須注明外部參數檢查。
2) 底層調用頻度比較高的方法。畢竟是像純凈水過濾的最后一道,參數錯誤不太可能到底層才會暴露問題,可以省略。
3) 被聲明成 private 只會被自己代碼所調用的方法,如果能夠確定調用方法的代碼傳入參數已經做過檢查或者肯定不會有問題,此時可以不校驗參數。
注釋規約
強制 類方法的注釋必須使用 PHPDoc 規范,使用/\*\*內容\*/格式,不得使用 // xxx 方式。
說明:在 IDE 編輯窗口中,PHPDoc 方式會提示相關注釋,生成 PHPDoc 可以正確輸出相應注釋;在 IDE 中,工程調用方法時,不進入方法即可懸浮提示方法、參數、返回值的意義,提高閱讀效率。
<span style="color:green;">**正例:**</span>
```
/**
* 簡短描述
*
* 詳細描述可以包括多行,解釋函數的作用、參數和返回值。
*
* @param string $param1 描述參數1
* @param int $param2 描述參數2
* @return bool 返回值的描述
*/
function example($param1, $param2) {
// 函數體
}
```
<span style="color:red;">**強制:**</span>
方法內部單行注釋,在被注釋語句上方另起一行,使用//注釋。方法內部多行注釋使 用/\* \*/注釋,注意與代碼對齊。
<span style="color:green;">**推薦:**</span>
代碼修改的同時,注釋也要進行相應的修改,尤其是參數、返回值、異常、核心邏輯 等的修改。
說明:代碼與注釋更新不同步,就像路網與導航軟件更新不同步一樣,如果導航軟件嚴重滯后,就失去了 導航的意義。
參考 好的命名、代碼結構是自解釋的,注釋力求精簡準確、表達到位。避免出現注釋的一 個極端:過多過濫的注釋,代碼的邏輯一旦修改,修改注釋又是相當大的負擔。
<span style="color:red;">**反例:**</span>
```
// 將大象放入冰箱
put($elephant, $fridge);
```
方法名 put,加上兩個有意義的變量名 elephant 和 fridge,已經說明了這是在干什么,語義清晰的代碼不需要額外的注釋。
前后端規約
<span style="color:green;">**推薦:**</span>
服務端返回的數據,使用 JSON 格式而非 XML。
<span style="color:green;">**推薦:**</span>
在前后端數據交互過程中,特別是在傳遞布爾型參數時,我們應當謹慎對待以避免潛在的數據類型混淆問題。由于PHP作為一種動態類型且類型檢查相對寬松的語言,直接傳遞true或false作為布爾值可能會遭遇意料之外的轉化行為,導致邏輯判斷失誤。具體而言,當布爾值在傳遞過程中被不恰當地轉化為字符串形式,如"true"或"false",PHP在處理時可能會將這些字符串誤解為其對應的非空真值,從而使得原本意指邏輯假的"false"在條件判斷中仍被視為真。
反例:本人之前對結果某銀行的接口,文檔中說明有個字段返回的是bool類型,于是用empty()判斷為空,哪知對方傳了一個string類型的false,導致接口被判讀成恒true。
強制 在服務端開發過程中,嚴謹區分Map(關聯數組)與數組(索引數組)的數據結構至關重要。由于PHP自身的特性并不強制區分這兩種類型的數組,若處理不當,極易導致在預期返回空Map的情況下錯誤地返回了空數組,進而引發客戶端對接口響應的誤解和后續邏輯處理的困擾。
<span style="color:red;">**反例:**</span>
不存在可用訂閱檔位的時候,為了減少查詢,服務端不打算返回訂閱檔位信息,于是將數組中的字段置空,導致客戶端收到了空數組,造成無法解析的問題。
說明:在實際操作中,可以用stdClass來返回空對象
```
function getUserPreferences($userId) {
// 查詢用戶偏好數據...
if (!$preferencesFound) {
// 如果沒有找到用戶偏好,返回一個空對象
return new stdClass();
}
// 否則,返回填充了數據的用戶偏好對象
return $populatedUserPreferencesObject;
}
```
<span style="color:green;">**推薦:**</span>
對于需要使用超大整數的場景,服務端建議使用 String 字符串類型返回。 防止出現浮點型導致的精度丟失情況。
<span style="color:red;">**強制:**</span>
無論是新接口還是舊接口改造,服務端都需要及時維護文檔,約定響應數據接口和對應的邏輯。同樣客戶端也需要仔細閱讀文檔,有問題及時提出。
<span style="color:green;">**推薦:**</span>
客戶端提交的請求參數,作為系統交互的源頭之一,本質上屬于外部輸入,其可信度無法預設。因此,秉持安全至上的原則,我們必須視其為潛在的不可信數據,嚴謹對待,以杜絕因參數誤傳、惡意篡改或缺失等情形引發的系統異常與安全隱患。
<span style="color:red;">**反例:**</span>
存在一個優惠券記錄接口,存在參數type 1 全部 2 過期 3 已使用,結果客戶端請求了 0 1 2 導致優惠券展示錯位。
MySQL 數據庫
**建表規約**
**? 表名**
<span style="color:red;">**強制:**</span>
表名、字段名必須使用小寫字母或數字,根據項目添加或者不添加前綴,禁止出現數字開頭,禁止兩個下劃線中間只出現數字。數據庫字段名的修改代價很大,因為無法進行預發布,所以字段名稱需要慎重考慮。
說明:MySQL 在 Windows 下不區分大小寫,但在 Linux 下默認是區分大小寫。因此,數據庫名、
表名、字段名,都不允許出現任何大寫字母,避免節外生枝。
<span style="color:green;">**正例:**</span>
aliyun_admins,rdc_configs,level3_names
<span style="color:red;">**反例:**</span>
AliyunAdmin,rdcConfig,level_3_name
推薦 表的命名最好是加上“業務名稱\_表的作用”。
<span style="color:green;">**正例:**</span>
alipay_tasks / force_projects / trade_configs
<span style="color:green;">**推薦:**</span>
由于laravel框架的約定,也為了保持一致,表名采用名詞的復數形式,以反映表中存儲的是多個同類實體的數據集合。
SQL
fq_users
? 字段名
<span style="color:red;">**強制:**</span>
字段名應使用全小寫字母或數字。同樣采用“蛇形命名”,即單詞間通過下劃線 \_ 連接。
<span style="color:red;">**強制:**</span>
禁用保留字,如 desc、range、match、delayed 等,請參考 MySQL 官方保留字。
<span style="color:red;">**強制:**</span>
小數類型為 decimal,禁止使用 float 和 double。
說明:float 和 double 在存儲的時候,存在精度損失的問題,很可能在值的比較時,得到不正確的結果。如果存儲的數據范圍超過 decimal 的范圍,建議將數據拆成整數和小數分開存儲。
<span style="color:red;">**強制:**</span>
表達是與否概念的字段,必須使用 is\_xxx 的方式命名,數據類型是 unsigned tinyint 1 表示是,0 表示否)。
說明:任何字段如果為非負數,必須是 unsigned。
<span style="color:green;">**正例:**</span>
表達邏輯刪除的字段名 is\_deleted,1 表示刪除,0 表示未刪除。
推薦 除非字段表示“是與否概念”時,強烈建議避免采用數值0作為選項值。這樣做有助于規避在使用empty()函數進行判斷時可能遭遇的潛在異常情況。選用TINYINT UNSIGNED數據類型,既能有效壓縮存儲空間,又能賦予字段寬泛的選擇范圍。
推薦 表必備三字段:id, created_at, updated_at。
說明:其中 id 必為主鍵,類型為 bigint unsigned、單表時自增、步長為 1。gmt\_create, gmt\_modified 的類型均為 timestamp 類型,前者現在時表示主動創建,后者過去分詞表示被動更新。
推薦 字段允許適當冗余,以提高查詢性能,但必須考慮數據一致。冗余字段應遵循:
1)不是頻繁修改的字段。
2)不是 varchar 超長字段,更不能是 text 字段。
<span style="color:green;">**正例:**</span>
商品類目名稱使用頻率高,字段長度短,名稱基本一成不變,可在相關聯的表中冗余存儲類目名稱,避免關聯查詢。
? 索引
強制 主鍵索引名為 pk_字段名;唯一索引名為 uk_字段名;普通索引名則為 idx\_字段名。
說明:pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的簡稱。
? 其他
<span style="color:green;">**推薦:**</span>
如果修改字段含義或對字段表示的狀態追加時,需要及時更新字段注釋。
推薦 單表行數超過 500 萬行或者單表容量超過 2GB,才推薦進行分庫分表。
說明:如果預計三年后的數據量根本達不到這個級別,請不要在創建表時就分庫分表。
<span style="color:green;">**推薦:**</span>
如果需要保存字符集推薦使用utf8mb4格式,特別是需要保存表情的場景下。使用其他字符集的可能導致表情保存失敗。且不同的字符集在JOIN的時候會由于字符集和校對規則的不一致,MySQL無法正確比較和處理涉及這些字段的查詢條件,出現Illegal mix of collations問題。
[該類型的內容暫不支持下載\]
MySQL索引規約
**? 唯一索引**
<span style="color:red;">**強制:**</span>
業務上具有唯一特性的字段,即使是多個字段的組合,也必須建成唯一索引。
說明:不要以為唯一索引影響了 insert 速度,這個速度損耗可以忽略,但提高查找速度是明顯的;另外,即使在應用層做了非常完善的校驗控制,只要沒有唯一索引,根據墨菲定律,必然有臟數據產生。
**? JOIN關聯**
<span style="color:red;">**強制:**</span>
API代碼中超過三個表禁止 join。需要 join 的字段,數據類型必須絕對一致;多表關聯查詢時,保證被關聯的字段需要有索引。
說明:即使雙表 join 也要注意表索引、SQL 性能。
**? 搜索條件**
<span style="color:green;">**推薦:**</span>
頁面搜索減少左模糊或者全模糊的使用,如果需要請走搜索引擎來解決。
說明:索引文件具有 B-Tree 的最左前綴匹配特性,如果左邊的值未確定,那么無法使用此索引。
**? 優化的目標**
推薦 SQL 性能優化的目標:至少要達到 range 級別,要求是 ref 級別,如果可以是 consts最好。
說明:
1)consts 單表中最多只有一個匹配行(主鍵或者唯一索引),在優化階段即可讀取到數據。
2)ref 指的是使用普通的索引(normal index)。
3)range 對索引進行范圍檢索。
反例:explain 表的結果,type=index,索引物理文件全掃描,速度非常慢,這個 index 級別比較 range 還低,與全表掃描是小巫見大巫。
**? 組合索引**
<span style="color:green;">**推薦:**</span>
建組合索引的時候,區分度最高的在最左邊。
正例:如果 where a=? and b=? ,如果 a 列的幾乎接近于唯一值,那么只需要單建 idx\_a索引即可。
說明:存在非等號和等號混合時,在建索引時,請把等號條件的列前置。如:where c>? and
d=? 那么即使 c 的區分度更高,也必須把 d 放在索引的最前列,即索引 idx_d_c。
**? 隱式轉化**
推薦 防止因字段類型不同造成的隱式轉換,導致索引失效。
**? 其他**
參考 創建索引時避免有如下極端誤解:
1)寧濫勿缺。認為一個查詢就需要建一個索引。
2)寧缺勿濫。認為索引會消耗空間、嚴重拖慢更新和新增速度。
3)抵制唯一索引。認為業務的唯一性一律需要在應用層通過“先查后插”方式解決。
MySQL語句
**? count(*)**
<span style="color:red;">**強制:**</span>
不要使用 count(列名)或 count(常量)來替代 count(*),count(*)是 SQL92 定義的標準統計行數的語法,跟數據庫無關,跟 NULL 和非 NULL 無關。
說明:count(*)會統計值為 NULL 的行,而 count(列名)不會統計此列為 NULL 值的行。
**? count(distinct col)**
<span style="color:red;">**強制:**</span>
count(distinct col) 計算該列除 NULL 之外的不重復行數,注意 count(distinct col1, col2) 如果其中一列全為 NULL,那么即使另一列有不同的值,也返回為 0。
**? 分頁**
<span style="color:red;">**強制:**</span>
在代碼中寫分頁查詢邏輯時,若 count 為 0 應直接返回,避免執行后面的分頁語句。
**? 存儲過程**
<span style="color:red;">**強制:**</span>
禁止使用存儲過程,存儲過程難以調試和擴展,更沒有移植性。
**? IN操作**
<span style="color:green;">**推薦:**</span>
API代碼中in 操作能避免則避免,若實在避免不了,需要仔細評估 in 后邊的集合元素數量,控制在 1000 個之內。
**? 查詢字段**
<span style="color:green;">**推薦:**</span>
在表查詢中,建議不要使用 \* 作為查詢的字段列表,需要哪些字段最好明確寫明。
說明:
1)增加查詢分析器解析成本。
2)增減字段容易與 resultMap 配置不一致。
3)無用字段增加網絡消耗,尤其是 text 類型的字段。
最后,祝大家沒有BUG。