第02课:技术架构松耦合

第02课:技术架构松耦合

概述

技术架构(此处我们单指作为应用系统基础的技术平台)对于研发维护多个软件产品的企业并不陌生。它试图解决企业内部技术复用、提升研发效率、规范研发质量的问题。

对于技术复用层面,除非是架构类型相差很大的软件产品(如企业信息管理系统与大数据分析系统),否则绝大多数公司更倾向于使用已有的技术框架来开发新的产品。这可以大大降低初期的技术成本。对于已经熟悉的框架,也不存在过多的学习成本(API 层面)和适应成本(规范层面),有助于开发人员快速投入开发工作。

由于开发人员已经非常熟悉当前的技术框架,再加上针对相似的产品有丰富的面向业务的封装组件,这些组件通常易用、业务切合紧密,已经经过许多项目检验,非常健壮,这会大大提升开发人员的工作效率。

此外,一个成熟的技术框架,一定在代码规范层面定义了良好的约束,使得代码更易维护。使用已有框架的好处就是,这些好的约束可以继承过来,开发人员已经熟悉了这些约束,不会有任何不适。

以上都是一个通用的技术框架的好处,而且从中我们不难发现一个技术框架所包含的内容。

  • 对于各种选型框架的集成。如 Spring MVC 与 Apache Shiro、MyBatis、Thymeleaf、Activiti 等的集成。
  • 针对各种选型框架,提供相关的业务封装组件。如常见 CRUD 的封装、JSP 标签或者 JS 组件的封装、图表的封装。
  • 提供软件开发规范及约束。如各层 API 的命名、配置文件的命名、参数命名等。

这些都是比较常见的一个技术框架所包含的内容。但是在构建技术框架时,我们会面临与构建一个业务系统同样的问题,而这些问题却并不总是按照业务系统的构建思想来看待它。

比如模块化,笔者接触到许多公司,对于技术框架还是以一种比较原始的方式来进行管理,即便他们对待业务系统时,习惯性的考虑如何划分子系统和模块。

本文中,笔者尝试结合业务系统以及部分成熟框架的模块化方案,来探讨一下与松耦合技术框架相关的内容。

技术架构起步

当开始搭建一个通用的技术架构时,它最常见的结构可能如下图所示:

enter image description here

这是一个集成了很多第三方框架的技术平台,它可能已经满足了很多业务系统的开发需求。

但是作为一个公司内部的通用平台,显然它需要做的不止这些。因为我们除了要在技术框架集成层面做到复用,以避免每个项目重复这部分工作外,还要尽可能的提升开发效率。而提升开发效率通常有两种途径:

  • 封装各种组件,减少功能开发中的重复性工作。如 JSP 标签库、JS 组件库、通用数据访问组件等。
  • 提供手脚架,自动生成部分代码。最常见的是针对 ORM 自动生成持久层代码,当然也可以针对几种常见的页面布局,提供自动生成代码的机制,甚至对于诸如对象增删改查这种常见的管理功能,生成从前端到后端的全部代码。

对于技术重复性工作,可以通过手脚架来解决,而对于功能重复性工作,则应通过封装各种组件来处理,以提升代码的可复用以及可维护性(但是需要保持封装组件的可扩展性,以便应对定制化需求)。

注解:所谓“技术重复性”,指的是那些技术实现上相同或相似,但是具体数据不同的工作场景。如为不同的两张数据表生成 ORM 映射。所谓“功能重复性”,则是指在功能实现上相似的工作场景。如尽管是展现数据不同的两张表格,但都要设置分页条数、表头样式、单元格编辑器等。

因此,进一步完善的一个技术框架如下图所示:

enter image description here

在 Ruby On Rails 刚诞生那几年,在 Java 平台,也出现过几个手脚架项目,如 AppFuse(2016 年已停止维护)、Spring ROO 等。作为 Web 应用快速开发框架,它们的特点是,集成了基本的 Web 应用所需的技术(当然要比上图中的少很多),提供了从前端 JSP 至后端 ORM 的自动化生成。换句话说,不需要编写一行代码,即可实现对某个对象的管理功能。它们基本符合一个 Web 开发框架(或者成为平台)的定义,但是考虑到其适用人群,它们都是尽可能只做通用性的集成。但是作为企业开发平台,却往往包含对一些特殊框架的集成处理。

除此之外,对于带有手脚架的开发框架,还要考虑如何将手脚架代码与通用组件分离,避免手脚架代码与所开发项目代码耦合,因为它们只是辅助开发的组件,并不需要包含到发布包中。

