好的项目是怎样的?

概览(Overview)

我觉得它应该至少包含以下几点:

  • README
  • 捕捉错误
  • 自定义错误类型
  • 健全的错误处理机制
  • 完善的日志
  • 低耦合高内聚的模块
  • 灵活且详细的配置管理
  • 自动化
  • 必要的测试
  • 文档
  • 清晰的目录结构
  • 对于第三方库进行二次封装
  • 不放置代码和文档以外的其他东西

README

一份项目说明文档,能做到让新人读完这篇文档,就对这个项目有大概的了解。

它应该至少包含以下几个:

  • 项目简介。一句话介绍它是做什么的。
  • 项目当前版本。如果这个项目是被其他项目使用的,说明项目的当前版本十分重要。(不过在 NodeJS 中,这通常都写在 package.json 中)
  • 项目依赖环境及其版本。比如说项目用到了数据库(如 MySql),你就必须注明数据库的版本号,便于每个开发者都使用一致的环境。
  • 目录结构。使用 tree 命令打印出的目录树,并对每个目录和重要的文件进行注释,方便理解。
  • 介绍如何快速上手项目(Quick Start)。必须做到新人能按照你写的步骤,一步步搭建环境并成功运行项目。并且中途不会有任何问题。具体可以分如下几个部分:
    • 安装。如何安装本项目以及其他依赖环境。
    • 配置。详细介绍应该如何进行配置,哪些配置项是必须修改的。
    • 运行。执行什么命令能够运行项目。
  • 部署。如何将项目部署到各个环境(测试环境,预发布环境,生产环境等的)
  • (如果这是个服务端项目)接口文档说明。指示读者哪里能够查阅接口具体说明。(不要告诉我去看代码!)
  • 如何提 BUG 或者 Issue。指示在哪里汇报项目的 BUG,或者给项目提建议。Bug 无处不在,所以你需要收集并跟踪 BUG。Issue 不仅可以搜集别人的意见,还可以放置你对项目的计划和想法。
  • 如何给项目做贡献。即引导新人开发者了解项目构成,项目的代码风格,要注意的地方,如何开发等的。
  • 版权申明。项目使用什么样的许可证书。即使是公司项目,最好也是写一写。

捕捉错误

不论何时在什么地方发生错误,应用必须能够捕捉到(底层捕捉或者人为判断),然后交给错误处理程序进行处理。

自定义错误类型

语言原生的错误类型是不够充分的,需要开发者在其之上构建适合于自己应用的错误类型。附加上更多信息,利于调试。

一般来说,你应该设置多种类型的错误,以应对不同的场景。
每类错误都应该有唯一的错误码,方便打印时供人查阅具体信息,也方便根据错误码进行错误处理(例如不同系统间的远程调用错误处理)。

以下是加分点

给你的错误加上元数据(meta),这种元数据能够方便打印错误日志的时候将元数据打印出来。开发者能够随意得往错误对象中添加上下文信息到元数据。
元数据能够被继承,比如 new Error(err) 这种情况返回新的错误将带有上一个错误的元数据。

健全的错误处理机制

当发生一个错误时,应用有以下几种处理方法:

  • 打印错误,一旦需要打印错误,必须至少要打印以下几个信息:
    • 错误消息(error message)
    • 错误码(error code)。这是在自定义错误类型中人为定义的
    • 错误类型(error type)
    • 错误堆栈(error stack)
  • 进行其他的处理,比如说发送警告邮件等等。
  • 在当前这层不做处理,递交给上层处理,直到应用最顶层。

以下是加分点

当你的代码没有处理对应错误的时候,应用能进行如下处理:

要么能够快速失败(fail fast),即不要做任何容错处理,立刻停止应用,让外部的守护进程重启你的应用。
因为一旦发生了你不能处理的错误,应用的状态就进入一种不可知的状态,你预估不了应用会做出什么行为。所以重启,让应用返回到最初的状态。

要么部分失败重启,例如 erlang 语言,子进程崩溃时能够在不影响主进程的情况下,通知主进程,打印日志,重启子进程。
这种处理机制主要是为了在生产环境保证其他功能可用。

然而以下两种处理方式,我认为是不当的:

