软件项目大部分是维护多开发少,软件代码大部分是读多写少。因此,做好一个软件项目跟做完一个软件项目同等重要。

那如何能做好一个软件项目呢?我从项目环境和文档、项目管理和项目代码三个方面总结了一些经验,希望对你有用。文章较长,我这里先附上一张思维导图,方便你了解全貌:

Better Project

项目环境和文档

项目环境

我们首先来看一下项目环境。项目环境包括本地开发环境、测试环境以及线上部署环境。

首先它们要做到尽可能一致,因为不一致就可能造成本地开发环境运行正常,一上线发现系统依赖缺少了或者系统指令集不同导致失败。

其次它们还应该做到便捷。开发环境如果复杂就意味着内部项目会新人难以入手,开源项目会更少贡献者,大部分人可能都经历过项目搭建失败的那种挫败感。测试和部署环境同样需要做到简易便捷,因为复杂意味着更冗长的操作,而操作越冗长不可控性就越高,就越可能失败。

很庆幸当前容器技术的蓬勃发展,可以很好的解决上述两个问题。要使用容器技术解决环境问题,我们可以这么做:

  1. 所有环境提供一个统一容器封装,只通过环境变量和配置来区分不同环境。通常我们会选择 Docker 工具,为每个服务创建一个 Dockerfile,使用 Dockerfile 的 target 功能区分不同环境。容器编排配置由于不同环境差异较大,可以拆分为不同文件:docker-compose.dev.yml, docker-compose.prod.yml
  2. 容器封装后,本地和线上环境都需要用到很多容器相关命令。我们需要考虑到项目的简易性,并不需要让所有人了解容器细节,因此需要对项目常用操作进一步封装,比如实现这些命令将会是一个好的开始:
    • setup: 本地环境一键搭建
    • verify: 本地开发完成后的验证命令,比如代码规范、新功能单元测试检测等,这个命令同样提供给 CI 中使用
    • deploy: 本地一键部署,通常需要有服务器或集群授权相关配置,这个命令同样提供给 CD 中使用
  3. 如果是单服务器项目,做到以上就够了。但是如果你的项目需要支持多机部署和弹性伸缩等功能,你还需要引入更强大的容器编排工具,Kubernetes 就是当前最好的一个选择。

项目文档

我们再来聊一下项目文档。项目文档主要包括文档主页和其他文档。首先我建议所有文档应该跟项目源码一同保存起来,比如放到项目的 docs 目录中,而不是一些额外的地方比如 Wiki 处。因为项目文档跟项目一样是有版本的,放到 Wiki 中会造成文档与项目版本不一致的问题。

文档主页就是常见的 README.md,通常包括项目的概览和重要信息,这里有个网站可以帮助你更好的组织:readme.so

其他文档主要包括项目使用文档、开发文档、API 文档等。

最后如果是开源项目,所有文档都应该支持多语言,不同语言可以放到不同语言目录。可以参考以下目录结构方式:

README.md               # 默认语言文档主页
README.zh-CN.md         # 中文文档主页
docs                  
├── en-US/              # 所有英文文档
├── zh-CN/              # 所有中文文档

项目管理

项目是持续的,是动态的,一个合理高效的项目管理能极大提升项目迭代效率。项目管理主要包含研发模式管理、本地开发管理和代码集成管理三个方面,下面来逐一进行讲解。

研发模式管理

研发模式的演进顺序为瀑布模式 -> 迭代模式 -> 敏捷模式,在当前互联网时代,这种敏捷模式更符合需求:

Agile methodology

敏捷模式的核心是用简易版本功能替代复杂版本,不要担心这会浪费资源,先用起来更重要。另外市场瞬息万变,研发周期过长也可能导致不能及时调整方向。

关于敏捷模式的实施,需要考虑:

  1. 对需求评估出不同成本的方案,然后根据版本计划进行选择当前够用且成本适合的方案。
  2. 代码审查中严格要求每个功能的代码量,保证需求拆分粒度。这里可以引入 CI 工具对 MR 的代码量进行检测。

管理平台的话可以选一些代码 Kanban 功能的,我的经验是功能适合团队够用即可,不需要追求大而全。比如以下这些:

本地开发管理

本地开发管理中,我们的目标是要保证团队成员代码规范,另外还要求有足够的单元测试保证功能的健壮性。

