我需要拍砖 和 看见你们的意见,为团队少挖坑
场景:创建订单
实际流程:
终端调用(PC端、移动端APP、微信端、Web端)-->控制器 或 接口-->实际的业务处理-->控制器 或 接口-->终端做出相应处理(控制器可能是渲染对应页面; 接口返回 JSON数据)
业务处理类动作:
- 检查用户是否登陆
- 验证商品 ID、购买数量等参数
- 检查该商品是否处于上架中
- 检查该商品是否可以购买
- 各种检查...
- 创建订单
- 记录 Log
- 返回订单创建结果给调用者
- 创建失败:...
- 创建成功:[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
php
public 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:
javascript
if("用户未登录,请登录!" == 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:
php
public 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
php
kernel::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 应该已怎样规范来定义所代表的含义
再来看如下常用的两种方法定义:
public function create($params, & $msg)
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传递错误信息之后的代码
php
public 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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。