我发现,我越是努力,就越发幸运。 — Thomas Jefferson

2.17.1 微服务

a pic

Martin Fowler(我喜欢和敬仰的大师)曾发表了上面这一段话。这段话也出现在了2015年QCon分享会上,并加了一张PPT“什么是微服务”加以说明。

a pic

里面提到了 微服务 这个概念,在PhalApi框架中即对应我们的Api接口服务层,只是我们不是称之为微服务,而是接口服务。不管何种说法,我们都应该关注里面提及到的这几点重要特质:

  • 小,且专注于做一件事情
  • 独立的进程中
  • 轻量级的通信机制
  • 松耦合、独立部署
    这里不过多地讨论微服务相关的分享,而是重温接口服务层Api与领域驱动和单元测试之间的关系,以及如何开发一个优雅、稳定又简单的接口。

2.17.2 层级调用的顺序

整体上讲根据从Api接口层、Domain领域层再到Model数据源层的顺序进行开发。

在开发过程中,需要注意不能 越层调用 也不能 逆向调用 ,即不能Api调用Model。而应该是 上层调用下层,或者同层级调用 ,也就是说,我们应该:

  • Api层调用Domain层
  • Domain层调用Domain层
  • Domain层调用Model层
  • Model层调用Model层
    如果用一张图来表示,则是:a pic

为了更明确调用的关系,以下调用是 错误 的:

  • 错误的做法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)创建一个接口服务类和声明函数

  1. //$ vim ./Vote/Api/Act.php
  2. <?php
  3. class Api_Act extends PhalApi_Api {
  4. public function joinIn() {
  5. }
  6. }

(2)配置接口参数规则

  1. <?php
  2. class Api_Act extends PhalApi_Api {
  3. public function getRules() {
  4. return array(
  5. 'joinIn' => array(
  6. 'teamName' => array('name' => 'team_name', 'require' => true, 'min' => 1, 'max' => 100),
  7. ),
  8. );
  9. }
  10. public function joinIn() {
  11. }
  12. }

(3)提供接口返回格式的注释

  1. <?php
  2. class Api_Act extends PhalApi_Api {
  3. public function getRules() {
  4. return array(
  5. 'joinIn' => array(
  6. 'teamName' => array('name' => 'team_name', 'require' => true, 'min' => 1, 'max' => 100),
  7. ),
  8. );
  9. }
  10. /**
  11. * 团队参赛接口
  12. *
  13. * @return int code 0,参赛成功;1,队名已存在
  14. * @return int team_id 新建的团队ID
  15. */
  16. public function joinIn() {
  17. }
  18. }

(4)在线查看一下效果

在完成上面的动作后,我们可以通过在线工具来看下实时的效果,在浏览打开后访问:

  1. http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.JoinIn

可以看到:a pic

到了这里,即使我们未完成接口的开发,也未提供更完善的接口文档,但接口客户端同学也可以根据这个在线的接口参数进行开发了。

2.17.4 在ATDD下讲述故事

我们一直推崇测试驱动开发,但在对于Api接口开发,更准确来说是ATDD,即:验收测试驱动开发(Acceptance Test Driven Development)。

在前面Domain层文档中,我们提到了Api层是讲述故事的场景。因此,为了验证我们的业务场景是否正确,我们应该事先编写好单元测试,以不断引导我们前往正确的目的地。

我们可以使用脚本来快速生成测试骨架:

  1. $ pwd
  2. $ /path/to/api.vote.phalapi.com/Vote/Tests/Api
  3. $ phalapi-buildtest ../../Api/Act.php Api_Act ../test_env.php > Api_Act_Test.php