代码规范

对于代码规范,每个语言基本都有官方或者社区的语言规范,其中重要的是要添加工具来约束,不然一切可能都是白用功。

单元测试

单元测试可以保证功能的健壮性,有了单元测试后面对代码的重构也能得心应手。我们需要加上单元测试检测和测试覆盖率检测,测试覆盖率检测可以用来防止开发人员直接移除失败的测试用例而不是修复它。

通过工具和命令减轻操作负担

本地开发中加入了规范和单元测试要求后,整个操作变得复杂起来,即使提供详细的开发文档操作起来也很难顺手。此时我们要为开发流程中提供一些工具和命令,帮助开发人员减轻操作负担。

具体完整的实践可以参考以下步骤:

  1. 为你的项目添加一键校验功能,保证开发完成后,可以一键校验所有流程和规范都符合要求。这里要做的就是实现上面“项目环境”章节中提到过的 verify 命令,以下是可以考虑校验的一些方面:
    • Lint: 检测代码规范,大部分语言都有官方或者社区的代码规范,我们应该为团队选择一份规范
    • Test: 保证单元测试是否能通过
    • Test Cover: 保证单元测试覆盖率不会低于阈值
    • Build: 如果是编译型语言,需要确保不会出现编译错误
    • License Check: 许可证相关检测,比如开源项目可能需要添加许可证头
    • Dependency Check: 有些项目需要控制第三方依赖包的使用
  2. 为你的项目 CI 步骤里加上 verify 校验,确保进入主分支的代码符合项目要求。
  3. 为本地开发提供一些其他效率工具,通过自动化帮助开发,比如:
    • Setup: 上面提到过的项目初始化命令
    • Gen: 代码生成命令,一些全栈框架比如 Rails 会自带,如果你使用的框架没有带,可以自己添加一些,减少重复工作
    • 上面 verify 的具体命令也记得要单独提供出来,方便用户根据需要单独做一些检验

代码集成管理

对于代码集成,基本上大家都会用到 Git 版本控制工具。其中需要做好的包括 Commit 规范、Git 工作流和代码审查三个方面。

Commit 规范

先讲简单的 Commit 规范,这个的目的就是让变更信息更规范,方便问题定位以及代码回滚。我推荐用上:

Git 工作流

再来讲下 Git 工作流。对于 Git 工作流,不同场景需求虽然会有些不同。但我建议无论如何在主分支下工作,因为主分支是 GitLab/GitHub 等平台默认展示的分支,也是 MR 默认的目标分支,这会让我们省很多事。在主分支下工作具体就是:新功能开发需要从主分支切出来,代码通过 MR 提交回主分支。

对于具体的 Git 工作流,以下我通过表格来列举三个常用的方案:

Git 工作流 说明 适用场景
集中式 直接提交到主分支 项目初期,成员简单
功能分支 通过 MR 合并代码 项目简单,不需环境和版本
功能分支 + 环境分支/标签 通过 MR 合并代码,环境分支/标签变更触发部署任务 常规项目,需要多环境

可能有些人还听说过 Fork 工作流,实际上 Fork 工作流等同于上述的功能分支工作流,唯一区别的只是开源项目你没有项目内创建分支的权限,因此你需要 Fork 一个新项目再提交 MR。

对于上面三种工作流,我的建议是根据项目需要逐步演进,最终使用功能分支 + 环境分支/标签这种适用性广且操作清晰的工作流。

对于功能分支+环境分支/标签这个工作流的具体使用,我来举几个例子来说明:

1. 当前需要增加一个预发布环境,应该怎么做?

  • 添加一个 staging 保护分支,之后分支情况就是:main > staging > production,对于两个环境分支还是照样变更则会触发自动部署(也可以将 production 配置成仅生成部署任务,然后人工来触发)

2. 生产环境遇到代码事故,需要快速修复(也就是我们常说的 hotfix),应该怎么做?

  • 直接从事故的环境切出临时修复分支(例如:hotfix/production-problem-xx),修复后创建目标为事故环境分支的 MR,MR 验证成功且评审通过后直接合并进环境分支并触发部署,部署完成后在事故环境中验证问题。

  • 最后再将代码同步添加回前置分支上(比如在 staging 上的修复要加回到 main,在 production 上的修复要加回到 staging 和 main)。

