今天给同事封装了一个接口,说起接口封装的事情,其实其实很有的聊。很多时候,说一个服务好,一个服务烂,实际上都是在吐槽服务队外暴露的接口好坏。不管什么语言,封装接口,抽象起来,就是由一个函数名,若干个参数,若干个返回值组成的。封装的好坏,就在这几个上面。
函数名
首先是函数名。函数名的好坏很明显,我的观点,是否简单,不重复。比如在一个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: LocationModelpublic 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 异常是默认出错,在错误中找“可修复”的错误。错误码是默认正常,在正常中找“可修复”的错误。前者更为保守。