第8章 PhalApi完美诠释

在软件工程这一学科和行业里,关于软件工程的解说有很多。有人说开发是一门艺术;有人说开发是一种技艺;也有人说开发是一门哲学。但个人认同,也更倾向从实用主义和理性的角度去理解。

例如一个框架,我们之所以认为它好是因为我们发现这个框架遵循了编程规范、适当地使用了设计模式、巧妙地结合了设计原则、有着稳定的依赖、代码复杂度低、并且有着很高代码覆盖率的单元测试等等。也就是说,好的框架都是可以被解释的。既然可以被解释、被量化,也就可以被学习、参考和借鉴。

PhalApi可以说,是一个设计巧妙的框架。对外而言,它是容易学习和掌握的,并且对开发者友好,因为提供了可视化的、自动生成的在线文档,辅助的脚本命令,多种语言的SDK开发包。对内而言,如果深入其内部,你会发现,它简单、优雅、恰到好处。

8.1 核心设计讲解

软件开发里有三层模型,分别是:概念模型、设计模型和代码模型。PhalApi是一个专注于接口服务领域开发的框架,这是它的概念模型。至于PhalApi的设计模型是怎样的,说实话在,最初是非常模糊的。因为不同的设计模型,将会决定PhalApi以后成长的道路。可以说,如何界定PhalApi的设计模型,这是非常关键的一步。

8.1.1 共性和可变性分析回顾

关于共性和可变性分析(Commonality and Variability Analysis,简称CVA),在《设计模式解析》一书中有着非常到位的讲解。 CVA是一种很容易的理念,按我的理解即: 抽离共性、隔离变化 。有点类似易经里面的“变”与“不变”。诚然,在过去的教育中(包括大学在内的),对于软件开发都着重谈论面向对象开发,即OOD,以致于很多人都对面向对象开发产生了很大的误解。而这种误解所带来的实际情况就是: 我们都在进行面向对象开发,但却是标准呆板的面向对象开发,缺少生气,缺少活力 。

所以,在对PhalApi进行核心设计时,我们进行了一次又一次地酝酿、尝试、思考。我们在思考:这些功能是否真的会在实际项目中被使用?开发人员是否可能很好地进行扩展?此种决策是否便于单元测试、从思路上减少代码异味?我们谨记敏捷开发,不过度设计。但我们也确实需要一种思想上的指导。正好,我们看到了共性和可变性分析 。它仿佛像黑夜里的一 盏明灯,照亮了前进的道路,一刹那,豁然开朗。

CVA和三种视角、抽象类之间的关系

如图8-1,三种视角是指:概念视角、规约视角和实现视角。其中,共性分析包括概念视角和规约视角,可变性分析则包括规约视角和实现视角。这应该怎么理解呢? 规约

图8-1 摘自《设计模式解析》的CVA说明

很简单,概念视角表达的是高层业务概念,对应着某一特定领域业务。例如,PhalApi的概念视角即它身身的定位:一个专注于接口服务领域开发的框架。概念视角需要体现概念的完整性,是属于公共固化的部分,一般不会轻易改变。除非一个游戏已不再是游戏,一个电商平台已不再是电商平台,这时它的概念视角才能改变。也就是说,除非本质改变,否则概念视角不会轻易动摇。

规约视角主要负责制定系统内部的接口、对象之间的协作关系,从而确定客户端如何进行调用。之所以它是属于共性分析,也是属于可变性分析,是因为纵使对于同一个领域业务,不同的人,不同的团队,不同的时期,会有不同的实现方案。没有说绝对哪一种设计最好,只能说不同的设计,做事方式不同,定位不同,偏向的人群不一样。就好比如操作系统,有Windows,有Linux,还有Mac等。正所谓“条条道路通罗马”。但一旦确定好接口之间协作的关系,在很长一段时间内将会保持不变,因为系统需要保持向前兼容。所以说,规约视角一开始是可变性分析,因为那时充满着不确定性,到了后期,随着系统的成熟性和流行成熟,会逐渐演进为共性分析。这是从时间维度对规约视角的解读。另一方面,对于高层的接口规约,往往是共性分析,低层的接口规约则属于可变性分析。