3. 功能 A 已经合进 main 分支,临近发版部署时遇到问题,功能 A 暂时不能部署,此时如何不影响其他功能发布?

  • 在 main 分支上对功能 A 进行回滚。这里我们的准则是保证所有保护分支提交只读。

最后推荐看看这篇工作流最佳实践文章:What are GitLab Flow best practices?

代码审查

对于代码审查,一个简单的流程参考:

  1. 为项目成员设置不同角色,比如:
    • Backend Reviewer
    • Frontend Reviewer
    • Security Reviewer
    • Maintainer
  2. 在 MR 中根据代码修改部分找到相应的 Reviewer 角色进行 Reviewer Review。例如当前后端都做了修改,此时应该找到 Backend Reviewer 进行代码审查
  3. Reviewer Review 通过后,找到 Security Reviewer 做代码安全审查,防止引入安全漏洞
  4. Security Review 也通过后,找到项目的 Maintainer 进行最终的 Maintainer Review。Maintainer Review 通过后 Maintainer 就可以合并代码。

推荐看看这篇代码审查指导文章,里面包含了一些具体的最佳实践:GitLab Code Review Guidelines

项目代码

最后也是最重要的,就是项目代码质量要高。这个是毫无疑问的,因为一个软件项目最长时间接触的就是代码。对于项目代码方面,我分了代码结构和代码设计两个方面。提高它们就是为了在功能实现的基础上,如何让代码的维护性和扩展性更好。

代码结构

代码结构简单来说就是目录结构加分层结构。他们俩个有一定联系,好的目录结构应该能直观的表现出项目的分层结构。对于目录结构,如果项目使用了框架,请保持框架的代码结构,因为它们都是经过大量项目验证的。

对于项目分层结构,我的推荐是:

Code Structure

  • Controller: 控制层,对上直接接收处理用户请求,对下通过组合使用不同业务层处理请求,并响应回用户。
  • Service: 业务层,对上由控制层来调用,对下通过组合使用不同模型层和客户端层来完成具体业务操作。这个层一般是项目中最多变的部分。
  • Model: 模型层,有时也叫做实体层(Entity),主要用来定义一些数据结构和数据内部方法。由业务层来调用。一些需要持久化的模型通常会配合 ORM 框架一起使用,方便把数据保存到数据库。
  • Client: 客户端层,对上由业务层来调用,对下通过调用第三方服务(例如缓存、日志服务)来完成对他们的操作。这个层一般会使用第三方服务 SDK,然后进行配置方便项目使用

代码设计

代码设计的目标,是让我们通过软件工程方法,以及前人总结的经验设计出更加健壮、可扩展、可维护的软件系统,提高代码质量和可读性。

设计模式

代码设计中,设计模式是最容易实施的,因为它是业界针对一些特定的场景总结出来的最佳实现方式,可以直接针对具体场景选择使用。

这里有个在线的设计模式网站,里面包含了各种语言的示例,方便你需要时查询:refactoringguru

其中列举几个比较常用的设计模式:

  • 单例模式:全局共享一个实例,比如常见的数据库实例
  • 策略模式:定义一些列相同接口的算法,使他们可以相互替换使用
  • 模板模式:在超类中定义了一个算法框架,允许子类在不修改结构的情况下重写某些步骤
  • 代理模式:在操作对象中引入一个代理对象,快速增加对象的功能,并能实现代理前后添加一些操作

SOLID 原则

SOLID 是面向对象设计和编程中的五个基本原则的缩写。对比设计模式,它是我们设计项目代码的指导方针:

名称 简写 解释
单一职责原则 SRP 一个类或模块只负责完成一个指责或功能
开放/封闭原则 OCP 软件实体应该对扩展开放,对修改关闭
里氏替换原则 LSP 派生类应该能够替换掉基类,而不会破坏程序
接口分离原则 ISP 客户端不应该依赖它不需要的方法
依赖倒置原则 DIP 依赖于抽象,而不是细节。面向接口编程

总结

以上就是做好一个软件项目的经验总结。打个广告,我当前开源了一个管理系统模板的开源项目:songhuangcn/admin-template,这是我用来实践这些经验的具体项目,希望对你有参考价值。

如果你有关于软件项目好的实践或想法,欢迎一起讨论。