编者注:本文为历史博文归档;涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效,阅读时请注意时效性。

前言

许久未进行技术分享,现将曾经在老新浪论坛中使用过的架构技术进行改进和整理,总结成文。欢迎交流指正。

1. 为什么分层?

计算机领域的体系结构普遍采用分层设计。

从整体结构来看,从最底层的硬件往高层依次有:

  • 操作系统
  • 驱动程序
  • 运行库
  • 系统程序
  • 应用程序

从网络分层模型 OSI 来讲,由上至下为:

  • 应用层
  • 表示层
  • 会话层
  • 传输层
  • 网络层
  • 数据链路层
  • 物理层

当然,实际应用的 TCP/IP 协议分层并没有 OSI 标准这么复杂。

从 C 语言文件编写到生成可执行文件的过程来看:

  1. 预处理:展开后的 C 语言代码。
  2. 编译:成汇编语言(特定 CPU 体系结构的汇编语言源文件)。
  3. 汇编:生成目标文件(CPU 可执行的二进制指令机器码)。
  4. 链接:连接目标文件生成可执行文件(操作系统可以加载执行的二进制文件)。

这虽不算是软件的分层结构,但可以理解为一种通过分层来简化复杂问题的思想。那么 PHP 语言可以认为是建立在 C 语言之上的层——其解释器 Zend 引擎是用 C 语言实现。毕竟用 PHP 这样的脚本语言编写动态网页要比用 C 语言方便得多。

当然,还有我们最熟悉的 MVC 分层技术,后面会做详细介绍。

分层的好处想必大家都已经比较熟悉,这是一种“分而治之,大而化小”的思想。说到分层就不得不提模块,其实分层和模块是从两种维度来进行“分而治之”的方式:

  • 模块:是从横向维度将一个整体分割成若干个独立的部分,每个部分行使自己的职责,当然它们之间也可能有依赖关系,这通过其对外提供的服务来实现。如果将整个系统比做中国版图,那么模块方式将中国分为省、自治区等。
  • 分层:则是从纵向维度将一个整体从高至低划分为若干个独立的层,一个完整的服务由底层至上层,层层传递最终产出。

分层和模块可以同时运用。例如中国用模块方式分为省之后,每个省的行政机构利用分层方式来行使职责,从低到高有户、村、乡、区、市、省等等,每一层都向上一层汇报。

分层和模块会提高系统复杂度并影响效率(户不能直接向省汇报,而需要一级一级向上汇报),但是这样有利于系统的扩展和维护。每一层只需要关注自己提供的服务接口以及它下一层所提供的服务接口。试想一下,如果省需要接受来自市、区、乡、村等所有下级层的汇报,那些省干部会很头疼的。

2. HTTP 服务传统的三层架构 MVC

HTTP 服务中最经典的分层架构非 MVC 莫属了,几乎任何一个 PHP 开发框架都是支持 MVC 分层模式。此模式历史也比较悠久,是在上个世纪八十年代为编程语言 Smalltalk 设计的软件模式,至今已经被广泛引用。

这里引用了百度百科中的图片:

关于 MVC 的优点我不做介绍,搜索引擎中能找到大量相关资料。

本文的标题是《HTTP 服务七层架构技术探讨》,比 MVC 多出了四层,这样复杂的分层是否有必要呢?

关于这个问题可谓仁者见仁,智者见智。本人认为 MVC 分层粒度不够精细,当然你也可以继续坚持传统的三层,那么后文你也没必要看下去了。

那么为什么 MVC 分层不够精细呢?在我曾经使用开源框架的 MVC 模式的经验中发现,VC 层功能职责一般都很清晰稳定,但是 M 层却常常显得臃肿笨拙。

  • C 层(Controller):主要是负责整体流程控制。一般规范的架构中,流程都可以用一张或几张流程图画出,表明流程一般都是固定的。
  • V 层(View):主要是负责页面呈现。可能使用 Smarty 模板引擎,也可能是自带的模板引擎,显示的页面可能是 HTML、XML 或者 JSON。这些种类再多也都是可以度量的,所以 V 层也可以说是固定的。
  • M 层(Model):却关系到系统的业务逻辑。随着系统不断迭代更新,M 层中的内容也会不断演变。而这一层中也有很多复杂的处理,如文件读取、数据库查询、缓存策略、逻辑运算、数据加工、数据打包等等。

