Web API 開發實踐

前言

以前在公司負責了一個項目,進行了先後端分離,筆者負責了整個項目的基本結構的搭建,在此總結一些經驗。本文主要介紹後端web api的設計與實現。demo代碼連接:github代碼php

基本架構

代碼分層

應用的基本架構主要包含如下5個部分:laravel

  • Controller Layer(控制器層)
  • Transformer Layer(轉換層)
  • Service Layer(服務層)
  • Repository Layer(倉庫層)
  • Model Layer(模型層)

各個層次的主要職責以下圖所示git

層次職責圖.png

詳細說明github

  1. 基本的程序流程如上圖所示,從1到8。若業務邏輯比較簡單,能夠直接跳過Service層,由Controller層直接調用Repository層。
  2. 各層次之間能夠經過依賴注入聯繫起來。
  3. 業務邏輯主要分佈在Service層和Model層。Service層負責工做流邏輯,即任務的具體執行流程,如事務處理等;Model層負責領域邏輯,領域邏輯包括了業務規則、業務計算等。
  4. 一般狀況下,Service層因爲包含了主要的工做流邏輯,其可複用性比較差,但當Service層的業務邏輯積累到必定程度的時候,會沉澱一些通用的業務邏輯(工做流邏輯),最好將通用的業務邏輯提取出來,造成一個Service層內的子層,稱爲「通用處理層」(General Process Layer),能夠將這部分代碼放到當前Services目錄下的General目錄中。
  5. Service層的返回值: 1.業務對象(model等業務數據)2.bool值,指示處理結果。
  6. 當Service層的業務邏輯沒法正常執行時,須要拋出業務處理異常BusinessException(注意,不是程序執行異常。業務處理異常例子:如帳戶餘額不足,沒法轉帳)。經過業務處理異常,將不正常的業務處理結果返回給調用者(eg:Controller或其餘Service)。而在正常執行業務邏輯的狀況下,則返回Service層的正常返回值,即上面第5點。
  7. 在每一層中,當新開一個子分類時,最好創建一個子分類的基類。以Controller層爲例子,當須要在app/Api/Controllers/V1目錄創建一個Blog子目錄時,最好在建好後的目錄中添加一個BaseController,做爲該目錄下的基類。
  8. Model層能夠細分爲AR(ActiveRecord)層和Domain層。Domain層一般是基於AR層。AR層中每一個類對應一張數據庫表,而Domain類中包含的數據能夠來自多個AR類。web

    • 一般會在AR層中寫與數據庫相關的代碼,如表的關聯關係,表屬性的可取值等。
    • 一般會在Domain層中寫相應的領域邏輯。eg : 領域模型某些值的取值規則
    • Domain類表明一個完整的領域模型,而AR類則不必定構成一個完整的領域模型。eg : 產品的數據存放在多張張表內:product_a和product_b等,所以會有多個AR類對應這些表;同時,能夠引入一個名爲「Product」的Domain類,它表明了一個完整的產品(領域模型)。Domain類能夠基於底層AR類中一個(通常來講是基於主表)。

目錄結構

目錄結構以下所示:數據庫

目錄結構圖.png