这样的一个带有手脚架的开发框架,可能在一段时间内会满足大多数项目的快速开发,如果你幸运的没有遇到以下问题。

随之而来的问题

对于企业开发平台,最初,它可能只包含仅有的几个基础功能,如容器、数据访问、事务、安全认证、MVC 以及模板引擎。但是随着业务系统功能需求的增加或者其所支撑的业务系统数量的增加,这个技术框架会越来越复杂,集成各种各样的技术组件。

如某个业务系统需要支持消息处理,那么便在框架中引入了 Apache ActiveMQ。如系统新增了批处理的需求,便又增加了 Spring Batch 的支持。如某个系统需要存储大量图片,那么我们就要考虑分布式文件系统,如 HDFS、Fastdfs。

随着技术集成的日积月累,最终的样子(也许还会更复杂)会像上图中所示,它成了一个大而全的平台。也就是说从技术需求角度考虑,它是相对完善的。但是它却面临一个严重的问题——如何松耦合。

它对于公司内所用到的技术无所不包,这个庞然大物不但厚重而且不稳定,稍不留神就会出现不可预料的缺陷。比如,也许一个业务系统并未使用工作流,但是却因为缺少了某个工作流的配置(也许是个非常简单的配置,但是每个系统却都要配置一下,无论需不需要),而导致启动失败。

这种匪夷所思的问题对于业务系统开发人员来说是极度崩溃的,他们无法处理这些莫名其妙的问题,只能再反馈给平台的维护者。这就与测试人员发现一个需求的变更竟然导致一个毫不相关的功能出现致命缺陷一样,简直让人无法理解。

还有就是各种依赖冲突问题,会发现由于 Spring 依赖的日志框架与 Hibernate 的版本不同,导致项目无法启动或者日志输出异常。

也许有人觉得这种情况不可想象,基础框架怎么可能会糟糕成这个样子,可现实就是还有很多公司仍采用这种原始的方式进行基础框架搭建。可能是团队技术能力的问题,也可能是公司研发投入成本不足。

不仅如此,我们还会面临另一个问题。也许不同的项目在少数几个组件的选型要求上会有所区别。如对缓存的选择,是 EhCache 还是 Memcached,具体到应用场景,有时候我们确实很难一刀切的规定只能用其中某一种。如果强制规定,要么是人为的增加了简单应用的复杂度,要么是降低了复杂应用的质量。这个时候,更好的方式可能是两种方案都集成,但是由项目具体选择使用哪一种。

如何解决

很明显,针对上面描述的问题。首先,多余的框架不应包含到项目中来。也就是说,如果我的项目没有批处理的需求,那么就不应该有 Spring Batch 的相关包,更不应该有与它相关的配置。其次,要支持在同类型(当然是有限的几种)框架中选择一个最适合当前项目的可选项。

可以先来看第二个问题,这个相对比较简单。我们不妨看一下 Spring,Spring 最早集中于提供 IoC 的解决方案,因此对于一些成熟的第三方框架,它会提供相关的集成方案,以便直接在实际项目中使用。

而且 Spring 对于同类型的框架,提供了风格一致的集成 API。也就是说,基本上不需要了解各种第三方框架的 API,而是通过一套类似的 Spring API 来使用这些框架。如针对数据访问层提供的模板类,无论是 Hibernate、JDBC 还是 Redis,它们的 API 都非常相似,易学易用。而且 Spring ORM 同时提供了对 Hibernate(多个版本并存)、Jdo、JPA 的支持,Spring Data 更进一步提供了对多种数据存储平台的支持,如 RDBMS、NoSQL 等。除了模板类外,Spring 对于功能相同、实现不同的各种组件还提供了一致的工厂类,如各种远程访问工厂。在实际项目中,我们可以任意选择其中的一种实现,而且使用配置极其相似,即便将来需要替换,成本也不会很高。

对于每种框架的集成代码,Spring 的处理方式也不相同,我们可以视情况而定。如 Spring ORM 中,所有框架的集成都放到了一个包中,这样无论你是否使用 Hibernate,项目中都会包含这些类。而 Spring Data 的处理方式则更清晰一些,它对于每种集成方案拆分为一个包,你只引用需要的 JAR 包即可。因为集成代码相对第三方框架的代码毕竟数量要少得多,所以并不是主要问题。如果你的集成代码只有几个类,完全没必要单独发布为一个包,而如果你的集成代码相对数量比较多,则可以考虑按集成框架进行拆分(当然,数量并不是是否拆分的唯一因素)。