所以 MVC 三层模型中,M 层是还能再做细分的。当 M 层有一个更精细合理的分层方式之后,我们的业务逻辑演变过程会更加的得心应手。

3. 七层架构

由上面的介绍,我们对 MVC 中的 M 层再进行分层规划。我这里给出的是一种对 M 层分五层的方式,读者如果觉得五层太多或者太少,可以参考这个再进行规划。

原来的 M 层被分为:

  • A 层:Application(应用层)
  • B 层:Business(业务层)
  • C 层:Component(组件层)
  • D 层:Datadriver(数据驱动层)
  • S 层:Systemdriver(系统驱动层)

那么整个七层架构则为:

  1. Controller
  2. View
  3. Application
  4. Business
  5. Component
  6. Datadriver
  7. Systemdriver

结构图还是参考经典 MVC,将其中 M 层换成新的五层即可。

现在依次介绍这几个新的层:

3.1 Application(应用层)

应用层在最上面,其针对实际中的单个页面或者单个接口。Controller 通过 HTTP 请求地址中的参数找到对应的 Application,然后执行中指定的公共方法,比如 main(),然后应用就开始启动。

应用层的职责包括接受 HTTP 参数(一般是间接接受,比如从 request 对象中获取),调用 Business 层的特定业务,保存业务执行结果。这些结果最终会由 View 显示出来,当然是通过 Controller 协调。应用层是 M 层分解成五层之后最高的层,Controller 会与此层直接通信。

3.2 Business(业务层)

业务层在应用层之下,通常一个应用实例对应一个业务实例,而一个业务有可能为多个应用服务。业务是一个执行流,它通过执行一系列的操作来完成应用的需求。这些操作来自下层的组件层 Component,可能一个业务需要一个或者多个组件来完成一个完整的需求。

因为一个业务实例通常只对应一个功能,所以只有一个固定的方法会被上层的应用调用,比如 flow()。业务层的职责是帮应用层执行业务流并且有必要的时候返回数据给应用层,它会调用下层 Component 的方法。

3.3 Component(组件层)

从组件层开始和上面两层有一个本质的区别,组件层开始有了类库的概念。前面两层的实例通常只暴露一个特殊约定的公共的方法让上层调用,从这一层开始一个实例会提供多个方法给上层。

组件层通常和系统中一个角色对应。例如在博客系统中,博文是一个角色,用户是一个角色,那么就会有博文组件 BlogComponent,用户组件 UserComponent。每个角色都有对应的操作,例如博文和用户都可以添加、删除、修改。

需要注意组件层中不应该有任何数据读取的操作,数据读取是下层数据驱动层来做的。如果组件层从下层获取了数据,那么它的一个职责就是对数据进行加工。例如 BlogComponent 有一个方法是获取一个博文 getBlog($id),那么 getBlog() 方法中,从数据驱动层中取得了对应 id 的博文数据之后,需要对博文数据进行一定的处理,比如将博文中的 HTML 特殊标签过滤等等。组件层不关心数据的读取方式,但是会关心数据的结果,比如数据不存在或者数据已经过期。

3.4 Datadriver(数据驱动层)

数据驱动层的职责是为组件层提供源数据,此层关心数据的存取介质、存取方式等等。数据可能被存储在 DB、MC、Filesystem 或者远程的 HTTP 服务器上。

数据驱动层不关心数据的内容,只关心数据读取的操作结果。例如假设数据存在 DB 中,但是数据驱动层在执行数据库查询的时候出错了,那么需要在此层处理。假设数据存储在远程的 HTTP 服务器上,那么数据驱动层需要关心 HTTP 返回码是否为正确的 200 系列或者错误的 400、500 系列。哪怕 HTTP 请求返回了错误的数据实体,但是返回码为 200,那么数据驱动层也不关心,这种情况需要上层组件层来处理。