最后一层是实现视角,负责具体功能的实现。这一层,尤其应考虑可测试性,并且遵循“高内聚、低耦合”。

在这种理念的指导下,我们会更愿意将接口领域开发过程的共性抽离统一起来,而可变性部分的则可以由开发人员根据不同的项目情况进行定制化实现。

不稳定性与抽象度分布

除了常谈及到的“低耦合、高内聚”外,在对代码进行静态分析和衡量其可维护度时,还有一个值得注意的值,即:不稳定性的度量。不稳定性的计算公式是:

I =\frac{Ce}{Ce+Ca}

其中,Ce表示离心耦合,Ca表示向心耦合。不稳定性的值界于0和1之间,值越大,表示越不稳定。即当不稳定性为1时,表示最不稳定;反之,为0时,表示最稳定。不稳定性可用于计算包或者类的不稳定性。稳定依赖原则的规则(SDP),则是指包之间的依赖应该朝着稳定的方向:不稳定的包应该依赖于更稳定的包。

因此从宏观上,我们的代码结构,从上层到下层,应该向着稳定的方向递增,也就是说越底层应越稳定。进一步,再结合图8-2 不稳定性与抽象分布图,PhalApi框架的代码应该大部分分布在抽象稳定区以实现框架高层的建设、少部分分布在具体不稳定区以提供一些公共基础的功能 。

图8-2 不稳定性与抽象分布图

架构有两个重要的特点,分别是:最高层次的系统分解,和系统中不轻易改变的决定。而这两个特点,前者对应图8-2的左上解区域,后者则对应图8-2的右下角区域。有效进行最高层次的系统分解,能帮助架构设计者找到深藏在特定领域背后的本质。而不轻易改变的决定越少,后续升级则更轻松,因为不必要过多考虑向前兼容。

SOLID原则在框架中的应用

SOLID原则是指单一职责原则、开放-封闭原则、里氏替换原则、接口分离原则和依赖倒置原则这五大原则。这五大原则对于框架设计来说,是非常具有参考和指导意义的。让我们来看下,它们是如何在PhalApi中得以体现的。

  • 单一职责原则 这是PhalApi一直都坚持和格守的原则,我们坚持短而美的风格, 致力于编写优雅的代码、编写人容易理解的代码 。一个概念,一个类,不做过多的聚合。

  • 开放-封闭原则 首先,在进行接口服务开发过程中,当需要新增一个接口服务时是开放的,对已有的响应调用流程是封闭的。即开发人员只需要实现新接口逻辑即可,不需要改动其他过程的调用。因此在OCP原则的指导下,我们通过结合工厂方法封装了对接口服务的初始化和调用。

  • 依赖倒置原则 PhalApi框架,最大的特色莫过于它提供了一种如何快速进行接口服务开发的机制,但它不强制你使用不必要的功能,甚至还鼓励你通过它来尝试研发自己的框架 。更进一步,PhalApi引入了新颖明确的概念,一如服务。我们把客户端调用的接口称之为接口服务,把服务端用到的资源称之为资源服务。对于后者,PhalApi提供了灵活的DI依赖注入机制,以支持各项目定制化的开发。这是一个开放的世界,注定会多样多彩。

8.12 解读主流程

经过前面的一番思索,以及借助共性和可变性、不稳定性和抽象图,以及SOLID原则,PhalApi最初基本的核心执行流程已得以慢慢明朗。有意思的是,PhalApi的主流程,从确定之初,时至今日,已历三载,目前还是和当初一样。这里,扼要说明一下PhalApi框架中接口请求背后的核心执行流程,以便大家洞明其中的原理。

图8-3 PhalApi的核心时序图

如图8-3所示,在PhalApi中,一个接口的请求处理,只要分为两个环节: 接口服务的初始化和接口服务的调用。

