PHP 伺服器端內部業務處理失敗訊息傳遞方式

weixin_34026276發表於2015-01-29

我需要拍磚 和 看見你們的意見,為團隊少挖坑

場景:建立訂單

實際流程:

終端呼叫(PC端、移動端APP、微信端、Web端)-->控制器 或 介面-->實際的業務處理-->控制器 或 介面-->終端做出相應處理(控制器可能是渲染對應頁面; 介面返回 JSON資料)

業務處理類動作:

  1. 檢查使用者是否登陸
  2. 驗證商品 ID、購買數量等引數
  3. 檢查該商品是否處於上架中
  4. 檢查該商品是否可以購買
  5. 各種檢查...
  6. 建立訂單
  7. 記錄 Log
  8. 返回訂單建立結果給呼叫者

    1. 建立失敗:...
    2. 建立成功:[return true|return Order info]

遊戲規則

前後端資料格式約定為 JSON格式如下:

{
    code: "00000",      // 狀態碼
    msg: "操作成功!",   // 提示資訊
    data: {}            // 資料
}

注:"00000":業務成功狀態碼;非"00000"都為業務失敗。
為了防止伺服器端狀態碼氾濫成災,code可以為"",這時 msg 裡面則是相應的錯誤資訊,只為給使用者提示。

導火線

專案開發完畢,測試人員去測試,提如下Bug:

如果使用者未登入,進個人中心,提示使用者未登入,然後會去登陸view;而在下單頁,提示使用者未登入,卻沒有去登陸view。

然後前端童鞋開始去修復該問題,查出如下問題:

  • 個人中心伺服器介面返回的資料格式:
{
    code: "00008",
    msg: "使用者未登入,請登入",
    data: [ ]
}
  • 下單頁伺服器介面返回的資料格式:
{
    code: "",
    msg: "使用者未登入,請登入!",
    data: [ ]
}

然後前端童鞋對伺服器端童鞋講,這裡你應該返回給我code: "00008",我這邊一看 code便知是使用者未登入,就可以做出相應的操作,這裡你只返回提示資訊,我這邊不好做更加細膩的操作。

然後,後端童鞋開始嘗試給該地方新增上 code。
開始著手修改程式碼:
首先找到介面方法裡面發現如下 demo:

php$order = kernel::single('sysapi_ecoupon_order')->create($params, $msg);
if (!$order) {
    return array('code' => '', 'data' => array(), 'msg' => $msg);
}
return array('code' => '00000', 'data' => $order);

改方法返回array(); 在外部統一入口、出口處再返回 JSON出去。

sysapi_ecoupon_order

phppublic function createNew($params, & $msg)
{
    // 獲取使用者資訊
    $member_info = app::get('b2c')->model('members')->get_current_member();

    if (empty($member_info)) {
        $msg = app::get('ecoupon')->_('使用者未登入,請登入!');
        return false;
    }
    // 繼續下面的業務處理
}

介面呼叫的kernel::single('sysapi_ecoupon_order')->create($params, $msg);這裡面做實際的業務處理,錯誤資訊是通過 $msg 向上傳遞出去,外部沒辦法通過 $msg 獲知對應的 code。然後給前端童鞋講這種情況沒辦法返回 code給你。

前端就只能通過判斷 msg的方式來修復該問題
然後寫了如下 demo:

javascriptif("使用者未登入,請登入!" == data.msg) {
    // 使用者未登入,去登入
    // ...
}

然後提交,測試,通過,上線,N天后

有人跑過來講:下單頁 與 個人中心的提示有點不同,貌似多了個 "!"。(舉例而已,更多的可能是提示不友好、錯別字等情況)

然後後端同學修改為 $msg = app::get('ecoupon')->_('使用者未登入,請登入'); 提交,測試不通過,前端同學再修改為if("使用者未登入,請登入" == data.msg),提交,測試通過

// 如此反反覆覆

終究有一天:產品、測試,前端、後端混戰了一場。N人,卒.....

重新正視問題

最終前後端得出結論:要想對使用者實現更加友好的體驗,前後端資料必須有個標識具有唯一性不變性。而現在用的 msg卻不具備,還是得用 code。並且這裡前後端極度耦合msg。

後端童鞋回來繼續修改程式碼,開始著手給這裡新增上相應的 code。
開始思考該怎麼新增 code,現在的問題是 create( ) 方法可能是其他童鞋開發,內部返回的提示資訊,我這邊是呼叫者,不能確定方法內部到底會返回什麼提示資訊,無解。

忽然,有一天想到,我在呼叫該方法之前檢查下使用者有沒有登入就OK了,然後開始寫如下實現:

public function create($params)
{
    $member = app::get('b2c')->model('members')->get_current_member();

    // 登入驗證
    if (empty($member)) {
        return array('code' => '00008');
    }

    $msg = '';

    $order = kernel::single('sysapi_ecoupon_order')->create($params, $msg);
    if (!$order) {
        return array('code' => '', 'data' => array(), 'msg' => $msg);
    }
    return array('code' => '00000', 'data' => $order);
}

呵呵,好機智的少年。
然後告訴前端,這裡可以返回 code了,前端愉快的刪掉原來那坨判斷 msg的程式碼,而在 ajax請求的地方統一判斷 code就能預知使用者未登入,做出相應的操作。

經測試,上線。一切又回到了美好時光。

隨著時光的流逝,業務的增加,後端童靴發現Order類裡面如下 demo:

phppublic function create($params)
{
    $member = app::get('b2c')->model('members')->get_current_member();

    // 登入驗證
    if (empty($member)) {
        return array('code' => '00008');
    }

    // 實際業務處理....    
}

