|
@@ -0,0 +1,275 @@
|
|
|
|
|
+# Clean Code 契约
|
|
|
|
|
+
|
|
|
|
|
+本文约束新增和修改代码时的代码质量形态。它只关注函数设计、命名、控制结构、错误处理、注释、副作用、重复和可读性,不替代前后端各自的架构开发规范。
|
|
|
|
|
+
|
|
|
|
|
+## 1. 函数设计
|
|
|
|
|
+
|
|
|
|
|
+- 新增或修改的函数默认不超过 20 行,不含空行和注释。
|
|
|
|
|
+- 业务编排函数最多不超过 30 行;超过时必须拆分。
|
|
|
|
|
+- 函数参数不超过 3 个;超过 3 个必须改为对象参数。
|
|
|
|
|
+- 圈复杂度 <= 5。
|
|
|
|
|
+- 嵌套深度 <= 2。
|
|
|
|
|
+- 必须优先使用 guard clause 提前返回,避免主流程被多层 `if` 包裹。
|
|
|
|
|
+- 一个函数只做一件事,不混合校验、转换、查询、保存、渲染等多个职责。
|
|
|
|
|
+
|
|
|
|
|
+Bad:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+function submit(user, form, config, notify) {
|
|
|
|
|
+ if (user) {
|
|
|
|
|
+ if (form.valid) {
|
|
|
|
|
+ if (config.enabled) {
|
|
|
|
|
+ save(user, form);
|
|
|
|
|
+ notify.success();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Good:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+function submit(options: SubmitOptions) {
|
|
|
|
|
+ if (!options.user) return;
|
|
|
|
|
+ if (!options.form.valid) return;
|
|
|
|
|
+ if (!options.config.enabled) return;
|
|
|
|
|
+
|
|
|
|
|
+ save(options.user, options.form);
|
|
|
|
|
+ options.notify.success();
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 2. 命名
|
|
|
|
|
+
|
|
|
|
|
+- 布尔变量必须以 `is`、`has`、`can`、`should` 开头。
|
|
|
|
|
+- 执行动作的函数使用动词开头,例如 `getUser`、`validateEmail`、`saveRole`。
|
|
|
|
|
+- 事件处理函数使用 `handle` 或 `on` 前缀,例如 `handleSubmit`、`onDialogClose`。
|
|
|
|
|
+- 有副作用的函数名必须体现动作,例如 `save`、`update`、`delete`、`load`、`fetch`、`sync`。
|
|
|
|
|
+- 避免无意义缩写。
|
|
|
|
|
+- 避免单字母变量,循环索引 `i` 除外。
|
|
|
|
|
+
|
|
|
|
|
+Bad:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+const visible = user.status === "enabled";
|
|
|
|
|
+
|
|
|
|
|
+function userData() {
|
|
|
|
|
+ return api.getUser();
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Good:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+const isVisible = user.status === "enabled";
|
|
|
|
|
+
|
|
|
|
|
+function getUserData() {
|
|
|
|
|
+ return api.getUser();
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 3. 控制结构
|
|
|
|
|
+
|
|
|
|
|
+- 禁止深层 `if/else`。
|
|
|
|
|
+- 连续分支超过 3 个时,优先使用对象映射、`switch`、策略方法或枚举方法。
|
|
|
|
|
+- 简单数据转换优先使用 `map`、`filter`。
|
|
|
|
|
+- 不为了“函数式”强行使用难读的 `reduce`。
|
|
|
|
|
+- 当需要提前退出、复杂流程控制或显式循环更清晰时,允许使用 `for` / `for...of`。
|
|
|
|
|
+- 不允许在 `switch` 或长 `if/else` 中堆大量业务逻辑,复杂分支必须拆成独立函数。
|
|
|
|
|
+
|
|
|
|
|
+Bad:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+function getStatusLabel(status: string) {
|
|
|
|
|
+ if (status === "enabled") return "启用";
|
|
|
|
|
+ if (status === "disabled") return "禁用";
|
|
|
|
|
+ if (status === "pending") return "待处理";
|
|
|
|
|
+ if (status === "locked") return "锁定";
|
|
|
|
|
+ return "未知";
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Good:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+const statusLabels: Record<string, string> = {
|
|
|
|
|
+ enabled: "启用",
|
|
|
|
|
+ disabled: "禁用",
|
|
|
|
|
+ pending: "待处理",
|
|
|
|
|
+ locked: "锁定"
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+function getStatusLabel(status: string) {
|
|
|
|
|
+ return statusLabels[status] ?? "未知";
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 4. 错误处理
|
|
|
|
|
+
|
|
|
|
|
+- 禁止吞异常。
|
|
|
|
|
+- 禁止空 `catch`。
|
|
|
|
|
+- `catch` 后必须处理、记录、转换或重新抛出异常。
|
|
|
|
|
+- 外部调用必须有明确错误处理,包括网络、数据库、文件、缓存、第三方服务。
|
|
|
|
|
+- 不允许只打印错误但不中断、不反馈、不恢复状态。
|
|
|
|
|
+- 不允许把异常转换成无意义的 `null`、`false` 或空数组,除非调用方能明确区分该状态。
|
|
|
|
|
+
|
|
|
|
|
+Bad:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+async function loadUser() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return await api.getUser();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.log(error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Good:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+async function loadUser() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return await api.getUser();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ showErrorMessage("用户信息加载失败");
|
|
|
|
|
+ throw error;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 5. 注释
|
|
|
|
|
+
|
|
|
|
|
+- 禁止废话注释,例如 `i++ // 增加 i`。
|
|
|
|
|
+- 代码能清楚表达“做了什么”时,不写解释性注释。
|
|
|
|
|
+- 复杂算法、特殊兼容、业务规则例外必须说明“为什么这么做”。
|
|
|
|
|
+- 临时方案必须标明原因和后续处理方式,避免无上下文的 `TODO`。
|
|
|
|
|
+- 注释必须随代码更新,禁止保留过期注释。
|
|
|
|
|
+
|
|
|
|
|
+Bad:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+// 遍历用户
|
|
|
|
|
+users.forEach(user => {
|
|
|
|
|
+ // 设置名称
|
|
|
|
|
+ user.name = user.name.trim();
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Good:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+// 后端历史数据可能包含首尾空格,提交前统一清理,避免唯一性校验误判。
|
|
|
|
|
+const normalizedUsers = users.map(user => ({
|
|
|
|
|
+ ...user,
|
|
|
|
|
+ name: user.name.trim()
|
|
|
|
|
+}));
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 6. 副作用
|
|
|
|
|
+
|
|
|
|
|
+- 优先编写纯函数:相同输入应产生相同输出。
|
|
|
|
|
+- 数据转换、格式化、校验逻辑应尽量保持无副作用。
|
|
|
|
|
+- 修改外部状态的函数必须在命名中体现副作用。
|
|
|
|
|
+- 不在工具函数中偷偷修改入参、全局状态、缓存或组件状态。
|
|
|
|
|
+- 修改入参前必须显式说明原因;默认应返回新对象或新数组。
|
|
|
|
|
+
|
|
|
|
|
+Bad:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+function normalizeUser(user: User) {
|
|
|
|
|
+ user.name = user.name.trim();
|
|
|
|
|
+ return user;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Good:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+function normalizeUser(user: User) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...user,
|
|
|
|
|
+ name: user.name.trim()
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 7. 重复与抽象
|
|
|
|
|
+
|
|
|
|
|
+- 禁止复制粘贴相同业务逻辑。
|
|
|
|
|
+- 出现第 2 次重复时可以接受局部重复。
|
|
|
|
|
+- 出现第 3 次重复时必须抽取公共函数、组件、常量或配置。
|
|
|
|
|
+- 不为了“预防未来变化”提前抽象。
|
|
|
|
|
+- 抽象必须基于已经出现的重复或明确稳定的业务概念。
|
|
|
|
|
+- 公共函数必须有清晰输入输出,不能依赖隐式上下文。
|
|
|
|
|
+
|
|
|
|
|
+Bad:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+const userStatus = row.status === 1 ? "启用" : "禁用";
|
|
|
|
|
+const roleStatus = role.status === 1 ? "启用" : "禁用";
|
|
|
|
|
+const postStatus = post.status === 1 ? "启用" : "禁用";
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Good:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+function getEnabledStatusLabel(status: number) {
|
|
|
|
|
+ return status === 1 ? "启用" : "禁用";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const userStatus = getEnabledStatusLabel(row.status);
|
|
|
|
|
+const roleStatus = getEnabledStatusLabel(role.status);
|
|
|
|
|
+const postStatus = getEnabledStatusLabel(post.status);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 8. 可读性
|
|
|
|
|
+
|
|
|
|
|
+- 优先让主流程从上到下阅读。
|
|
|
|
|
+- 变量声明尽量靠近使用位置。
|
|
|
|
|
+- 避免长链式调用;链式调用过长时拆成中间变量。
|
|
|
|
|
+- 避免布尔表达式过长;复杂条件必须提取为语义明确的变量或函数。
|
|
|
|
|
+- 避免魔法值;业务含义明确的数字、字符串应提取为常量。
|
|
|
|
|
+- 不写“聪明但难懂”的代码。
|
|
|
|
|
+
|
|
|
|
|
+Bad:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+if ((user.age > 18 && user.status === 1 && !user.deleted) || user.role === "admin") {
|
|
|
|
|
+ allowAccess();
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Good:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+const isActiveAdult = user.age > 18 && user.status === 1 && !user.deleted;
|
|
|
|
|
+const isAdmin = user.role === "admin";
|
|
|
|
|
+
|
|
|
|
|
+if (isActiveAdult || isAdmin) {
|
|
|
|
|
+ allowAccess();
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 9. 自动化检查
|
|
|
|
|
+
|
|
|
|
|
+完成代码后必须运行与改动相关的检查。
|
|
|
|
|
+
|
|
|
|
|
+建议静态检查覆盖:
|
|
|
|
|
+
|
|
|
|
|
+- 函数长度
|
|
|
|
|
+- 参数数量
|
|
|
|
|
+- 圈复杂度
|
|
|
|
|
+- 嵌套深度
|
|
|
|
|
+- 空 `catch`
|
|
|
|
|
+- 未使用变量
|
|
|
|
|
+- 重复分支
|
|
|
|
|
+- 基础命名规则
|
|
|
|
|
+
|
|
|
|
|
+## 10. 例外规则
|
|
|
|
|
+
|
|
|
|
|
+- 如果确实需要突破本契约,必须说明原因。
|
|
|
|
|
+- 例外只能针对具体代码点,不能作为整个模块放宽标准的理由。
|
|
|
|
|
+- 为了满足行数限制而机械拆分、降低可读性的改动不接受。
|
|
|
|
|
+- 可读性优先于形式主义,但必须能解释为什么更清晰。
|