所以,对于这个问题,一个关键点是,对于同类框架如何提供风格一致的 API,以便业务系统开发人员易于学习使用、减少迁移/变更成本。

对于第一个问题,解决起来则要复杂一些。在 Spring Boot 等出现之前,更多的解决方式则是对平台按照集成的各种框架进行模块化拆分。

我们继续拿 Spring 作为例子(毫不夸张的说,Spring 可以作为 Java 平台的一款教科书式的框架),它以 IoC 作为载体,将整个框架划分为多个模块,如 MVC、ORM、JMS、JMX 等等。你可以选择只将其中的某几个模块添加到你的项目中。同时,还需要添加这几个模块的依赖包。如果采用 Maven 或者 Gradle 等进行项目依赖管理,这个过程会简单的多。例如 Maven2,它支持传递性依赖,这样我们项目在引用这个模块时,就不必再关心它依赖哪些第三方包了,否则这将是一件比较折磨人的事情,因为你不可能总是记得清楚编译阶段依赖哪些包,运行阶段依赖哪些包,除非你已经在项目环境中开发了一个简单的功能并运行成功。

模块化拆分加上传递性依赖,可以初步的实现技术框架中各组件的松耦合管理。当然,这种松耦合是指使用上的,而框架发布还是需要统一进行,否则容易导致依赖的第三方框架版本冲突的问题。

通过这种方式进行管理的技术框架架构如下图所示(技术集成我们只是列举了很少一部分作为示例,以下图例同):

enter image description here

当然,我们说,这只是框架演进的开始,下面继续。

借鉴 Spring Boot 的思想

通过诸如 Maven 等管理工具,我们可以方便的对技术框架进行模块化管理及使用,但是这种组织形式还是以单个 JAR 包为主,但有时候粒度相对大一些反而更方便。

为什么这么说呢?因为项目中使用时基本上不是以单个模块进行添加的。比如,技术框架以“Spring MVC+模板引擎”的方式提供 Web 开发支持。此时,对于 Spring MVC 的集成和对于模板的集成需要拆分为两个模块,因为我们可能提供多种可选的引擎。而在实际项目开发中,则更希望指定一种组合式的依赖,而不是分别指定 Spring MVC 和模板引擎。

很多用过 Spring Boot 的人,都说它是“傻瓜式”的。这是因为它屏蔽了绝大部分的集成细节,通过优秀的依赖管理,使得模块化添加非常自然便捷,不必担心依赖冲突等问题,更不必逐项添加你所依赖的 Spring 模块。

只需要在项目的 pom.xml(以 Maven 为例)文件中添加各种 starter 即可(如 spring-boot-starter-web)。如果使用 springsource 提供的 STS 作为开发工具,那么可以直接使用该 IDE 创建 Spring Boot 项目。

enter image description here

上图是通过 STS 创建一个 Spring Boot 项目时的可选的 starter,可以说它涵盖了非常广泛的、各种用途的第三方框架的集成。

在 Spring Boot 中,这些 starter 也是一个 Jar 包,它包含了一个 pom.xml 文件,声明了其所依赖的 Spring 模块、第三方框架及其版本。下图是 spring-boot-starter-thymeleaf 包含的 pom.xml,它依赖 spring-boot-starter 和 spring-boot-starter-web 两个 starter 和 thymeleaf 包。

enter image description here

使用时只需要简单选择自己希望使用的 starter 即可,完全不需要关心它的依赖问题。也就是说,通过这种方式,我们可以灵活组装自己的基础技术框架。

而且在特定的 Spring Boot 版本内,第三方框架的版本肯定是统一的,不必担心存在版本冲突的问题。

按照这种思路可以在自己的技术框架中,将完成特定功能的一组模块定义为一个单独的 starter(没有想到更好的名称,暂时也使用这个称谓),这样只需要在项目的 pom.xml 文件中添加该 starter 依赖,而不需要添加任何模块或者第三方框架的依赖。如下图所示:

enter image description here

由图可知,我们的技术框架分为以下三层。

  • 第一层:以第三方框架为基础;
  • 第二层:对于各种集成方案进行模块化;
  • 第三层:同时进一步将模块进行依赖整合,便于项目引用。

我们的集成代码完全集中在第二层,而第三层仅仅是出于依赖管理的需要。