public function getOrderList($params)
{
    $member = app::get('b2c')->model('members')->get_current_member();

    // 登入驗證
    if (empty($member)) {
        return array('code' => '00008');
    }

    // 實際業務處理....    
}

public function getOrderDetail($params)
{
    $member = app::get('b2c')->model('members')->get_current_member();

    // 登入驗證
    if (empty($member)) {
        return array('code' => '00008');
    }

    // 實際業務處理....    
}

// ...

這都是什麼玩意............ 然後開始封裝,稍微好了點

又過了一段時間,有人過來說建立訂單還需要優化體驗,
點選建立訂單提示如下:

  • 超過最大購買量——給出提示,繼續留在建立訂單頁
  • 該商品已賣光或已下架——引導使用者去商品列表頁

這時,前端童鞋告訴後端童鞋,商品下架的時候,你也應該返回一個狀態碼。

後端童鞋開始打算新增 code,發現如下 demo

phpkernel::single('sysapi_ecoupon_order')->create($params, $msg);

這裡的提示資訊是 $msg 返回的,使用者登入外部可以提前檢測,這裡的商品能否購買要實現新增 code也需要提前檢測,將來要是需要新增類是功能豈不是...... 每需要一個精確的 code返回出去,這裡就需要新增檢測,這裡程式碼將會變得無法直視。
況且這裡本該在業務裡面檢測,一切不那麼友好起來了。

再次思考,程式碼寫的不爽了,一定是哪裡不對

問題所在

開始懷疑 public function create($params, & $msg) { } 這裡不應該是通過 & $msg 來作為 呼叫者與 被呼叫者之間的 錯誤資訊通訊約定,一切的問題都出在了這裡。錯誤訊息向上傳播的約定不合適

如果這裡約定的是 code作為錯誤向上傳播一切的問題即將不復存在。在呼叫業務方法之前的檢測程式碼就都可以去掉了,程式碼簡約,一切又美好起來。

接下來繼續思考,使用 code作為業務處理失敗訊息傳遞問題又來了

  • 現在已有的業務程式碼都是如此定義public function create($params, & $msg),怎樣更加友好的替換成 code
  • 如果使用 code,code只能伺服器端 與 前端約定的一個具有唯一性的標識(code 比 msg 對國際化的實現更加容易)但是並不能直接展示給使用者,那麼就需要定義每個 code 的代表的意義 與 對應的提示資訊。那麼問題來了,code 應該已怎樣規範來定義所代表的含義

再來看如下常用的兩種方法定義:

  1. public function create($params, & $msg)
  2. public function create($goodsId, $num, & $msg)

第一種方式,引數通過一個 $params陣列傳遞過來,方法內部在把錯誤提示放到 $msg中。

  • 好處:$params是個陣列,裡面引數可以任意新增
  • 缺點:該方法呼叫者在外部不能知道該方法需要什麼引數,必須來看該方法內部實現,做出對應的陣列 key的轉換(如: user_id 轉 userId)。方法呼叫者 與 方法實現 極度耦合。維護成本大、出Bug係數高

第二種方式,按基本型別分別傳遞單個引數

  • 好處:方法呼叫者根據方法定義就能夠知道方法具體需要的引數,呼叫方法時不需要作 key轉換,只需要傳遞對應的引數即可
  • 缺點:引數數目過多時,慘不忍睹

這裡有如下問題:

  • 這裡如何已一種更加容易維護,擴充套件的方式來處理(Java裡面方法引數已物件的方式傳遞可以借鑑
  • 這裡的& $msg 真的合適嗎,如果是第二種方式定義的方法,以後擴充套件個 $phone 該如何處理?public function create($goodsId, $num, & $msg, $phone='')這樣麼?怎麼看怎麼蛋疼
  • 再來看不通過 & $msg傳遞錯誤資訊之後的程式碼
phppublic function create($goodsId, $num)
{
    if ( ? ) {
        // 返回狀態碼
        return '0001';
    }
    if ( ? ) {
        // 返回狀態碼
        return '0002';
    }
    // 建立訂單
    // ...
    // 返回訂單資訊
    return $order;
}

看似實現了,但是方法呼叫者,怎麼呼叫怎麼蛋疼,一會返回狀態碼,一會返回訂單資訊,完全兩種型別。

綜合以上問題:

得出以下結論:

  • 業務處理失敗訊息要以 code 的方式向上傳遞給呼叫者
  • 業務處理失敗訊息以引數的方式傳遞不是很適合,並且不能以 return的方式返回

再次思考,最終從 Java裡面想到了一點思路(幸好是 Java出身。疑問:為何面試的時候 Java的工作經驗都不算在 PHP工作經驗裡呢,並沒有因此而加分)

解決方案:

  • 自定義一個異常類,包括 codo屬性 和 msg 屬性
  • 凡是遇到業務不能正常處理的時候就建立一個異常物件,設定對應的 code 或者 msg屬性(為了減少 code氾濫,這裡的 code 與 msg 可以2選一,如果前端需要做精準的處理,就設定 code,如果只是為了給使用者提示,就只返回 msg,則可以減少一個 code),然後丟擲異常
  • 方法呼叫者在外部統一捕捉該異常,如 介面的統一入口出口的方法內部處理

因個人工作時間、專案經歷不多、歸根結底經驗不足。現在將該方案寫下來,還望有經驗的大神拍磚,以免給團隊挖坑,以上 $msg 就是 N久以前埋下的坑。

該文章釋出在自己站點地址:http://www.webdevs.cn/article/91.html

相關文章