好孩子的編碼習慣

風中有php做的雲發表於2020-08-19

前言

我經常能聽到一些對話
狗腿子A:哇 我剛剛去改**專案的程式碼,看的我有點懷疑人生
狗腿子B: 我現在專案的跟屎山一樣
狗腿子C: 我隔壁那哥們每天寫程式碼都特別隨性,我有點按耐不住我的刀
.....
image.png
今天跟大家聊聊一些 我眼中 好孩子的編碼習慣,而不是程式碼風格習慣 ,當然還是強烈建議大家程式碼風格跟psr-12psr-1靠齊。

psr-1基礎編碼規範 、psr-12編碼規範託充
This specification extends, expands and replaces PSR-2, the coding style guide and requires adherence to PSR-1, the basic coding standard.

離職流程.png

推薦一本《程式碼整潔之道》,這本書我已經書都快翻爛了,牆裂推薦!!!

image.png

不過度的if巢狀判斷

案例背景
有個函式需要判斷使用者是否參與活動
流程圖1.png

案例程式碼

    if (使用者 == VIP) {
        if (使用者的過期時間 <= 1個月內) {
            if (使用者沒參加過任務) {
                return true;
            }
        } else {
            return false;
        }
    } else {
        return true
    }
    

面對這種多條件的判斷可以試著用攔截法逆向思維
攔截法只要符合條件立馬返回結果,不再巢狀的if。可以理解成橫向判斷變成縱向判斷。

舒適感
從上往下看 > 從左往右看

逆向思維 大家上學的時候都瞭解過,與其漫天去找符合的條件還不如找不符合條件,這樣的邏輯程式碼可以少很多。

    if (使用者 != VIP) {
        return true;
    } 
    
    if (使用者參加過任務) {
        return false;
    }
    
    if (使用者的過期時間 <= 1個月內) {
        return true;    
    }
    
    return false;
    

不過度的try-catch巢狀

我遇到過很多專案都過度巢狀try-catch導致最上層的try-catch catch了寂寞。
image.png

案例程式碼

function insertUser($data)
{
    try {
        userIsInValid();
    } catch (Exception $exception) {
    }
}

function userIsInValid()
{
    try {
        //邏輯判斷
    } catch (Exception $exception) {
        return true;
    }
    return true;
    
}

這樣的程式碼沒有問題,但是如果假設userIsInValid真的發生程式碼級的錯誤沒法知道那裡出問題,雖然不會破壞業務的健壯性。
可能有人說了在Excetion加個日誌,但是如果巢狀的try-catch多了,排查日誌也是一件很痛苦的事情。

1.儘可能業務最上層包裹異常 除非網路IO請求函式。
2.如果非要異常巢狀 需要定義每個異常的型別。
3.儘可能根據特定的異常進行catch 不建議直接catch Exception。
4.異常和日誌是個cp,還是不要忘記了。
image.png

<?php

function insertUser($data)
{
    try {
        userIsInValid();
    } catch (Exception $exception) {
        // 日誌
        // 業務處理
    } catch (HttpException $httpException) {
       // 日誌
       // 業務處理
    }
}

function userIsInValid()
{
    //
    return true;
}

不要用if-else做錯誤型別判斷

案例程式碼 (來源某個網民前段時間諮詢)

<?php