其一,忽略错误,即不处理错误(包括不打印错误)。即使你觉得这个错误不会影响到业务逻辑,甚至无关紧要,也不要漏掉任何细节,尤其是那些不按照期望代码执行而抛出的错误(error)或者异常(exception)。这会为你调试某些 BUG 有益的。
其二,容错处理。任何容错处理,都是对错误的容忍、娇惯、延误。一旦你将错误延迟,你就不能在第一时间发现错误。这是导致复杂嵌套的 BUG 最主要的原因之一。

完善的日志

日志应该有恰当的日志级别,以便根据不同环境来输出对应等级以上的日志。级别不必太复杂也不能太少,一般我使用 debug, info, warn, error, fatal 这五种等级。

对于错误日志,除了 #健全的错误处理机制# 中说明的必要字段,还需要打印更多详细信息,比如发送错误的地方的上下文(如函数输入参数),以便复现错误场景。

日志消息(message)必须完整,通俗易懂,能够自我描述。

以下是加分点

针对终端输出人类阅读友好的格式,针对文件输出机器友好的格式(如 JSON Log)。
因为终端通常是给人看的,所以务必把输出的堆栈,错误信息,额外字段,json 等重整格式,加上颜色高亮,让人一目了然。
而输出到文件通常是用于 ELK 之类进行日志收集整理的,所以用 JSON 格式更容易整理字段。

使用键值对打印出数据变量(即上下文),这样既方便 ELK 检索,也方便阅读,更易扩展。

如果日志能够对某些模块进行开关是极好的。比如在某环境不打印数据库日志,在某环境不打印 RPC 日志,在某环境不打印 HTTP 日志等等。

低耦合高内聚的模块

模块能够独立启用,而不需要启动整个应用。这能够利于单元测试以及自己调试。
模块能够管理与被管理,外部调用者能够轻松控制模块的整个生命周期,而模块能够管理其下的其他组件。
模块间没有复杂的引用关系。使用者只要引用一个模块即可使用。

灵活且详细的配置管理

理想的配置是如 node-config 这种。给项目提供一个 default 模板,让项目使用者写自己的本地配置来覆盖 default 配置。

个人不喜欢 config.sample 或者 config.template 这种做法,因为这样意味着使用者必须把 config.template 中的所有内容复制到 config 中,一旦模板发生改变,diff 起来是很麻烦的。而且你本地的配置和默认配置都混在了一个文件里,如果我问哪些配置项是本地的,想必是无法立刻回答上来的。

配置应该只提供可能需要变化的数据,而项目中不会变的数据不应该放置在配置中,而应该放置在常量声明里。

加分点

给每个配置项写注释,注明每个配置的含义,它可以填哪些值,其值又对应什么样的功能。越详细越好。

自动化

自动化部署,自动化测试,自动化构建。能做到一键做某事的程度。

加分点

易用,易扩展,易理解。

必要的测试

虽然不要求百分百的测试覆盖率,但针对关键部分,易错部分需要有单元测试、系统测试,有的时候甚至还端到端测试。

加分点

有帮助测试的 mock、stub、fake 等工具。

文档

这个文档不只是 README,还包含其他各种文档,比如:项目规范文档,API 文档、HTTP API 文档,必要的流程文档,系统设计文档(如果系统很复杂,不能通过目录结构看出来的话)等等。

加分点

对项目中用到的黑科技写个文档,因为如果阅读代码要很久才能理解,不如写一段话描述一下方便他人理解。

清晰的目录结构

当添加新功能时,你总能将新代码快速地放到适合它存在的地方。
当你想找某块功能的代码时,你总能快速地找到它。

目录不宜嵌套过深。

当一个目录下同类型的文件和代码很少,不如把它合到一个文件里去。
当一个文件代码非常庞大,把它拆分成一个目录下的多个文件。

对于第三方库进行二次封装

因为第三方库有可能有漏洞,或者不符合项目需求的地方。这时候就需要打补丁(不要在源代码上直接修改!),如果已经有个文件对第三方库进行二次封装,那么打补丁就很简单了。

不放置代码和文档以外的其他东西

如下:

  • 一次性的脚本文件:因为它是用之即弃的东西,它不会随着项目一起成长,时间久了就不再适用了,放在项目里只会徒增烦恼。
  • PS 源文件:代码开发项目,这种文件应该放在其他地方。
  • 过大的图片:应该用其他工具管理。
  • 证书:隐私敏感的文件不应该放在代码仓库中。
  • 其他