詳細說明後端

  1. 如上圖所示,各個層次Controller、Service、Transformer、Model、Repository都有本身相應的目錄
  2. Controllers目錄說明(Controller層)api

    • Controller層,全部api的控制器放在該目錄下,按版本分類(V1,V2...),版本目錄下按照業務分類
    • Controller層的職責:服務器

      • 校驗輸入
      • 處理請求&構造響應
      • 調用Transformer層、Service層、Repository層,但不該該在Controller中包含任何業務邏輯
    • 在各個版本目錄之下(V1,V2...),按照業務將Controller分到不一樣的子目錄中(eg:Blog,Marketing...),而不是按照數據庫進行劃分,雖然按照業務劃分與按照數據庫劃分的結果可能同樣
    • 每一個版本目錄下有一個版本控制器(eg:V1Controller),該版本下的全部控制器須要繼承自該控制器。版本控制器必須繼承自App\Http\Controllers\ApiController
    • 按照業務劃分的控制器子目錄中應該有一個控制器基類(eg:BaseController),全部該目錄下的控制器繼承自該基類控制器
  3. Common目錄說明架構

    • Common目錄用於放置一些在整個項目中均可以使用的通用代碼,一般這些代碼不該該包含特定的業務邏輯
    • 子目錄Components用於放置組件代碼(注意:這些組件代碼不該該繼承自框架代碼/第三方代碼,不然應該將其放置到Extensions目錄)。一般這些代碼能提供一個特定的功能,但又不依賴框架自己,能夠做爲其餘項目的第三方包使用
    • 子目錄Extensions用於放置擴展了框架代碼/第三方代碼原有功能的代碼(一般意味着繼承自框架代碼/第三方代碼),注意與Components區分
    • 子目錄Enum用於放置「常量定義」的代碼
    • 子目錄Helpers用於放置一些工具類,工具類中一般會提供一些靜態方法,方便調用
    • 子目錄Scopes用於放置與Eloquent ORM相關的Scopes定義
    • 子目錄Lib用於存放一些底層的庫文件
  4. Models目錄說明(Model層)

    • Model層,全部的模型類放置在該目錄下。一般按數據庫進行分類(eg: DbBlog)
    • Model層的職責(繼承自Eloquent class時):

      • 對應一張數據庫表,一個model實例表示表中一條記錄
      • 處理property ,如$db, $table,$fillable等;處理scope
      • Accessors & Mutators : 在從model實例中獲取或存儲屬性時對其進行格式化
      • 關聯關係配置: 使用hasMany()、belongsTo()等
      • model自己行爲的代碼(即領域邏輯代碼,屬於業務邏輯的一部分),包括了model在運行時的狀態變化,如status由valid變換成invalid
    • Model層的職責(不繼承自Eloquent class時):

      • 做爲一個領域類,包含領域邏輯
    • 當一個完整的領域類被分割成多個數據庫表存儲在數據庫中時,能夠在各數據庫目錄(eg:DbBlog)下建立Domain目錄,用於存放完整的領域類。
    • 全部對應數據庫表的Model應該間接繼承自AppModel。每一個數據庫目錄下(eg: DbBlog)應該包含一個BaseModel(表明該數據庫),其餘Model繼承自該BaseModel
    • 注意:對數據庫表進行「增刪改查」的操做代碼請不要放置到Model,應該將「增刪改查」的代碼放置到Repository層
  5. Repositories目錄說明(Repository層)

    • Repository層,全部倉庫類放置在該目錄下。一般按照業務/數據庫進行劃分
    • Repository層的職責:

      • 僅包含對數據庫直接進行增刪改查操做的代碼,輔助Model層(除此以外請不要放置其餘代碼;一般增刪改的邏輯比較單一,而查則會有多種狀況,將各類查詢邏輯在此處實現)
    • Repository層僅包含直接對數據庫進行操做的代碼,其餘涉及外部調用等功能的代碼應該考慮放置在Service層中。
    • 全部的倉庫類應該繼承自AppRepository類。
  6. Services目錄說明(Service層)

    • Service層,全部的服務類放置在該目錄下。一般按業務進行分類
    • Service層的職責:

      • 處理牽涉到的外部行爲:如發送郵件,使用外部API(如使用隊列,調用thrift,調用其餘團隊的服務等)
      • 包含業務邏輯(主要是工做流邏輯(workflow logic),即完成某個任務的具體流程):service層是業務邏輯存在的主要地方,輔助Controller層;當須要對數據庫進行增刪改查時,則應該調用相應的Repository層
    • 全部的服務類都應該繼承自AppService類
  7. Transformers目錄說明(Transformer層)

    • Transformer層,全部的轉換類放置在該目錄下。一般按照業務進行分類。
    • Transformer層的職責:

      • 處理顯示邏輯
      • 管理API接口的輸出(使接口的輸出與底層的Service,Repository,Model等解耦,這樣即便底層數據庫表進行了修改,也能夠不影響接口的使用)
    • 全部的轉換類都應該繼承自AppTransformer類

響應

注意 : 這裏討論的響應格式指的是應用業務相關的響應,由第三方提供的api接口的響應不歸入處理範圍(eg:laravel passport提供的響應,swagger提供的響應)

