代码改变世界

说说接口封装

2017-03-09 15:36  轩脉刃  阅读(9394)  评论(3编辑  收藏

今天给同事封装了一个接口,说起接口封装的事情,其实其实很有的聊。很多时候,说一个服务好,一个服务烂,实际上都是在吐槽服务队外暴露的接口好坏。不管什么语言,封装接口,抽象起来,就是由一个函数名,若干个参数,若干个返回值组成的。封装的好坏,就在这几个上面。

函数名

首先是函数名。函数名的好坏很明显,我的观点,是否简单,不重复。比如在一个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 异常是默认出错,在错误中找“可修复”的错误。错误码是默认正常,在正常中找“可修复”的错误。前者更为保守。