写在前面
我,是一个老程序员。所谓老,一方面年龄不小了,绝大多数在公司中成长的我这个年龄的技术人员都已经放弃了写代码的工作,开始转型做其它工作;另一方面,作为一个从小学四年级开始学习 BASIC 语言的人来说,我从事编程的工作的时间相对于很多年轻人而言,还是长了不少的,如果说熬夜通宵,已经拼不过年轻人,但经验,多多少少还是积累了一点。
三十年的时间,除了小学初中时代的懵懵懂懂,到了高中之后,已经可以独立编写一些小软件了。高中时玩儿的比较多的是 Delphi,1997 年开始学 LAMP,2000 年开始用 Java (Struts、Torque、Ant)写远程教育的教务管理,2003 年在摩托罗拉中国软件公司实习时跟的小组是在 NetBeans 的基础上做内部开发工具,2004 年做研究生论文时花了很多时间琢磨操作系统最底层是怎么回事儿,2005 年下半年为了给学生讲课给 Moodle 写了插件,又在 Linux 上写了个 Online Judge,然后从 2005 年开始的十几年一直给计算机和软件专业的学生讲“程序设计方法与实践”,中间还讲了两年“Linux 系统管理”,闲暇时做过几个项目和几个小小的开源软件……不说了,这些原本就是一个老程序员该有的经历,不是吗?
这篇文章我想讲述的故事,跟上面的这些乱七八糟的经历有些关系,因为长时间的软件开发让我产生了一系列问题,到了 2013 年,我有了机会去尝试回答这些问题。
对一系列问题的思考
2000 年的纳斯达克泡沫并没能阻止万维网的快速发展,各种传统的语言纷纷开始支持 Web 应用开发。最早的 PHP,可以快速编写出一个网站,但如果程序员的功力稍微差一点,其代码的难理解程度不亚于早期用 GOTO 写出的面条代码。后来的 Java,在没有 Struts 时有人写过上万行的 Servlet,但有了 SSH 框架后烦人的事情变成了写 XML 配置 ,很多程序员折腾“小毛驴”的时间甚至超过了编码时间。
早期的 PHP 和早期的 Java 开发 Web 应用的方式,真的有点混乱。所以很长一段时间,我不再关注 Web 相关的东西,甚至对这些语言本身都有了一些成见。难道 Web 程序写起来真的就这么烦人吗,真的就没有好的办法让程序开发更加规范,也能让编码效率大大提升吗?
因为工作关系,我接触到了 Moodle,又因为要扩展其功能,所以花了不少时间去看 Moodle 本身的代码 。这个过程,让我彻底对软件开发有了新的认知,原来通过好的软件架构设计,用 PHP 也可以做出这么优秀的软件。但 Moodle 的软件架构,完全是为了其产品自身设计的,很难被其它项目复用。
这就产生了新的问题,优秀的程序员永远是稀缺的,而项目的时间又是紧张的,如果从头去做一个项目,功能还做不过来,又有多少时间能用来做软件架构方面的事情呢?
答案在哪里
2007 年第一次接触到 RoR (Ruby on Rails),我的很多疑问得到了解答。Rails 的设计思路真正体现出了大师级的智慧,让我这样的菜鸟大开眼界。以下的部分,摘自百度百科,有删改:
1.一站式的 MVC 框架:通过 Rails 可以实现 MVC 模式中的各个层次,并使它们无缝地协同运转起来。过去在项目开始时需要程序员自己做的事情,不需要做了,已经妥妥地准备好。
2.约定优于配置:为了说明各个对象之间的关联关系,一般的 Web 应用开发框架往往采用写入 XML 配置文件的方法。这种方式虽然可以解决一些问题,但是却带来了管理上的混乱。Rails 对此的态度是约定优于配置,这意味着在 Rails 中不会出现 XML 配置文件。而约定优于配置的另外一个好处,就是程序员必须遵守特定规范,避免了团队成员甚至不同团队之间对于大量细节处理的不一致。
3.更少的代码:使用约定来代替 XML 配置文件说明 Rails 本身完成了大量的底层工作,这意味着使用更少的代码来实现应用程序是极有可能的。此外,代码量的缩减也减小了出现 bug 的可能性,降低了维护程序和升级程序的难度。
4.生成器:Rails 使用的实时映射技术和元编程技术,免去了开发人员在开发过程中编写大量样板文件代码的烦恼。在少数需要使用样板文件代码的时候,开发人员可以通过 Rails 内建的生成器脚本实时创建,而不再是通过手工编写。Rails 的这个特点可以使开发人员更专注于系统的逻辑结构,而不必为一些琐碎的细节所烦扰。
5.零周转时间:对已有的 Web 应用系统进行修改后,其一般需要经过配置、编译、发布、重新设置、测试等一系列步骤才能投入使用,这明显浪费了许多时间。而使用 Rails 开发 Web 应用系统,可以通过浏览器即时查看程序运行结果,从而节约了大量的时间。
6.脚手架系统:Rails 的脚手架系统可以自动为任何相关的数据库表创建一套包含标准CRUD 操作和前台视图的系统。通过脚手架系统,开发人员可以方便快捷地操纵数据库中的数据表。此外,Rails 也允许开发人员使用自己设计的代码或视图来替换自动生成的代码和视图。
RoR 的成功并不是因为 Ruby,而是因为它基于 Ruby 语言特点提出了一系列解决 Web 应用开发中已经出现的问题的思路。 而这些思路,得到了业界的广泛认可,于是程序员们开始基于各自习惯的编程语言,利用 RoR 的思路,开发了各种不同语言下的类 RoR 框架。譬如 PHP 的 Yii、Laravel,Python 的 Django,Scala 的 Play 以及 Groovy 下的 Grails,这些开发框架的快速成熟将 Web 应用开发从混沌的状态带入了一个新的规范化的阶段,配以“敏捷编程”的软件工程方法,Web 应用编写的效率大大提升了。
今天,几乎所有的成熟 Web 开发框架中,都能或多或少的看到 RoR 的影子。
尝试和新的问题
在进入网络中心工作后,由于多种原因,导致我们不得不自己写一些 Web 应用来支撑学校的工作。“工欲善其事,必先利其器”,我们比较、尝试了几种不同的语言和类 RoR 框架后,选择了 Grails,原因主要有以下几点:
- 连接商业数据库对于 PHP 和 Python 等纯开源语言比较繁琐,而其支持企业级应用的各种类库也远不如 Java 丰富;
- Groovy 是在 Java 基础上开发的动态语言,全面支持 Java 的语法和类库,对于 Java 程序员更容易学习;
- Scala 初出茅庐,当时 Play 框架社区中提供的现成插件几乎为零;
- Grails 的功能相当一部分能力基于 Spring、Hibernate 等非常成熟的组件封装,不重新发明轮子也证明了其背后研发团队思路上的成熟。
于是,在经过一段时间的学习、摸索和在一两个小业务上的尝试后,我们用 Grails 做了学校的离校系统,并正式上线使用。在这个过程中,我们解决了很多不得不解决的问题,譬如用户和权限的管理、跟 CAS 的认证集成、不太难看的 PC 和手机自适应界面等等,当然还要把离校系统自己的业务写完。这个过程,用到了好几个 Grails 开源社区提供的插件。
经过这个系统的研发,我们基本上已经可以快速用 Grails 开发新的小应用了,但当一个新的事情到来时,却有了新的问题困扰我:我如何把前面的这个应用中写好的通用的部分拿出来直接用的新的应用中呢,总不能直接拷贝过去吧?
复用:提高软件研发效率的不二法门
在开始思考这个问题之后,我才发现我之前真的忽略了 RoR、Grails 中一个非常重要的概念:插件(Plugin)。我们在开发的过程中,大量的使用了插件,但却从来没有仔细去思考过如何把自己已经写过的,通用性比较强的代码变成插件。
与函数库、类库不同,插件不仅仅是完成某种计算的代码,插件中可以有自己的初始化代码、可以包含前端页面和后端处理逻辑,可以引用其它插件也可以被应用或其它插件引用。也就是说,我们可以将已经在前面的应用中编写好的模块,在提高其通用性后封装到插件中,进而在所有的后续应用中使用。
于是我们开始将应用中通用代码剥离写入插件,随着我们做过的应用越多,我们积累起来的插件其功能也越完善,几年下来,我们已经有现成的插件解决以下问题了:
- 界面:对 Bootstrap 模板的优化使用,目标是在应用中编写界面所用的时间尽量少,且有规范可遵循。
- 认证和权限:完整的用户和权限管理需要花很多时间编写,如何用一个通用模型来支持大部分 MIS 所需要的授权场景。支持 DB、LDAP、CAS 等常见的认证模式,甚至同时支持启用其中多种,这样才可以适应不同的环境和应用场景。
- 消息:支持通过学校的服务器或外部云服务对外发送短信或邮件。
- 报表:支持利用 JasperReport 制作的报表并生成 PDF。
- 支付:支持利用学校自己的支付网关完成支付,这样每个应用跟网关的对接部分始终都是稳定的。
- 文件:解决用户上传文件的存储问题,是放到本地目录,还是放到 Oracle 的 SecureFile 字段中。
- 数据:如何只定义数据表结构,就生成一整套高质量的增删改查和搜索的界面,默认不仅支持整个权限体系,可以通过配置调整,适应大多数使用场景,又让程序员可以手工定制其中的一部分以满足特定需求。
- API:应用对外开放 API 时的访问控制和授权管理,应用只负责 API 的实现。
在引入插件后,我们不仅可以大大提升后续应用的开发速度,还可以让我们对插件本身持续改进,而改进后的结果,又可以重新用到之前的应用中。
团队组织
引入“插件”这种软件组件复用的机制后,不仅生产效率大大提升,还有一个好处就是给了团队成员学习和成长的空间。通常一个 985 大学计算机专业的毕业生,基本的编程能力都是具备的,但就像我自己刚刚毕业时那样,缺少对软件架构的理解。我必须让每一个团队成员投入到工作中,完成必须完成的任务,但又不能长期让他们编写细节性的、初级的代码,这样他们会无法提升。
所以,当团队中有新的成员加入时,我会让他们通过做一两个小应用对整个框架进行熟悉,然后通过一两个大一点的应用对整个框架达到一个相当的理解。再接着,就可以让他们在任务不那么紧张的时候,去改进某个插件,或者把新的通用的功能写成新的插件。
虽然形成一定的研发规范,对每一个成员而言是有一定限制的,但使用通用的开发框架的好处在于这种框架不仅仅可以用于编写用于学校场景的应用,用它可以编写任何一种 Web 应用。虽然到了另外一个环境,团队成员也许不再使用 Grails 框架,但 RoR 背后的思想会促进他们的认知层次从一个单纯的码农向软件架构师提升。
一些数据
实际上,提高软件开发效率,应该用实际的时间和质量的数据来分析,但我们并非一个只做软件开发的团队,开发必要的软件只是我们工作的一部分,再加上这个故事已经是历史,也很难再去统计什么。所以只能用现成的一部分已经编写好的代码做个简单统计。
首先看几个被引用的最多的插件:
插件 | 后端代码 | 前端代码 |
---|---|---|
界面 | 0 | 2084 |
认证和权限 | 2772 | 1618 |
文件存储 | 412 | 0 |
数据 | 8381 | 4215 |
再看几个比较典型的应用:
应用 | 引用插件 | 应用后端 | 应用前端 | 插件占比 |
---|---|---|---|---|
离校 | 界面、认证 | 1555 | 2048 | 64% / 64% |
双学位管理 | 界面、认证 | 4937 | 5973 | 35% / 38% |
迎新 | 界面、认证、文件存储 | 7607 | 10613 | 29% / 38% |
秦分校教务 | 界面、认证、数据 | 3381 | 1946 | 76% / 80% |
如果我们把一个应用自身代码和它所引用的我们编写的插件代码相加作为总代码行数,可以看出通过引用插件可以让一个应用少写的代码的总量。由于“数据”插件我们比较晚才开始写,所以只在后期的一些应用中使用,但可以看出其带来的效率提升是非常明显的。
当然这个统计方法并不完全科学,因为一个东西提高其通用性所付出的代价肯定是超过非通用的。
写在最后
以上的整个过程,仅仅是我个人对于如何“通过改善软件架构设计提高研发效率”这个问题的一次思考和实践的总结,并不代表文中所述的方法是一种最好的方法,也并不代表这种方法可以适用于不同的软件产品和软件研发组织。
但我认为这是值得相当一部分软件研发组织思考和解决的问题。