接口服务的初始化

在Web Service中,往往需要对服务进行注册发布后,才能开放请求。这里免去这一层,但遵循创建和使用分离的原则,我们将接口服务的初始化进行了封装,以便可以统一进行初始化、异常处理和一些权限ACL的控制,甚至接口访问的统计等操作,更为重要的是接口开发人员可以进行无绪开发,而不需要过多知道如何合法创建接口服务。

在1.2. 步骤中,UML时序图中的::generateService()表示对静态函数的调用,即对应代码:

$a = PhalApi_ApiFactory::generateService();

假设我们这次请求的服务为:?service=Demo.DoSth,可以看到,创建了一个指定接口类实例(此接口类须继承于PhalApi_Api基类),并以变量a返回该实例。 正确创建接口服务a后,则会进行接口的初始化,其中有接口参数规则的解析和注册了过滤器服务后的检测操作。 当这一系列的操作都成功执行后,将会得到一个接口服务实例a返回。

至此,接口服务的创建完成。

接口服务的调用

在完成复杂的接口类实例创建工作后,客从事服务端开发的开发客户端只需要简单调用需要进行的操作即可。而这一块,则需要接口项目具体开发实现,也是我们项目级的核心部分。

在获取接口服务的背后,我们建议结合领域驱动设计的理念,对项目代码进行这样的层级划分:

  • Api接口层:用于接收参数并响应接口的请求;
  • Domain领域层:用于处理复杂的领域业务逻辑,保证规则只出现一次;
  • Model数据源层:更广义上的Model层,提供数据来源,不限于数据库。

最后,是我们客户端关心的返回格式。 默认情况下,我们都是以JSON格式返回的,但仍然可以轻松支持其他格式的返回,如JSONP、XML等。只需要简单地开发实现,然后重新注册即可。

至此,接口服务的调用完毕。

8.1.3 UML静态类结构

介绍完PhalApi的核心执行主流程后,再来看下PhalApi的UML静态类结构。前者是动态的执行,后者是静态的关系。

图8-4 PhalApi的UML静态类结构图

从图8-4所折射出PhalApi的核心架构、层级和代码是如此的简单明了、统一规范。至少我是这么认为的,也是一直这样努力的。 从中可以看出,中间红色部分的DI处于汇点位置,提供各种资源服务的定位、创建、管理和提供。而左上角的代码示例则表达本系统框架运行的主流程: 创建一个接口实例,运行响应。右上角黄色部分则为多变的接口应用开发的代码,这里特意罗列了两组接口,意在表明可以在此框架下挂靠多套接口。

最下面是接口开发过程中所用到的各种基础设施和技术,如日记、配置读取、缓存、加密、请求和响应等。同样,除各应用项目中形式多变的接口开发外,这块的底层技术亦支撑不一而足的需求。因为,PhalApi只是作了共性的抽离,即提供一级抽象且稳定的接口或者抽象类,以约定规约视角中接口的函数签名,不作过多的具体实现。同时以DI作为辅助,支持快速扩展。

8.2 性能剖析

这一章,我们将通过两方面来剖析PhalApi的性能。一方面是针对框架内部的白盒性能分析,这将借助于Xhprof工具来完成;另一方面则是针对框架对外的黑盒压力测试,这将借助于Autobench来完成。

测试环境配置为:

  • 阿里云服务器ECS(CPU:1核 内存:1 GB 宽带:1Mbps)
  • 操作系统:CentOS release 6.7 (Final)
  • Nginx 1.8.0
  • PHP 5.3.5

Xhprof性能分析和Ab压测的接口服务皆为默认接口服务?service=Default.Index,框架版本为PhalApi 1.4.1。

8.2.1 Xhprof性能报告

使用XHprof对PhalApi 1.4.1 版本进行性能剖析,经过多次分析并取各自最优值,对关键性能指标的如表8-1所示。

表8-1 PhalApi 1.4.1的性能概要