我们的 starter 可以结合架构类型等来划分,如 Web 应用、SOA 应用、批处理应用等等。而 Web 应用则包含了我们要使用的 MVC 框架、模板引擎、持久化框架等。如果支持对模板引擎或者持久化框架的选型,那么可以针对 Web 应用提供多个 starter,如 thymeleaf-jpa-starter、freemarker-mybatis-starter。

在这种情况下,我们的技术框架完全由 Maven 进行管理(可以搭建 Maven 私服),不必提供一个全集的技术框架发布包。新建项目时,按照包含的应用类型添加对应的 starter 即可。

静态文件的管理

对于技术架构部分,我们前面谈到的几乎都是如何有效的对第三方框架和集成代码(服务端)进行管理,而对于企业级开发框架,还忽视了一点,就是对于静态文件的管理,如第三方 JS 库、公司封装的 JS 库、CSS 框架等。显然,这些是无法按照前面的方案进行管理的。但是,尽管如此,最终的使用方式还是希望像前面讲解的一样简单,而不是手动复制文件和目录。这时候可以通过 Maven 的 archetype 来解决。

针对我们要提供的应用类型,创建不同的 archetype。对于每种 archetype,创建静态资源目录,复制相关的静态文件,如富网络应用、Web 应用、移动 Web 应用等。假如创建的是一个复合应用(如富网络应用+SOA),那该怎么定义?没关系,现在主要解决的是静态文件管理的问题,对于未包含的,完全可以在 pom.xml 中添加其对应的 starter 即可。

当我们按照前端的分类创建了各种 archetype 后,搭建项目环境就会变得更加简化,尤其是使用 STS 等 IDE 时。我们只需要选择合适的 archetype,便可一键创建项目的工程目录,它包含了我们想要的前端静态文件、所有依赖的第三方框架以及集成代码,它已经完全是一个可以正常运行的环境,而且不包含任何多余的我们不需要的内容。

加入 archetype 后,我们的技术框架如下图所示:

enter image description here

手脚架

最后我们再谈一下手脚架,尽管很多人认为自动生成的代码维护起来比较麻烦,但是不可否认它会显著提升我们的工作效率。例如对于 Java 语言,如果稍微研究过 Eclipse JDT,那么完全可以结合公司代码规范、技术框架的 API,开发出自己的手脚架。如 Spring ROO 一样,我们可以通过各种命令以及命令选项,控制生成的内容,如 ORM、控制层、展现层。

如前所说,手脚架只是一个辅助开发的工具,它不应包含在项目的发布包中。可以通过以下几种方式提供手脚架。

  • IDE 插件:无论是 Eclipse,还是 NetBeans 等 IDE,都支持插件机制。通过开发一个独立的插件,我们可以在 IDE 中直观的选择配置并生成代码,而且这种方式不会对项目造成污染。
  • 独立的手脚架目录:当创建项目工程时,单独建立一个目录用于存放手脚架代码。这种方式会对项目造成污染,尽管它不会最终包含在发布包中。而且这种方式只能通过命令行进行操作,与 IDE 的结合较差。

至于手脚架可以包含哪些内容,只要是我们认为有助于减少开发人员工作量的工作都可以包含进来。它不像技术框架的组件,并不会影响最终的项目质量。只要项目开发人员认为自动生成的代码有问题,他完全可以基于自动生成的代码进行修改,以符合自己的需要,甚至也可以暂时不使用相应的命令,等待它们变得更加完善,而并不会阻塞项目的开发进程。

当然,如果你正在开发一个手脚架,还需要注意手脚架生成代码与项目代码冲突的问题,避免由此导致的代码覆盖。例如对于一个自动生成 ORM 代码的手脚架,如果使用者在实体类中添加了自定义方法,那么后期 ER 变更需要同步更新实体类时,是否会直接覆盖这些自定义方法?当然,这个问题还会存在于可视化开发界面的一些工具中。你应该对于自动生成的代码进行良好设计,如预留自定义接口,以避免这种情况,使得即支持自定义代码扩展,又可以对自动化部分进行多次同步更新(即自动生成的代码与自定义代码松耦合)。

添加了手脚架的技术框架如图所示:

enter image description here

在图中,我们为手脚架设置了不同的背景色,表示它不作为项目的一部分。

小结

对于技术架构的松耦合,它与业务功能的松耦合出发点并不完全一样。它更多体现的是一种 API 设计方法和模块化管理方式。通过风格一致的 API,降低研发人员的学习和使用成本,使组件易于替换。而通过模块化的管理方式,则可以使得项目开发环境的搭建大大简化,使开发人员不必关注各种依赖问题。

上一篇
下一篇
目录