.....
if ($code === 'NOTENOUGH') {
    packApiData(400014, 'Company have no enough money to pay', [], '企業餘額不足');
} elseif ($code === 'AMOUNT_LIMIT') {
    packApiData(400015, 'Amount limit', [], '金額超限或被微信風控攔截');
} elseif ($code === 'OPENID_ERROR') {
    packApiData(400016, 'Appid and Openid does not match', [], 'Openid格式錯誤或不屬於此公眾號');
} elseif ($code === 'SEND_FAILED') {
    // 付款錯誤,要查單來看最終結果
    if ($orderInfo[1]['status'] == 'SUCCESS') {
        // 還是成功給了,扣回餘額
        
        packApiData(200, 'success', [$orderInfo[1]]);
    } else {
        packApiData(400017, 'Weixin pay failed', [], '微信支付付款失敗');
    }
} elseif ($code === 'SYSTEMERROR') {
    packApiData(400018, 'Weixin pay server error', [], '微信支付伺服器錯誤');
} elseif ($code === 'NAME_MISMATCH') {
    packApiData(400019, 'Real name mismatch', [], '微信使用者的真名校驗失敗');
} elseif ($code === 'FREQ_LIMIT') {
    packApiData(400020, 'Api request frequently', [], '微信支付介面呼叫過於頻繁,請稍候再請求');
} elseif ($code === 'MONEY_LIMIT') {
    packApiData(400021, 'Company have reached total payment limit', [], '已經達到今日付款總額上限');
} elseif ($code === 'V2_ACCOUNT_SIMPLE_BAN') {
    packApiData(400022, 'This payment account has no real name', [], '使用者的微信支付賬戶未實名');
} elseif ($code === 'SENDNUM_LIMIT') {
    packApiData(400023, 'The number of times the user paid today exceeded the limit', [], '該使用者今日收款次數超過限制');
}

這樣的程式碼可能寫起來特別舒服,但是後期進行業務的增加改寫和時間的沉澱,容易變成讓人害怕的屎山程式碼。

image.png

我們用mapping錯誤碼來調整下

function packApiDataByOrderError($code)
{
    $errorCodeMappins = [
        "NOTENOUGH" => [
            "code" => 400014,
            "wx_message" => "Company have no enough money to pay",
            "error_message" => "企業餘額不足"
        ],

        "AMOUNT_LIMIT" => [
            "code" => 400015,
            "wx_message" => "Amount limit",
            "error_message" => "金額超限或被微信風控攔截"
        ],

        .....
    ];

    if (array_key_exists($code, $errorCodeMappins)) {
        packApiData(
            $errorCodeMappins[$code]['code'],
            $errorCodeMappins[$code]['wx_message'],
            [],
            $errorCodeMappins[$code]['error_message']
        );
    }

    packApiData(
        999999,
        "undefined message",
        [],
        "未知錯誤"
    );
}

建議errorCodeMappins不要放在函式內,可以放在類頂部或者專門列舉類。
通過errorCode 可以避免調整主流程程式碼,能夠保證主流程的程式碼比較精簡也能對不同的code進行錯誤的定義

if ($code == "SEND_FAILED") {
    // 付款錯誤,要查單來看最終結果
    if ($orderInfo[1]['status'] == 'SUCCESS') {
        // 還是成功給了,扣回餘額
        PDOQuery($dbcon, 'UPDATE user SET money=money-? WHERE open_id=?', [$payAmount, $openId], [PDO::PARAM_INT, PDO::PARAM_STR]);
        packApiData(200, 'success', [$orderInfo[1]]);
    } else {
        packApiData(400017, 'Weixin pay failed', [], '微信支付付款失敗');
    }
}

packApiDataByOrderError($code);

在合適的場景使用設計模式

上述可能只能針對錯誤碼進行改造,如果萬一我們需要不同的錯誤進行邏輯處理還怎麼辦。這時候可以考慮用設計模式 (比如用以多型取代條件表示式)

設計模式固好但不要過度使用,不然整個專案更難維護,你要堅信未來的你隊友不知道是什麼樣的生物

image.png

$callbackCodeMappings = [
    "SEND_FAILED" => OrderSendFailed::class,
];

if (array_key_exists($code, $callbackCodeMappings)) {
    $class = new $callbackCodeMappings[$code];
    $class->handle();
}


interface OrderStateImp
{
    public function handle($context);
}

class OrderSendFailed implements  OrderStateImp
{
    public function handle($context)
    {

    }
}

$callbackCodeMappings同樣建議配置專門列舉檔案內。
給出得程式碼比較粗糙,其實可以更加健壯性的做一些判斷

統一處理浮點運算結果

由於php是弱物件語言,所以面對一堆情況總能出現,這個訂單資料怎麼不對了,介面有問題。

$int = 0.58; var_dump(intval($int * 100));
output:57