性能指标 PhalApi v1.4.1
Total Incl. Wall Time (microsec) 5,355 microsecs
Total Incl. CPU (microsecs) 4,999 microsecs
Total Incl. MemUse (bytes) 774,208 bytes
Total Incl. PeakMemUse (bytes) 783,376 bytes
Number of Function Calls 636

就上面报告的数据可以看出,请求默认接口服务时,总的执行时间是5,355毫秒,即约0.005秒;峰值内存是783,376 bytes,即约765KB;函数调用的次数是636次。

其中,Top 10 耗时操作主要是: 文件类的加载,请求与响应的初始化,判断文件是否存在等。但这些已在更新的版本中得以优化。

表8-2 PhalApi 1.4.1的Top 10 耗时操作

Function Name Calls Calls% Excl. Wall Time (microsec) EWall%
PhalApi_Loader::loadClass 15 2.40% 450 8.40%
PhalApi_Loader::loadClass@1 8 1.30% 249 4.60%
load::PhalApi/Request.php 1 0.20% 245 4.60%
PhalApi_Request::getAllHeaders 1 0.20% 223 4.20%
load::PhalApi/DI.php 1 0.20% 201 3.80%
run_init::Public/init.php 1 0.20% 173 3.20%
load::PhalApi/Api.php 1 0.20% 166 3.10%
load::PhalApi/Response.php 1 0.20% 156 2.90%
load::Cache/File.php 1 0.20% 151 2.80%
file_exists 27 4.20% 141 2.60%

对应的高清版可视化图表如下:

图8-5 PhalApi 1.4.1 Xhprof性能调用图

上图的红色部分是最耗时的部分,对应类文件的自动加载。而黄色部分则是整个耗时路径的链路。

8.2.2 Ab基准测试

这里,使用Autobench进行基准测试,压测脚本为:

#!/bin/bash