然后,稍微修改完善测试场景:

  1. /**
  2. * @group testJoinIn
  3. */
  4. public function testJoinIn()
  5. {
  6. //Step 1. 构建请求URL
  7. $url = 'service=Act.JoinIn';
  8. $params = array(
  9. 'sign' => 'phalapi',
  10. 'team_name' => 'test team name',
  11. 'user_id' => '1',
  12. 'token' => '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731',
  13. );
  14. DI()->notorm->team->where('team_name', $params['team_name'])->delete();
  15. //Step 2. 执行请求
  16. $rs = PhalApi_Helper_TestRunner::go($url, $params);
  17. //var_dump($rs);
  18. //Step 3. 验证
  19. $this->assertNotEmpty($rs);
  20. $this->assertArrayHasKey('code', $rs);
  21. $this->assertArrayHasKey('team_id', $rs);
  22. $this->assertEquals(0, $rs['code']);
  23. $this->assertGreaterThan(0, $rs['team_id']);
  24. //create again
  25. $rs = PhalApi_Helper_TestRunner::go($url, $params);
  26. $this->assertEquals(1, $rs['code']);
  27. }

从上面测试的代码可以看出,我们先后进行了两次报名。明显地,第一次报名应该是成功的,第二次则应该提示不能重复报名。

单元测试的好处,不但在于可以引导我们做正确的事情,还可以提高我们的关注点,不致于在开发过程中被各种事务(如临时性的会议)打断后回来却不知刚才开发到哪了。然而,更多的是为后期的维护、扩展提供可验证的业务场景。这点是很重要的。因为每一个测试场景,都保存了对应场景的模拟信息,这样不仅仅在后面的扩展,还是突如其来的BUGFIXED,我们都可以快速证明我们的修改是正确的,至少不会影响到原来的业务流程。试想一下,如果原来可以下单支付的接口,突然被影响到而导致支付不成功,这是何等的损失!

我现在慢慢地,每当需要修改别人的代码时,我都会看下有没有对应的单元测试。如果没有,我会先补回,这样能增强我修改别人代码的信心。

2.17.5 精益接口开发

传统的接口开发,由于没有很好的分层结构,而且热衷于在一个文件里面完成绝大部分事情,最终导致了臃肿漫长的代码,也就是通常所说的意大利面条式的代码。

在PhalApi中,我们针对接口领域开发,提供了新的分层思想:ADM(Api - Domain - Model)。即便这样,如果项目在实际开发中,仍然使用原来的做法,纵使使用再好的接口开发框架,也还是会退化到原来的局面。

为了能让大家更为明确Api接口层的职责所在,我们建议:

(1)Api接口层应该做:

  • 应该:对用户登录态进行必要的检测
  • 应该:控制业务场景的主流程,创建领域业务实例,并进行调用
  • 应该:进行必要的日记纪录
  • 应该:返回接口结果

    (2)Api接口层不应该做:

  • 不应该:进行业务规则的处理或者计算

  • 不应该:关心数据是否使用缓存,或进行缓存相关的直接操作
  • 不应该:直接操作数据库
  • 不应该:将多个接口合并在一起
    在明确了上面应该做的和不应该做的,并且也完成了接口的定义,还有验收测序驱动开发的场景准备后,相信这时,即使是新手也可以编写出高质量的接口代码。因为他会受到约束,他知道他需要做什么,主要他按照限定的开发流程和约定稍加努力即可。

如果真的这样,相信我们也就慢慢能体会到 精益开发 的乐趣。

最后,让我们一起来看下上述团队参赛接口开发的代码实现:

  1. /**
  2. * 团队参赛接口
  3. *
  4. * @return int code 0,参赛成功;1,队名已存在
  5. * @return int team_id 新建的团队ID
  6. */
  7. public function joinIn() {
  8. $rs = array('code' => 0, 'team_id' => 0);
  9. DI()->userLite->check(true);
  10. $domain = new Domain_Team();
  11. if ($domain->isExists($this->teamName)) {
  12. $rs['code'] = 1;
  13. return $rs;
  14. }
  15. $teamId = $domain->joinIn($this->teamName);
  16. $rs['team_id'] = $teamId;
  17. return $rs;
  18. }

可以看出,上面的代码短小达意,简单清晰。

原文: https://www.phalapi.net/wikis/2-17.html