3.5 Systemdriver(系统驱动层)

系统驱动层是系统环境提供的数据访问实例。例如数据库服务的 Systemdriver 可能是一个 db handler,HTTP 服务的 Systemdriver 可能是一个 http handler,文件存储系统驱动层可能是一个 file handler。系统驱动层相对简单,这层可以和数据驱动层进行合并,其职责也较少,仅仅只是执行数据驱动层的数据访问指令。

通常情况下这五个层中,上层的实例数量比下层的实例数量要多,整体类似一个倒置的梯形:

在上图中一共有 6 个 Application,5 个 Business,4 个 Component,3 个 Datadriver,2 个 Systemdriver。

  • 每个 Application 都由一个 Business 为其服务。
  • 每个 Business 都服务一个或者多个 Application(B5 同时服务 A5、A6),都有一个或者多个 Component 为其服务。
  • 每个 Component 为一个或者多个 Business 服务,都有一个或者多个 Datadriver 为其服务。
  • 每个 Datadriver 为一个或者多个 Component 服务,都有一个或者多个 Systemdriver 为其服务。
  • 每个 Systemdriver 为一个或者多个 Datadriver 服务。

4. 七层架构运用

现在运用这样的架构来设计一个简单的博客系统,服务端用 PHP 语言实现。当然,架构是思想,不区分语言。

整个系统包含以下功能:

  1. 发布博文
  2. 修改博文
  3. 删除博文
  4. 评论博文
  5. 修改用户信息

要求每个功能都记录操作日志。

设计的数据存储包括:

  1. 博文数据表
  2. 用户数据表
  3. 评论数据表
  4. 日志(存文件系统)

在表结构设计的时候我们加入了一些冗余字段信息。例如在博文表中有评论数量字段 comment_nums,博文每被评论一次其值加 1,每删除一个评论其值减 1。用户数据表中我们添加了用户发布的博文数量字段 blog_nums,用户每发布一篇博文其值加 1,每删除一篇博文其值减 1。

下面设计分层:

应用层:一共有 5 个应用

  1. PostBlogApplication:发布博文
  2. UpdateBlogApplication:修改博文
  3. DeleteBlogApplication:删除博文
  4. CommentBlogApplication:评论博文
  5. UpdateUserApplication:修改用户信息

业务层:这 5 个应用分别有 5 个业务对其服务

  1. PostBlogBusiness:博文发布业务
  2. UpdateBlogBusiness:博文修改业务
  3. DeleteBlogBusiness:博文删除业务
  4. CommentBlogBusiness:博文评论业务
  5. UpdateUserBusiness:用户修改业务

组件层:系统一共有 4 个角色对应 4 个组件

  1. BlogComponent(博文组件)

    • postBlog():发布博文
    • deleteBlog():删除博文
    • updateBlog():修改博文
    • getBlog():获取博文内容
  2. CommentComponent(评论组件)

    • postComment():发布评论
    • deleteComment():删除评论
  3. UserComponent(用户组件)

    • updateUser():修改用户信息
  4. LogComponent(日志组件)

    • logMsg():记录日志信息

数据驱动层:和 4 个组件对应

  1. BlogDatadriver:DB 类型,提供 blog 的 select、insert、delete、update
  2. CommentDatadriver:DB 类型,提供 comment 的 select、insert、delete、update
  3. UserDatadriver:DB 类型,提供 user 的 select、insert、delete、update
  4. LogDatadriver:FS 类型,提供 file 的 read、write

系统驱动层:DB 类型和 FS 类型

  1. MySqlSystemdriver:DB 的 handler
  2. FileSystemdriver:FS 的 handler