在浮點數裡面 58是被視為57.999999999999999999999……9999無限接近58
再intval強制轉換乘整型的時候就預設採用擷取法取整

所以最好養成一個好習慣每次在計算浮點數的時候用
BC Math

$int = 0.58;
intval(strval($int * 100))

或者使用BC MATH

bcmul(0.58, 100, 0);

image.png

鼓勵用全域性錯誤碼來控制錯誤

寫介面的我們對以下的json格式特別熟悉

{
    "success": true,
    "error_code": 0,
    "message": "",
    "results": []
}

對以下的程式碼也已經熟悉

if (***) {
    $this->error(999,"****", []);
}

這樣的結果的錯誤碼容易重複沒有統一管理,事實上唯一錯誤碼應該有以下幫助。
1.前端可以根據錯誤碼做邏輯處理
2.根據錯誤碼能直接快速定位到錯誤程式碼

建議

<?php

namespace App\ErrorCode;

class UserErrorCode
{
    const USER_DISABLE_ERROR = [
        "error_code" => 1050001,
        "message" => "使用者已被停用"
    ];
}

$this->error(UserErrorCode::USER_DISABLE_ERROR);

錯誤碼建議

1-2位 - 專案碼 | 3-4位 - 模組碼 | 5-7位具體業務錯誤碼

可靠的命名規範

不可靠的命名總會讓人誤導。
比如變數命名為userArrayList 我以為是個陣列列表變數,事實上這個特麼是個物件列表。

1.做有意義的區分
比如 singleUserItemuserItem有啥區別
比如 getUserListgetUsers有啥區別
image.png

2.可以通過搜尋翻譯能知道的變數含義
不要把變數貼入搜尋翻譯會出現七七八八的東西
3.如果真的不知道該怎麼翻試試用拼音把別硬凹了
比如之前做百度的一個介面對接
變數命名為hundredDegree而不是baidu
image.png
其他的可以參照《程式碼簡潔之道》

擅用middleware

middleware可以理解成觀察者模式,我們開發的介面總會遇到很多同樣操作,比如
1.身份檢測
2.許可權判斷
3.請求引數filter調整
4.記錄介面資訊
5.介面限流
我見過挨個介面去實現、也見過初始化一個ControllerBase的類,實現這些,子類的Controller去繼承這些。
其實我們可以抽離成middleware去實現
image.png

好處可以根據不同介面對middleware進行組合選擇,而不是對程式碼進行各特殊化處理.

函式的單一職責

最最最最後也是最重要的,程式碼的噁心大多數來源於函式的職責不清晰,有全都塞在一起的、東一塊西一塊的。
其實關於單一職責有很多文章在描述,如何去檢驗或者去寫符合標準的單一職責。
畫流程圖
如果你能把業務的流程圖畫的特別清晰,那麼你的函式的職責也就定下來了。
image.png

<?php

// 兌換邏輯
function doExchange()
{
    if (checkIsLock()) {
        
    }
    lock();
    if (!checkUserIsExchange()) {
        
    }
    costUserPoint();
    exchangeGoods();
}
// 判斷是否悲觀鎖
function checkIsLock(){}
// 上悲觀鎖
function lock(){}
// 判斷使用者是否可以兌換
function checkUserIsExchange(){}
// 扣除積分
function costUserPoint(){}
// 兌換商品
function exchangeGoods(){}

最後

上述為洪光光心中的好孩子的習慣,也有可能是你眼中壞孩子的習慣。如果你認為是壞孩子的習慣或者認為還有其他好孩子的習慣歡迎評論撕逼討論。
畢竟
image.png

留個彩蛋 看看大家怎麼實現
寫一個函式returnScoreResult,請根據輸入的分數,返回對應的成績的等級。
1.如果分數小於0或者大於100 返回 【無效分數】
2.如果分數>=0,<60 返回 【不及格】
3.如果分數>=60,<70 返回【及格】
4.如果分數>=70,<80 返回 【一般】
5.如果分數>=80, <90 返回 【良好】
5.如果分數>=90, <100 返回 【優秀】
6.如果分數=100 返回【滿分】

相關文章