if [ $# -eq 0 ]; then
    echo "Usage: $0 <host> <uri>"
    echo ""
    exit
fi

DM=$1
URL=$2

autobench \
    --single_host \
    --host1=$DM \
    --port1=80 \
    --uri1=$URL \
    --low_rate=1 \
    --high_rate=50 \
    --rate_step=1 \
    --num_call=1 \
    --num_conn=50 \
    --timeout=5 \
    --file ./$DM.tsv

压测的结果如下图所示。

图8-6 PhalApi 1.4.1 的Ab压测结果

在并发量为50以内时,PhalApi 1.4.1 的响应时间约为20 ms。

8.3 静态代码质量

PhalApi的性能和单元测试都相当优秀,那它的静态代码质量如何呢?我们可以从Sonar分析报告和PHPMetric分析报告中找到答案。

8.3.1 Sonar分析报告

这里主要使用了开源中国码云代码托管平台提供的质量分析服务,最终会得到以下这样的Sonar常规代码分析报告。

图8-7 PhalApi 1.4.1 的Sonar分析报告

从报告中可以看到,PhalApi共有2267行代码,注释率为36%,技术债务为1天5小时。这些是较为关键的衡量指标。

我曾经见过技术债务为几百天的项目,代码多达近十万行,可以说是一个巨大的焦油坑。而PhalApi在2015年时的代码行数是1400行,技术债务为1天1小时。代码规模虽然增长了约60%,但技术债务只是相应的增长了一点,约16%。由此可见,PhalApi的静态代码质量是不错的。最起码,它敢于将自己最真实、最深处的一面,毫无保留地公开于众。我想,这不仅是一种对自身代码的自信,更是对开源社区的负责精神。

8.3.2 PHPMetrics分析报告

另一个值得推荐的工具是PHPMetrics,它也是一个用于分析PHP静态代码质量的工具。PHPMetrics提供了更多细致的度量数据。

PHPMetrics的官网:http://www.phpmetrics.org/

同样,对PhalApi框架的核心代码进行分析,可以得到以下这样的报告数据。

LOC
    Lines of code                               4144
    Logical lines of code                       2242
    Comment lines of code                       1907
    Average volume                              279.35
    Average comment weight                      34
    Average intelligent content                 34
    Logical lines of code by class              39
    Logical lines of code by method             10

Object oriented programming
    Classes                                     58
    Interface                                   7
    Methods                                     229
    Methods by class                            3.95
    Lack of cohesion of methods                 1.23
    Average afferent coupling                   1.32
    Average efferent coupling                   1.73
    Average instability                         0.59

Complexity
    Average Cyclomatic complexity by class      3.94
    Average Relative system complexity          15.64
    Average Difficulty                          6.97

Bugs
    Average bugs by class                       0.09
    Average defects by class (Kan)              0.43

Violations
    Critical                                    0
    Error                                       4
    Warning                                     6
    Information                                 0

PHPMetrics还提供了精美的可视化报告。

图8-8 PhalApi 1.4.1 的PHPMetrics报告局部

而对比早期的PHPMetrics报告,也可以发现PhalApi的静态代码质量虽然稍微变差了一点,但整体上看还是不错的。

图8-9 PhalApi早期的PHPMetrics报告局部

8.4 追求极致的单元测试

PhalApi框架,本身拥有完善的测试套件。当需要执行这些自动化测试套件时,可进入/path/to/PhalApi/PhalApi/Tests单元测试目录,然后执行以下命令即可运行单元测试:

$ phpunit

自PhalApi诞生以来,就一直坚持着测试驱动开发的最佳实践,并且将可测试性纳入到框架的核心设计。从早期开始,PhalApi的核心代码单元测试桥北率就高达90%以上。

图8-10 PhalApi早期的测试覆盖率情况

对于一个框架,它所被应用的领域是广泛的。就PhalApi而言,它可能用于开发接口服务,可能用于开发命令行应用,可能用于开发后台计划任务,也有可能是用于执行单元测试。不管何种应用场景,PhalApi都应能支持。这就要求,框架的初始化与调度应该是分离的,在完成初始化工作后不应紧接着强制进行调度。因为不同的场景,调度方式不一样。如果过早地确定调度方式,势必将限制后续其他场景的调度机制的实现。就好比如,在处理接口响应请求时,PhalApi需要返回输出的是JSON格式的HTTP内容,而在命令行模式时则需要输出的是面向终端的文本内容。

另一方面,如果单元测试不能真实环境那样共享同一个初始化过程的话,除了需要重复维护两份初始化过程外,在单元测试时的成功通过很有可能会给开发人员造成一种假象——正式初始化过程也是没问题的。

值得庆幸的是,PhalApi都考虑到了以上两个方面。因此,PhalApi自身的单元测试是容易搭建的,从而推导使用PhalApi进行开发的项目的单元测试体系也是容易搭建的,并且能与正式初始化保持一致性,因为它们是共享的。

除此之外,关于单元测试,PhalApi还有两点也是值得注意的。一个是自动生成测试骨架的脚本命令,另一个是仿真。前者无疑提升了开发的效率,后者则给了开发人员一致的开发体验。这里说的仿真主要是针对接口类的单元测试与真实接口请求之间的效仿。

例如,在真实的情况下,如果需要请求默认接口服务,可以在浏览器中输入地址并访问:

http://dev.phalapi.net/?service=Default.Index&username=dogstar

注意到,我们在后面还加了一个请求参数&username=dogstar。在单元测试中,也可以类似地,

$url = 'service=Default.Index&username=dogstar';
$rs = PhalApi_Helper_TestRunner::go($url);

可以发现,你可以直接把真实请求的URL作为测试时的模拟请求链接,反之亦然,也可以把测试时的模拟请求链接作为真实的URL使用。这种方式,对开发人员来说是友好的,一致的。

8.5 DI与扩展类库

当使用一个开源框架时,我们既希望它能提供强大的功能,但矛盾的是,我们又害怕它强大背后所隐藏的复杂性,从而导致学习成本过高、出现问题时又难以驾驭。 而在这里,在PhalApi这里,这一切都是这么简单,简单地又如此明了。

PhalApi是一个开放式的框架,当它提供的已有的功能无法满足你项目的实际开发需要时,你可通过两种途径来获得项目的扩展和定制能力。一种是单个类粒度的,即DI依赖注入;一种是更大规模的,偏向于包粒度的,即扩展类库。下面分别再来讲解之。

8.5.1 再谈DI依赖注入

DI依赖注入是PhalApi框架中一个至关重要的概念,理解DI以及其在PhalApi扮演的角色,对于高效使用PhalApi大有禆益。

鸭子类型

"当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。"

在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的。同样,注册在DI容器中的资源服务,关注的不是注册对象的类型本身,而是这些资源服务是如何使用的。例如,回顾缓存在PhalApi中的使用示例:

// 设置
DI()->cache->set('thisYear', 2015, 600);

// 获取,输出:2015
echo DI()->cache->get('thisYear');

// 删除
DI()->cache->delete('thisYear');

只要注册的DI()->cache对象,具备以上这些操作,它都能很好地正常工作,而不管是它是PhalApi框架内本身的实现类,还是项目定制扩展的自定义类类型,甚至是有着同样实现接口的其他类型。

约定编程

当然,为了保持一致性和透明性,我们对于公共的资源服务定义了其接口类型,或者抽取了层基类或抽象类。这不仅能复用已有的代码,还能够明确引导开发人员需要实现的接口有哪些。例如,上述缓存的接口定义为:

interface PhalApi_Cache {

    /**
     * 设置缓存
     * 
     * @param string $key 缓存key
     * @param mixed $value 缓存的内容
     * @param int $expire 缓存有效时间,单位秒,非时间戳
     */
    public function set($key, $value, $expire = 600);

    /**
     * 读取缓存
     * 
     * @param string $key 缓存key
     * @return mixed 失败情况下返回NULL
     */
    public function get($key);

    /**
     * 删除缓存
     * 
     * @param string $key
     */
    public function delete($key);
}

当需要自己的缓存机制时,可实现此PhalApi_Cache缓存接口即可。实现后且重新注册DI()->cache资源服务后,PhalApi不会再进行类型的检测的。因为PhalApi提倡的是约定编程。即我们约定DI()->cache资源服务应该实现了PhalApi_Cache接口,或者至少是此鸭子类型,而不做过多的检测和限制。这样,不仅提升了框架的性能(不用每次都校验资源服务的类型),又减少了框架的复杂度(省略了异常处理的机制)。

打个比方,在农村,每逢过年过节,村民都会虔诚地去宗庙进行祈福礼拜。相邻的村庄,有很多不同的村民,即使在同一村庄内又会有自自的宗庙。宗庙是开放式的,但很少、甚至从来没有村民会去非自己的宗庙进行礼拜。这就是一种俗成的约定。而在城市里,连出入同一座大厦,每个人每天频繁地进出都要打一次卡,以证明你是属于这里的,有着被肯定的身份。哪怕你和保安都有打照面,但如果哪天你没有带上允许你出入的门卡时,保安还是会按流程将你“拒于门外”的。同样,PhalApi是一个开放式的框架,它不会做过多地强制,而是倾向一种约定俗成的风范,从而把更大的自由留给开发人员。因为它相信开发人员。

惯例优于配置,配置优于实现

Ruby是一门优秀的编程语言,而且也处处体现了惯例优于配置的理念。可以通过简单的示例代码来体会这一约定。

如普通的操作:

def say_hello(name)
    # 先打声招呼
    puts "hello #{name}"
end

返回布尔类型的操作,方法名后面需要有个问号。

def more_money?()
    # 假设有点钱
    return true
end

一些危险的、可能会抛出异常的操作,方法名后面需要有个叹号。

def borrow_me!(money)
    #  假设钱不够
    raise "Not enough momey!"
end

为了串联进来,假设有这么一个用户故事:你好某某人,有钱吗?借一点给我! 则对应的代码片段是:

say_hello '某某人'

borrow_me! 100 if more_money?

那么运行的效果类似如下:

hello 某某人
./test.rb:13:in `borrow_me!': Not enough momey! (RuntimeError)

可以说,拥有惯例的开发团队,会有更高效的合作以及更为畅快的沟通。因为大家都能快速明白简明代码所体现的意图和目的,不存在混淆和错乱。如果缺少开发语言的特性支持,或者所在的开发团队缺少约定编程的氛围,可以退而求其次,采用配置优于实现的做法。

小结

当需要定制专属的缓存实现时,按前面的说明,可先实现在规约视角约定的接口PhalApi_Cache,如:

class MyCache_File implements PhalApi_Cache {
    public function set($key, $value, $expire = 600) {
        //...
    }

    public function get($key) {
        //...
    }

    public function delete($key) {
        //...
    }
}

随后,实现自己的功能后,只需要简单地在入口文件重新注册即可。如:

DI()->cache = new MyCache_File();

最后,另人兴奋的是,原来全部的调用代码都不需要改动,即可享受后期调整升级后的新功能!完全避免了曾经那种“牵一发而动全身”的痛苦。定制开发出来的实现类,还可以跨越业务在其他项目中共用。这不正是我们常常所说的代码重用吗?而如今,我们很优雅地做到了。

然而,我们在实际开发中收获到的远远不是代码重用这么简单,而是一种更好的开发实践。因为通过DI使得创建和使用分离,所以我们可以让高级的开发同学实现服务功能的开发,然后再提供给普通的开发同学使用,新手亦然,因为对他们来说:会用就行。当然,对于高级的同学,还应该遵循开发的最佳实践,坚持单元测试,以保证我们提供了可靠的接口(广义上的接口,非HTTP请求的接口)给我们的“客户端”使用。

作为一个框架,我们应当以发散的方式去设计;但为了能为应用提供可用的功能,我们又应当以收敛的方式去实现。如果框架提供的功能不足以满足大部分主流的业务场景,那么至少需要提供可扩展的空间。

正是出于这样的考虑,我们虔诚地引入了DI依赖注入。

8.5.2 可重用的扩展类库

当重要的粒度更大,不再仅仅是某单个类时,我们则需要以包的形式来提供。将一系列的功能操作封装成包,或者用PhalApi的话说,封装成一个可重用的扩展类库。PhalApi扩展类库也有其约定。下面简单回顾一下。

扩展类库的使用规范

当使用一个新的扩展类库时,对于项目开发人员来说,通过需要三步曲:安装、配置注册、使用。

  • 安装 即安装扩展类库的源代码,可直接下载对应的源代码,并放置在项目的Library目录下。
  • 配置注册 进行适当的扩展类库的配置,如果需要,在DI容器中注册此扩展类库。
  • 使用 根据扩展类库的操作说明,在项目中进行使用。

这些扩展类库都是直接可集成到PhalApi项目里,然后直接使用的。它们通常是特定业务无关的,更多是可重用的基础设施,负责某一技术的实现,例如邮件发送、自定义路由、Excel操作等。又或者是某一独立可重用的基础业务功能,如用户模块。重用已有的扩展类库,或者将一组可重用的功能封装成扩展类库,对于提升开发产品簇项目的速度非常有帮助。有人曾经评价说,扩展类库是PhalApi又一重要而明智的决策。

扩展类库的开发规范

开发扩展类库的职责,不局限于PhalApi开源团队,也不局限于开源社区的广大的开发贡献者,而是把这种权利赋予给了每一位开发人员。因此,为了统一扩展类库的风格、便于用户更容易使用,这里建议:

  • 代码 统一放置在Library目录下,一个扩展包一个目录,尽量以Lite.php文件为入口类,遵循PEAR包命名规范。
  • 配置 统一放置在DI()->config->get('app.扩展包名')中,避免配置冲突。
  • 文档 统一提供对扩展类库的功能、安装和配置、使用示例以及运行效果进行说明的文档。

代码、配置和文档都是扩展类库中不可或缺的重要元素,这与前面的三步曲也是一一对应的。

8.6 PhalApi内省

在我们不断维护、演进PhalApi框架的同时,我们也在使用这个框架进行了很多项目的开发,与此同时也在阅读各方面的书籍以获得更深层次的理解。在这样实践、思考、再设计的不断反馈迭代后,我们看到了PhalApi确实在某方面表现得出色。

但一个负责任的开源框架,应该也明确指出它的不足。这里,我们将PhalApi开发中的不足罗列如下,希望为你进行框架设计或者对PhalApi的使用有更好的理解。

  • 1、接口结果中msg应该改名为error

我们推荐的接口返回格式为:

{
    "ret": 200,
    "data": {
        "code": 0,   //对操作码进行说明
        ....         //更多结果的说明
        "msg": ""
    },
    "msg": ""
}

显然,上面两个msg字段,会给开发团队带来困惑或混淆。更好是应该把最外层的msg改成error更为贴切,因为只有错误时此字段才有效。

但基于前期的大量文档说明,此外层的ret、data、msg三个字段已约定。所以,只能从应用层的msg进行重命名,如tips。

  • 2、对NotORM中limit操作的错误优化

前期,由于没有深刻留意MySql中OFFSET关键字的作用,导致了做了一些不精确的优化。可注意以下的微妙区别:

limit 5 OFFSET 10   #从第10个位置开始,查询前5个

limit 5, 10         #从第5个位置开始,查询前10个

但重点考虑到如果修复这个之前犯下的错误,会对项目升级后有很大的冲动。可预料的故障有:”升级后,首页列表无任何数据显示“和“升级后,列表数据过多导致App加载崩溃”。

最后,出于对已在开发或已上线项目的保护和承诺向前兼容的原则,我们不得不保留了这个污点。所以,当对底层进行改动时,须确保已透彻理解各操作的微妙区别。

  • 3、对数据库操作封装的欠缺

一直以来,PhalApi对数据库支持这块都是比较欠缺的。所以我们使用了NotORM。但为了能更好把NotORM与PhalApi整合,将它调整成更适合我们的使用方式,我们又为NotORM提供了一个封装层,类似代理。

然而,这会为新手入门这个框架造成一定的迷惑。因为,这有两套操作数据库的区别。但他们一开始不能很好地理解这样的区别,以及这样设计的初衷。

坦白来说,PhalApi对于数据库这块不是好的设计,但好在它可以使用并正常地工作。

本章小结

在这一章,讲述了PhalApi在研发之初的设计思想,通过结合共性与可变性分析、不稳定性的度量以及抽象关系,在“专注于接口服务领域的开发框架”这一高层概念下,我们看到了PhalApi动态的核心时序图和整体的UML静态类结构。动态时序图对于理解系统中各个对象的协作关系和执行顺序有很大的帮助,而表态类结构则对于建立框架的整体设计模型有着重要的指导意义。这两者缺一不可。

使用Xhprof工具对PhalApi的默认接口服务进行性能剖析,可以发现,执行的总时间为5,355毫秒,内存峰值约为765KB。而在使用Autobench进行基准测试时,并发量为50以内时,PhalApi 1.4.1 的响应时间约为20 ms。

自诞生以来,PhalApi就一直维护着完善的自动化测试套件,并坚持着较高的单元测试覆盖率,普遍情况下高达90%以上。

PhalApi是一个开放式的框架,当它提供的已有的功能无法满足你项目的实际开发需要时,你可通过两种途径来获得项目的扩展和定制能力。一种是针对单个类粒度的DI依赖注入;一种是更大规模的、偏向于包粒度的扩展类库。

最后,我们还探讨了一下PhalApi设计中存在的不足,以便给读者提供理全面的参考。

Fork me on GitHub