響應分類

  1. 成功類響應:http響應碼介於200~300。返回此類響應表示服務器完整處理了該請求,沒有未捕捉處理的異常或錯誤。(除了正常狀況,在業務邏輯處理失敗時,也會返回此類響應,同時會帶上相應的業務處理失敗信息
  2. 失敗類響應 : http響應碼不介於200~300。返回此類響應表示服務器拋出了未捕捉處理的異常或錯誤。

響應例子

成功類響應

1.業務邏輯處理成功
業務邏輯處理成功響應.png

2.業務邏輯處理失敗

業務邏輯處理失敗響應.png

結構如上圖所示:結構與業務邏輯處理成功是同樣。區別在於成功時的code爲0,失敗時則爲相應的錯誤碼,code的取值爲爲app\Common\Enum\ErrorCode.php中的業務級錯誤碼(見下面的錯誤碼)。

失敗類響應

失敗類響應.png
失敗響應的格式配置在文件config/api.php中(關鍵詞爲:errorFormat)。主要包括了message、errors、code、status_code、debug。有些信息在生產環境不會展現。

響應格式化處理的思路

響應格式化處理的大體思路:對特定的請求(對此類請求作標記)的處理結果,在返回給用戶時進行攔截(使用事件機制),對原有響應進行格式化處理。
響應的代碼:

  • App\Http\Middleware\BusinessFormatOutput : 路由中間件,在某些路由放置該中間件,則標記該請求,代表其響應須要進行格式化處理
  • App\Listeners\AddBusinessStatusToResponse : 事件handler,處理由dingo觸發的ResponseWasMorphed事件,對響應進行格式化處理
  • App\Http\Controllers\ApiController.php文件中的常量BusinessStatusHeader,經過響應中的header爲中介,將業務邏輯處理結果傳遞到2中的事件handler中,並最終構成格式化響應。

錯誤碼

錯誤碼相關的代碼文件爲:app\Common\Enum\ErrorCode.php
錯誤碼格式:A-BB-CCC

  • A : 表示錯誤級別,0表明成功,1表明系統級錯誤,2表明服務(業務)級錯誤;
  • B : 表示項目/模塊/分類;
  • C : 具體錯誤編號;

不一樣錯誤級別錯誤碼的使用:

  • 業務級錯誤碼用於表示業務處理結果。

    • Service層業務處理失敗,拋出BusinessException時使用業務級狀態碼
    • Controller層構造響應時,定義響應的業務處理結果,eg:return $this->response->array($validator->errors()->toArray())->withHeader(self::BusinessStatusHeader, [ErrorCode::BUSINESS_INVALID_PARAM, '業務處理結果信息']);
    • 用於日誌記錄(業務相關的日誌)
  • 系統級錯誤碼用於表示代碼運行異常。

    • 用於記錄系統性異常日誌,Controller、Service、Transformer、Repository、Model各個層皆可

注意:

  1. 錯誤碼文件不能重寫,如有新的錯誤碼,請按現有分類添加,不能刪除或修改舊的錯誤碼。

異常與異常處理

異常相關的代碼:app/Exceptions目錄。在應用代碼中,只能拋出BusinessException或者是SystemException。請不要拋出其餘的異常,不一樣異常經過異常的code來區分(code的定義在app/Common/Enum/ErrorCode.php)。

當業務邏輯執行失敗時,拋出BusinessException,常見可能狀況以下:

  • Controller層校驗輸入失敗,拋出BusinessException
  • Service層業務邏輯執行失敗,直接拋出BusinessException(如帳戶餘額不足,沒法轉帳)
  • Service層業務邏輯執行失敗(但沒有拋出異常,而是經過返回值指明執行失敗),則接受到該返回值的調用者拋出BusinessException

Controller必須捕捉BusinessException(所以即便拋出了BusinessException,依然要返回一個成功類響應(見上文)),並根據BusinessException的相應信息構造響應。建議全部Controller的action如下面的格式進行編寫。

public function add(Request $request, ReserveService $reserveService){
    try{//將全部的控制器邏輯放到try塊中
        $postData = $request->post();
 
        //校驗數據有效性
        /** @var \Illuminate\Validation\Validator $validator*/
        $validator = Validator::make($postData, [
            'orderName' => 'required',
            'reservePhone' => 'required',
        ]);
 
        if($validator->fails()){//校驗失敗
            new BusinessException(ErrorCode::BUSINESS_INVALID_PARAM, "", $validator->errors()->toArray());
        }
 
        $result = $reserveService->addReservation($postData);
        if(true === $result){
            //業務邏輯執行成功
            return $this->response->array([]);
        }else{
            //經過返回值指示業務邏輯執行失敗
            throw new BusinessException(ErrorCode::BUSINESS_BUSY);
        }
    } catch (BusinessException $e){//捕捉BusinessException,根據異常的信息構造響應,下面這段代碼能夠通用
        return $this->response->array($e->getExtra())
            ->withHeader(self::BUSINESS_STATUS_HEADER, [$e->getCode(), $e->getMessage()]);
    }
}

當發生底層系統異常時,拋出SystemException。沒有捕捉處理的SystemException會形成一個失敗類響應(見上文)。

日誌與預警

日誌組件與預警組件的存在是爲了更好的維護項目,及時處理bug。應該根據本身的須要添加相應的日誌組件和預警組件。

文檔

能夠選擇集成一個成熟的文檔工具,如swagger,blueprint等。

相關文章
相關標籤/搜索