在我的工作中,我从零开始搭建了不少软件项目,其中包含了基础代码框架和持续集成基础设施等,这些内容在敏捷开发中通常被称为“第0个迭代”要做的事情。但是,当项目运行了一段时间之后再来反观,我总会发现一些不足的地方,要么测试分类没有分好,要么基本的编码架子没有考虑周全。
另外,我在工作中也会接触到很多既有项目,公司内部和外部的都有,多数项目的编码实践我都是不满意的。比如,我曾经新加入一个项目的时候,前前后后请教了3位同事才把该项目在本地运行起来;又比如在另一项目中,我发现前端请求对应的Java类命名规范不统一,有被后缀为Request的,也有被后缀为Command的。
再者,工作了这么多年之后,我越来越发现基础知识以及系统性学习的重要性。诚然,技术框架的发展使得我们可以快速地实现业务功能,但是当软件出了问题之后有时却需要将各方面的知识融会贯通并在大脑里综合反应才能找到解决思路。
基于以上,我希望整理出一套公共性的项目模板出来,旨在尽量多地包含日常开发之所需,减少开发者的重复性工作以及提供一些最佳实践。对于后端开发而言,我选择了当前被行业大量使用的Spring Boot,基于此整理出了一套公共的、基础性的实践方式,在结合了自己的经验以及其他项目的优秀实践之后,总结出本文以飨开发者。
本文以一个简单的电商订单系统为例,源代码请访问:
git clone https://github.com/e-commerce-sample/order-backend
git checkout a443dace
所使用的技术栈主要包括:Spring Boot、Gradle、MySQL、Junit 5、Rest Assured、Docker等。
第一步:从写好README开始
一份好的README可以给人以项目全景概览,可以使新人快速上手项目,可以降低沟通成本。同时,README应该简明扼要,条理清晰,建议包含以下方面:
- 项目简介:用一两句话简单描述该项目所实现的业务功能;
- 技术选型:列出项目的技术栈,包括语言、框架和中间件等;
- 本地构建:列出本地开发过程中所用到的工具命令;
- 领域模型:核心的领域概念,比如对于示例电商系统来说有Order、Product等;
- 测试策略:自动化测试如何分类,哪些必须写测试,哪些没有必要写测试;
- 技术架构:技术架构图;
- 部署架构:部署架构图;
- 外部依赖:项目运行时所依赖的外部集成方,比如订单系统会依赖于会员系统;
- 环境信息:各个环境的访问方式,数据库连接等;
- 编码实践:统一的编码实践,比如异常处理原则、分页封装等;
- FAQ:开发过程中常见问题的解答。
需要注意的是,README中的信息可能随着项目的演进而改变(比如引入了新的技术栈或者加入了新的领域模型),因此也是需要持续更新的。虽然我们知道,软件文档的一个痛点便是无法与项目实际进展保持同步,但是就README这点信息来讲,还是建议开发者们不要吝啬那一点点敲键盘的时间。
此外,除了保持README的持续更新,一些重要的架构决定可以通过示例代码的形式记录在代码库中,新开发者可以通过直接阅读这些示例代码快速了解项目的通用实践方式以及架构选择,请参考ThoughtWorks的技术雷达。
一键式本地构建
为了避免诸如前文中所提到的“请教了3位同事才本地构建成功”的尴尬,为了减少“懒惰”的程序员们的手动操作,也为了为所有开发者提供一种一致的开发体验,我们希望用一个命令就可以完成所有的事情。这里,对于不同的场景我总结出了以下命令:
- 生成IDE工程:
idea.sh
,生成IntelliJ工程文件并自动打开IntelliJ - 本地运行:
run.sh
,本地启动项目,自动启动本地数据库,监听调试端口5005 - 本地构建:
local-build.sh
,只有本地构建成功才能提交代码
以上3个命令基本上可以完成日常开发之所需,此时,对于新人的开发流程大致为:
- 拉取代码;
- 运行
idea.sh
,自动打开IntelliJ; - 编写代码,包含业务代码和自动化测试;
- 运行
run.sh
,进行本地调试或必要的手动测试(本步骤不是必需); - 运行
local-build.sh
,完成本地构建; - 再次拉取代码,保证
local-build.sh
成功,提交代码。
事实上,这些命令脚本的内容非常简单,比如run.sh
文件内容为:
#!/usr/bin/env bash
./gradlew clean bootRun
然而,这种显式化的命令却可以减少新人的恐惧感,因为他们只需要知道运行这3个命令就可以搞开发了。另外,一个小小的细节:本地构建的local-build.sh
命令本来可以重命名为更简单的build.sh
,但是当我们在命令行中使用Tab键自动补全的时候,会发现自动补全到了build
目录,而不是build.sh
命令,并不方便,因此命名为了local-build.sh
。细节虽小,但是却体现了一个宗旨,即我们希望给开发者一种极简的开发体验,我把这些看似微不足道的东西称作是对程序员的“人文关怀”。
目录结构
Maven所提倡的目录结构当前已经成为事实上的行业标准,Gradle在默认情况下也采用了Maven的目录结构,这对于多数项目来说已经足够了。此外,除了Java代码,项目中还存在其他类型的文件,比如Gradle插件的配置、工具脚本和部署配置等。无论如何,项目目录结构的原则是简单而有条理,不要随意地增加多余的文件夹,并且也需要及时重构。
在示例项目中,顶层只有2个文件夹,一个是用于放置Java源代码和项目配置的src
文件夹,另一个是用于放置所有Gradle配置的gradle
文件夹,此外,为了方便开发人员使用,将上文提到的3个常用脚本直接放到根目录下:
└── order-backend
├── gradle // 文件夹,用于放置所有Gradle配置
├── src // 文件夹,Java源代码
├── idea.sh //生成IntelliJ工程
├── local-build.sh // 提交之前的本地构建
└── run.sh // 本地运行
对于gradle
而言,我们刻意地将Gradle插件脚本与插件配置放到了一起,比如Checkstyle:
├── gradle
│ ├── checkstyle
│ │ ├── checkstyle.gradle
│ │ └── checkstyle.xml
事实上,在默认情况下Checkstyle插件会从项目根目录下的config
目录查找checkstyle.xml
配置文件,但是这一方面增加了多余的文件夹,另一方面与该插件相关的设施分散在了不同的地方,违背了广义上的内聚原则。
基于业务分包
早年的Java分包方式通常是基于技术的,比如与domain包平级的有controller包、service包和infrastructure包等。这种方式当前并不被行业所推崇,而是应该首先基于业务分包。比如,在订单示例项目中,有两个重要的领域对象Order
和Product
(在