今天給同事封裝了一個接口,說起接口封裝的事情,其實其實很有的聊。很多時候,說一個服務好,一個服務爛,實際上都是在吐槽服務隊外暴露的接口好壞。不管什么語言,封裝接口,抽象起來,就是由一個函數名,若干個參數,若干個返回值組成的。封裝的好壞,就在這幾個上面。
函數名
首先是函數名。函數名的好壞很明顯,我的觀點,是否簡單,不重復。比如在一個User類中你封裝一個方法,叫做findUser。我就覺得很啰嗦了。你使用的時候會這樣使用
User::findUser($id);
那又是何必呢?為什不直接叫做find呢?
User::find($id);
我記得前段時間在網上還看到一篇文章,你見過哪些奇葩的代碼。其中就有一些有趣的函數名。在我的視角看來,下面的函數名都很奇葩:
function weizhi() // 中文拼音
function getuserinfo() // 單詞和單詞沒用大小寫分割
function getUserIsEnable() // 明明是bool判斷卻用get開頭
基本上,我們選擇使用 動詞 或者 動詞+名詞 或者 動詞+名詞 + 副詞
比如
function find()
function getUser()
function getUserByName()
我覺得這些都是很符合人性的函數名。
參數
一句話, 參數盡量不要封裝。。。盡量不要太多。。。
盡量不要封裝就是,能隊外暴露的細節越多,用戶使用成本越低,比如,根據地理位置獲取地址的函數
// 里面的$coord是一個數組['lat','lng']
function getCityByCoord($coord)
就不如
function getCityByCoord($lat, $lng)
還有不要太多就是如果你參數個數超過5個,就該考慮封裝了。封裝的時候,我習慣會把一些“不重要的”,“不常用的”封裝成一個參數,並且設置這個參數默認值。
// 這里的conditions 可以設置表列名,只能用等號 ['class' => 1]
function getUsers($offset, $limit, $sort, $conditions = [])
返回值
這個返回值就很有的說了。首先遇到的問題是,返回值是否是返回數組。這個問題讓我想起了在剛接觸php的時候,那時候剛從c#轉過來,對接手的項目的一個函數返回值包含什么一直不理解。問了同事,他的回復是,你調用一下就可以知道他們返回什么了。
反正吧,對於php的返回值,我的觀點就是,如果你的項目在追求的是快,而且開發人數也不多,那么,你就可以使用數組來做交互。如果你的項目追求的是工程化,模塊和模塊之間的交互需要人與人的溝通,那么,盡量定義好對象。使用對象進行交互。事實上,像laravel這類追求工程化的框架,你在實現和別人交互的接口的時候,盡量傳遞的是Model,或者Collection比較好。
比如
// in Service
// return: LocationModel
public function findByName($name){
return LocationModel::where('name', trim($name))->first();
}
異常和錯誤
接口函數定義好了,可不是就結束了,這個函數是否會拋出異常?是否會返回錯誤?
對於錯誤和異常的理解,我的理解是:
- 異常是不能被兼容處理的
- 錯誤是希望被兼容處理的
我記得在上一個項目,我強烈建議團隊小伙伴們在封裝對外的soa的sdk的時候,使用的方式是如此:
list($code, $data) = UserService::getUserByName($name);
if ($code) {
// 處理對應的錯誤
}
php原本的返回值只有一個對象,這里使用list拆成兩個對象,一個是code,代表返回值的錯誤信息,一個是data,代表如果沒有錯誤的話,返回的結構。這個接口,我們在內部做了try_catch,不會拋出異常,所有的信息都以錯誤碼的形式返回。
如果在內部try_catch捕獲到了異常,則返回的code會是500。
其實使用異常還是錯誤碼處理錯誤都是可以的。錯誤碼是寫程序的時候最早使用的方法,但是異常機制出現后,各個語言都傾向於使用異常處理錯誤了。
就php而言,是建議使用異常處理的。它本身的內部也定義了一堆的build-in 異常。
- BadFunctionCallException
- BadMethodCallException
- DomainException
- InvalidArgumentException
- LengthException
- LogicException
- OutOfBoundsException
- OutOfRangeException
- OverflowException
- RangeException
- RuntimeException
- UnderflowException
- UnexpectedValueException
體會下下面兩段代碼,分別使用異常和錯誤碼處理
const PARAM_ERROR = 100;
const AGE_TOO_BIG = 200;
const INSERT_ERROR = 300;
class UserException extends \Exception{
}
function insertUserByField($name, $code, $age) {
$db = db::connect();
if(empty($name) || empty($code) || empty($age)) {
throw new \UserException(PARAM_ERROR);
}
if ($age > 15) {
throw new \UserException(AGE_TOO_BIG);
}
$ret = $db->insert('user')->create(compact('name', 'code', 'page'));
if (empty($ret)) {
throw new \UserException(INSERT_ERROR);
}
return $ret;
}
// 使用
try{
$user = $userService->insertUserByField('foo', 291212, 34);
} catch(\UserException $e) {
switch($e->getCode()):
case PARAM_ERROR:
//
case AGE_TOO_BIG:
//
case INSERT_ERROR:
//
default:
//
} catch(\Exception $e) {
//
}
和
const OK = 500;
const PARAM_ERROR = 100;
const AGE_TOO_BIG = 200;
const INSERT_ERROR = 300;
const INNNER_ERROR = 500;
function insertUserByField($name, $code, $age) {
try {
$db = db::connect();
if(empty($name) || empty($code) || empty($age)) {
return [PARAM_ERROR, null];
}
if ($age > 15) {
return [AGE_TOO_BIG, null];
}
$ret = $db->insert('user')->create(compact('name', 'code', 'page'));
if (empty($ret)) {
return [INSERT_ERROR, null];
}
return [OK, $ret];
} catch (\Exception $e) {
// do log
return [INNNER_ERROR, null];
}
}
// 使用
list($code, $user) = $userService->insertUserByField('foo', 291212, 34);
if ($code) {
switch $code {
case PARAM_ERROR:
//
case AGE_TOO_BIG:
//
case INSERT_ERROR:
//
default:
//
}
}
我認為,golang中的錯誤處理機制給了我們很好的示范。它有個error機制代表錯誤,panic機制代表異常。
func getUserByName(name string) (int, error) {
if len(name) == 0 {
return 0, errors.New("param error")
}
//
}
data, err := getUserByName(name)
if err != nil {
....
}
這里的err代表getUserByName的時候有可能返回錯誤。它也是期望(甚至於強制)調用方處理各種error。但是它並不保證這個函數不會發生panic,一旦發生panic,整個系統也會崩潰。你需要使用recover來捕獲。
如果把golang的這種做法應用在php中,上面的例子可能就會變成:
const OK = 500;
const PARAM_ERROR = 100;
const AGE_TOO_BIG = 200;
const INSERT_ERROR = 300;
const INNNER_ERROR = 500;
// 這里對可能出現的exception就不需要管了,只處理希望上層處理的“錯誤”
function insertUserByField($name, $code, $age) {
$db = db::connect();
if(empty($name) || empty($code) || empty($age)) {
return [PARAM_ERROR, null];
}
if ($age > 15) {
return [AGE_TOO_BIG, null];
}
$ret = $db->insert('user')->create(compact('name', 'code', 'page'));
if (empty($ret)) {
return [INSERT_ERROR, null];
}
return [OK, $ret];
}
// 如果你有框架的話,這里的try catch就可以在框架統一捕獲了。
list($code, $user) = $userService->insertUserByField('foo', 291212, 34);
if ($code) {
switch $code {
case PARAM_ERROR:
//
case AGE_TOO_BIG:
//
case INSERT_ERROR:
//
default:
//
}
}
關於異常和錯誤這塊,不同的語言,不同的人有不同的使用習慣,我的看法,golang中對異常和錯誤的處理機制是最好的。將兩者分別對待。
但是在php中,如果需要有個“銀彈”說法的話:盡量使用異常來處理。
如果硬要問為什么?基本上,有兩個原因:
1 異常的堆棧信息比錯誤碼豐富
2 異常是默認出錯,在錯誤中找“可修復”的錯誤。錯誤碼是默認正常,在正常中找“可修復”的錯誤。前者更為保守。