微服务:Api接口服务层
我发现,我越是努力,就越发幸运。 -- Thomas Jefferson
2.17.1 微服务
Martin Fowler(我喜欢和敬仰的大师)曾发表了上面这一段话。这段话也出现在了2015年QCon分享会上,并加了一张PPT“什么是微服务”加以说明。
里面提到了 微服务 这个概念,在PhalApi框架中即对应我们的Api接口服务层,只是我们不是称之为微服务,而是接口服务。
不管何种说法,我们都应该关注里面提及到的这几点重要特质:
- 小,且专注于做一件事情
- 独立的进程中
- 轻量级的通信机制
- 松耦合、独立部署
这里不过多地讨论微服务相关的分享,而是重温接口服务层Api与领域驱动和单元测试之间的关系,以及如何开发一个优雅、稳定又简单的接口。
2.17.2 层级调用的顺序
整体上讲根据从Api接口层、Domain领域层再到Model数据源层的顺序进行开发。
在开发过程中,需要注意不能 越层调用 也不能 逆向调用 ,即不能Api调用Model。而应该是 上层调用下层,或者同层级调用 ,也就是说,我们应该:
- Api层调用Domain层
- Domain层调用Domain层
- Domain层调用Model层
- Model层调用Model层
如果用一张图来表示,则是:
为了更明确调用的关系,以下调用是 错误 的:
- 错误的做法1:Api层直接调用Model层
- 错误的做法2: Domain层调用Api层,也不应用将Api层对象传递给Domain层
- 错误的做法3: Model层调用Domain层
这样的约定,便于我们形成统一的开发规范,降低学习维护成本。
比如需要添加缓存,我们知道应该定位到Model层数据源进行扩展;若发现业务规则处理不当,则应该进入Domain层探其究竟;如果需要对接口的参数进行调整,即使是新手也知道应该找到对应的Api文件进行改动。
2.17.3 接口服务的定义
现实项目开发过程中,绝大部分我们编写的接口是给别人使用的,或许给Android客户端同学使用,或者给iOS客户端同学使用,抑或提供给其他后台系统的同学使用。
为了提高并行开发的速度,我们不能等待接口完全开发完成后才提供接口文档,而且他们也不能忍受这么漫长的等待。
所以,客户端同学时常会问我们:什么时候可以提供接口文档?
我们提倡“接口先行”,如果有时不能很好地做到这一点(毕竟多变的需求促发多变的情境),我们可以快速提供接口的定义。
这有点像规约层对接口的定义一样,在PhalApi中定义一个接口,再具体一点即:
- 1、创建一个接口服务类和声明函数
- 2、配置接口参数规则
- 3、提供接口返回格式的注释
简单来说,就是创建一个类,写个函数,定义参数和返回结果。
下面以 开发实战3:一个简单的小型项目开发(奔跑吧兄弟投票活动) 中的团队参赛接口为例,说明这三步操作的过程。
(1)创建一个接口服务类和声明函数
//$ vim ./Vote/Api/Act.php
<?php
class Api_Act extends PhalApi_Api {
public function joinIn() {
}
}
(2)配置接口参数规则
<?php
class Api_Act extends PhalApi_Api {
public function getRules() {
return array(
'joinIn' => array(
'teamName' => array('name' => 'team_name', 'require' => true, 'min' => 1, 'max' => 100),
),
);
}
public function joinIn() {
}
}
(3)提供接口返回格式的注释
<?php
class Api_Act extends PhalApi_Api {
public function getRules() {
return array(
'joinIn' => array(
'teamName' => array('name' => 'team_name', 'require' => true, 'min' => 1, 'max' => 100),
),
);
}
/**
* 团队参赛接口
*
* @return int code 0,参赛成功;1,队名已存在
* @return int team_id 新建的团队ID
*/
public function joinIn() {
}
}
(4)在线查看一下效果
在完成上面的动作后,我们可以通过在线工具来看下实时的效果,在浏览打开后访问:
http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.JoinIn
可以看到:
到了这里,即使我们未完成接口的开发,也未提供更完善的接口文档,但接口客户端同学也可以根据这个在线的接口参数进行开发了。
2.17.4 在ATDD下讲述故事
我们一直推崇测试驱动开发,但在对于Api接口开发,更准确来说是ATDD,即:验收测试驱动开发(Acceptance Test Driven Development)。
在前面Domain层文档中,我们提到了Api层是讲述故事的场景。因此,为了验证我们的业务场景是否正确,我们应该事先编写好单元测试,以不断引导我们前往正确的目的地。
我们可以使用脚本来快速生成测试骨架:
$ pwd
$ /path/to/api.vote.phalapi.com/Vote/Tests/Api
$ phalapi-buildtest ../../Api/Act.php Api_Act ../test_env.php > Api_Act_Test.php
然后,稍微修改完善测试场景:
/**
* @group testJoinIn
*/
public function testJoinIn()
{
//Step 1. 构建请求URL
$url = 'service=Act.JoinIn';
$params = array(
'sign' => 'phalapi',
'team_name' => 'test team name',
'user_id' => '1',
'token' => '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731',
);
DI()->notorm->team->where('team_name', $params['team_name'])->delete();
//Step 2. 执行请求
$rs = PhalApiTestRunner::go($url, $params);
//var_dump($rs);
//Step 3. 验证
$this->assertNotEmpty($rs);
$this->assertArrayHasKey('code', $rs);
$this->assertArrayHasKey('team_id', $rs);
$this->assertEquals(0, $rs['code']);
$this->assertGreaterThan(0, $rs['team_id']);
//create again
$rs = PhalApiTestRunner::go($url, $params);
$this->assertEquals(1, $rs['code']);
}
从上面测试的代码可以看出,我们先后进行了两次报名。明显地,第一次报名应该是成功的,第二次则应该提示不能重复报名。
单元测试的好处,不但在于可以引导我们做正确的事情,还可以提高我们的关注点,不致于在开发过程中被各种事务(如临时性的会议)打断后回来却不知刚才开发到哪了。
然而,更多的是为后期的维护、扩展提供可验证的业务场景。这点是很重要的。因为每一个测试场景,都保存了对应场景的模拟信息,这样不仅仅在后面的扩展,还是突如其来的BUGFIXED,我们都可以快速证明我们的修改是正确的,至少不会影响到原来的业务流程。
试想一下,如果原来可以下单支付的接口,突然被影响到而导致支付不成功,这是何等的损失!
我现在慢慢地,每当需要修改别人的代码时,我都会看下有没有对应的单元测试。如果没有,我会先补回,这样能增强我修改别人代码的信心。
2.17.5 精益接口开发
传统的接口开发,由于没有很好的分层结构,而且热衷于在一个文件里面完成绝大部分事情,最终导致了臃肿漫长的代码,也就是通常所说的意大利面条式的代码。
在PhalApi中,我们针对接口领域开发,提供了新的分层思想:ADM(Api - Domain - Model)。
即便这样,如果项目在实际开发中,仍然使用原来的做法,纵使使用再好的接口开发框架,也还是会退化到原来的局面。
为了能让大家更为明确Api接口层的职责所在,我们建议:
(1)Api接口层应该做:
- 应该:对用户登录态进行必要的检测
- 应该:控制业务场景的主流程,创建领域业务实例,并进行调用
- 应该:进行必要的日志纪录
- 应该:返回接口结果
(2)Api接口层不应该做:
- 不应该:进行业务规则的处理或者计算
- 不应该:关心数据是否使用缓存,或进行缓存相关的直接操作
- 不应该:直接操作数据库
- 不应该:将多个接口合并在一起
在明确了上面应该做的和不应该做的,并且也完成了接口的定义,还有验收测序驱动开发的场景准备后,相信这时,即使是新手也可以编写出高质量的接口代码。
因为他会受到约束,他知道他需要做什么,主要他按照限定的开发流程和约定稍加努力即可。
如果真的这样,相信我们也就慢慢能体会到 精益开发 的乐趣。
最后,让我们一起来看下上述团队参赛接口开发的代码实现:
/**
* 团队参赛接口
*
* @return int code 0,参赛成功;1,队名已存在
* @return int team_id 新建的团队ID
*/
public function joinIn() {
$rs = array('code' => 0, 'team_id' => 0);
DI()->userLite->check(true);
$domain = new Domain_Team();
if ($domain->isExists($this->teamName)) {
$rs['code'] = 1;
return $rs;
}
$teamId = $domain->joinIn($this->teamName);
$rs['team_id'] = $teamId;
return $rs;
}
可以看出,上面的代码短小达意,简单清晰。