现在以发布博文操作来介绍流程。假设接口地址为:http://www.xxxxx.com/postBlog

  1. Controller 通过重写规则发现其对应的 Application 为 PostBlogApplication,于是 PostBlogApplication 被实例化,并且其中的特殊方法 main() 会被自动调用。
  2. PostBlogApplication 需要 PostBlogBusiness 业务来完成博文发布操作,PostBlogBusiness 被实例化,并且其中的特殊方法 flow() 会被调用。
  3. 根据需求,发布博文的时候需要在博文表中插入一条博文,然后修改用户信息中的博文数量字段。那么 PostBlogBusiness 业务流就包括两个操作,这两个操作分别由 BlogComponent 中的 postBlog() 方法和 UserComponent 中的 updateUser() 方法来实现。其中前者往博文表中插入博文信息,后者将用户信息中的博文数量字段加 1。由于系统要求任何操作都需要记录日志,因此还有第三个操作就是记录日志,通过 LogComponentlogMsg() 方法实现。那么 PostBlogBusiness 业务流一共包括了三个操作,分别由三个组件来完成。
  4. 下面就需要分别考虑上面三个组件的下层调用了:

    • BlogComponentpostBlog() 调用 BlogDatadriver 的 insert 相关方法来插入博文数据,BlogDatadriver 是 DB 类型,因此通过 MySqlSystemdriver 来实现。
    • UserComponentupdateUser() 调用 UserDatadriver 的 update 相关方法来实现博文数量更新,UserDatadriver 也是 DB 类型,因此也通过 MySqlSystemdriver 来实现。
    • LogComponentlogMsg() 调用 LogDatadriver 的 write 相关方法,LogDatadriver 是 FS 类型,因此通过 FileSystemdriver 来实现。
  5. 三个组件的操作都执行成功后,PostBlogBusiness 告诉 PostBlogApplication 博文发布成功,然后 PostBlogApplication 通过 Controller 来调用 View 相关的方法显示执行结果。

其他几个操作流程读者可以举一反三,这里不再多介绍了。

那么现在我们通过几个系统功能的演变用例来看看这个分层带来的益处。

用例 1:为了方便对日志的管理,现在希望能够将日志存储在 DB 中而不是 FS 中。

  • 解决方法:这是对数据存储的改造,我们知道应该从数据驱动层入手。日志功能是由日志组件 LogComponent 实现的,其中 LogComponentlogMsg() 方法调用 LogDatadriver 来存日志。我们将 LogDatadriver 由 FS 类型改造成 DB 类型,接口方法保持不变,这样很快就完成了改造。

用例 2:新增需求 - 用户 A 可以将一篇博文转移给另外一个用户 B。

  • 解决方法

    1. 首先这个新需求对应了一个新的应用,于是我们新增了一个 SendBlogApplication
    2. 需要有一个业务完成操作,新增业务为 SendBlogBusiness
    3. 考虑转移一篇博文涉及到的操作:

      • 将博文表中对应的用户 ID 字段由 A 的 ID 切换到 B 的 ID。
      • A 用户的博文数量减 1。
      • B 用户的博文数量加 1。
        这三个操作需要两个组件来完成,这两个组件我们系统中已经有了,BlogComponentupdateBlog() 完成操作 1,UserComponentupdateUser() 完成操作 2、3。

从用例 2 可以看出,当新增了这么一个需求的时候,我们在应用层和业务层添加了实例,组件层以下都不变。这是因为现在的组件层已经能够满足新的业务的需求,当我们现有组件无法满足新的业务需求的时候,我们则需要对组件层做修改。

通过这两个简单的用例我们发现,我们对系统的修改要么可以很明确的确定在哪些层,要么就是从上层组件往下层进行,操作起来很方便。

5. 小结

这种纵向的分层和横向的模块结合起来,能让整个系统的结构清晰流畅。在本人设计的一款框架里面,原生支持这样的分层架构和模块,使用者只需要按照同一个模式简单的操作,层之间的接口和协议已经由框架本身约定好。框架还不够完善,仅供内部使用,暂时不发布了。

说明:本文涉及的技术架构探讨基于早期 PHP 开发环境(约 2012 年左右),文中提到的部分外链图片可能已失效,部分设计模式(如 MVC 变体)在当今微服务或云原生架构下可能有不同的演进形式,阅读时请结合当前技术栈酌情参考。