ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
*** _我发现,我越是努力,就越发幸运。 -- Thomas Jefferson_ ##2.17.1 微服务 ![a pic](http://7qnay5.com1.z0.glb.clouddn.com/qq_20150607110405.jpg) Martin Fowler(我喜欢和敬仰的大师)曾发表了上面这一段话。这段话也出现在了2015年QCon分享会上,并加了一张PPT“什么是微服务”加以说明。 ![a pic](http://7qnay5.com1.z0.glb.clouddn.com/qcon_20150607.png) 里面提到了 **微服务** 这个概念,在PhalApi框架中即对应我们的Api接口服务层,只是我们不是称之为微服务,而是接口服务。 不管何种说法,我们都应该关注里面提及到的这几点重要特质: + 小,且专注于做一件事情 + 独立的进程中 + 轻量级的通信机制 + 松耦合、独立部署 这里不过多地讨论微服务相关的分享,而是重温接口服务层Api与领域驱动和单元测试之间的关系,以及如何开发一个优雅、稳定又简单的接口。 ##2.17.2 层级调用的顺序 整体上讲根据从Api接口层、Domain领域层再到Model数据源层的顺序进行开发。 在开发过程中,需要注意不能 **越层调用** 也不能 **逆向调用** ,即不能Api调用Model。而应该是 **上层调用下层,或者同层级调用** ,也就是说,我们应该: + Api层调用Domain层 + Domain层调用Domain层 + Domain层调用Model层 + Model层调用Model层 如果用一张图来表示,则是: ![a pic](http://7qnay5.com1.z0.glb.clouddn.com/201506071124.png) 为了更明确调用的关系,以下调用是 **错误** 的: + 错误的做法1:Api层直接调用Model层 + 错误的做法2: Domain层调用Api层,也不应用将Api层对象传递给Domain层 + 错误的做法3: Model层调用Domain层 这样的约定,便于我们形成统一的开发规范,降低学习维护成本。 比如需要添加缓存,我们知道应该定位到Model层数据源进行扩展;若发现业务规则处理不当,则应该进入Domain层探其究竟;如果需要对接口的参数进行调整,即使是新手也知道应该找到对应的Api文件进行改动。 ##2.17.3 接口服务的定义 现实项目开发过程中,绝大部分我们编写的接口是给别人使用的,或许给Android客户端同学使用,或者给iOS客户端同学使用,抑或提供给其他后台系统的同学使用。 为了提高并行开发的速度,我们不能等待接口完全开发完成后才提供接口文档,而且他们也不能忍受这么漫长的等待。 所以,客户端同学时常会问我们:什么时候可以提供接口文档? 我们提倡“接口先行”,如果有时不能很好地做到这一点(毕竟多变的需求促发多变的情境),我们可以快速提供接口的定义。 这有点像规约层对接口的定义一样,在PhalApi中定义一个接口,再具体一点即: + 1、创建一个接口服务类和声明函数 + 2、配置接口参数规则 + 3、提供接口返回格式的注释 简单来说,就是创建一个类,写个函数,定义参数和返回结果。 下面以 [开发实战3:一个简单的小型项目开发(奔跑吧兄弟投票活动)](http://git.oschina.net/dogstar/PhalApi-Demo-Vote) 中的团队参赛接口为例,说明这三步操作的过程。 ###(1)创建一个接口服务类和声明函数 ```javascript //$ vim ./Vote/Api/Act.php <?php class Api_Act extends PhalApi_Api { public function joinIn() { } } ``` ###(2)配置接口参数规则 ```javascript <?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)提供接口返回格式的注释 ```javascript <?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 ``` 可以看到: ![a pic](http://7qnay5.com1.z0.glb.clouddn.com/201506071302.jpg) 到了这里,即使我们未完成接口的开发,也未提供更完善的接口文档,但接口客户端同学也可以根据这个在线的接口参数进行开发了。 ##2.17.4 在ATDD下讲述故事 我们一直推崇测试驱动开发,但在对于Api接口开发,更准确来说是ATDD,即:验收测试驱动开发(Acceptance Test Driven Development)。 在前面Domain层文档中,我们提到了Api层是讲述故事的场景。因此,为了验证我们的业务场景是否正确,我们应该事先编写好单元测试,以不断引导我们前往正确的目的地。 我们可以使用脚本来快速生成测试骨架: ```javascript $ pwd $ /path/to/api.vote.phalapi.com/Vote/Tests/Api $ phalapi-buildtest ../../Api/Act.php Api_Act ../test_env.php > Api_Act_Test.php ``` 然后,稍微修改完善测试场景: ```javascript /** * @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接口层不应该做: + 不应该:进行业务规则的处理或者计算 + 不应该:关心数据是否使用缓存,或进行缓存相关的直接操作 + 不应该:直接操作数据库 + 不应该:将多个接口合并在一起 在明确了上面应该做的和不应该做的,并且也完成了接口的定义,还有验收测序驱动开发的场景准备后,相信这时,即使是新手也可以编写出高质量的接口代码。 因为他会受到约束,他知道他需要做什么,主要他按照限定的开发流程和约定稍加努力即可。 如果真的这样,相信我们也就慢慢能体会到 **精益开发** 的乐趣。 最后,让我们一起来看下上述团队参赛接口开发的代码实现: ```javascript /** * 团队参赛接口 * * @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; } ``` 可以看出,上面的代码短小达意,简单清晰。