上下文代码质量 — TypeScript 智能体技能
1. 目的
作为 TypeScript 代码质量智能体,使现有代码更易于理解、维护、测试和扩展。
不要为了显得复杂而重构代码。改进代码,使开发者能够理解:
- 每个函数的作用。
- 业务规则存在的原因。
- 每个变量代表的含义。
- 函数期望和返回的数据。
- 数据缺失或无效时会发生什么。
- 应用程序的哪些部分负责业务逻辑、数据访问和用户交互。
- 更改如何影响现有应用程序行为。
使用领域特定语言,而不是模糊的变量和函数名称。
在处理销售 CRM 代码时,使用清晰的业务概念,如 Lead、Contact、Opportunity、SalesRepresentative、LeadStatus、PipelineStage 和 FollowUpDate。
目标是可理解、强类型、可靠、可测试和可维护的软件。
2. 何时激活
在以下情况下使用此技能:
- TypeScript 代码包含诸如
x、data、obj、temp、thing或result等没有足够上下文的变量。 - 函数名称含糊不清,例如
process()、handle()、manage()或doStuff()。 - 一个函数同时执行验证、业务计算、数据库更新、通知和日志记录。
- 应用程序依赖
any、不安全的类型断言、未解释的字符串或魔法数字。 - 业务规则在多个地方重复。
- 错误被静默吞掉或转换为误导性的成功响应。
- 函数包含深度嵌套的条件或过多的分支。
- CRM 术语在组件、API 端点和数据库模型之间不一致。
- 代码难以测试,除非连接到真实的数据库或外部服务。
- 开发者希望改进现有代码而不改变其预期行为。
3. 核心原则
3.1 首选领域特定名称
名称应解释其在应用程序中的含义。
首选:
const leadId = "lead_123";
const salesRepresentativeId = "user_456";
const nextFollowUpDate = new Date();
const qualifiedLeadCount = 12;
避免:
const id = "lead_123";
const user = "user_456";
const date = new Date();
const count = 12;
当短名称在较小范围内含义明显时是可以接受的,例如在短循环中使用 i。不要机械地扩展每个名称;当额外的上下文有助于理解时再扩展名称。
3.2 使用 TypeScript 表示业务概念
优先使用显式类型、接口、可区分联合和命名领域模型,而不是非结构化对象。
避免将 any 用作逃生舱。对真正不受信任的数据使用 unknown,并在使用前进行验证。
3.3 分离职责
每个函数或模块应有一个明确的主要职责。
CRM 服务不应在一个函数中构造 HTTP 响应、操作 UI 组件、发送电子邮件和执行数据库查询。
3.4 保持预期行为
在重构之前,确定现有的业务规则和预期结果。
不要静默更改:
- 潜在客户资格标准。
- 管道转换规则。
- 销售代表分配。
- 跟进调度规则。
- 重复检测行为。
- 权限和访问控制。
- API 响应格式。
- 数据库语义。
如果现有行为看起来不正确或含糊不清,明确地指出问题,而不是将行为更改伪装成重构。
3.5 优先使用简单代码而非不必要的抽象
除非设计模式、类、泛型、工厂或额外依赖能够解决实际问题,否则不要引入它们。
一个具有描述性名称的简单函数通常比不必要的复杂抽象更好。
4. 编写不佳的销售 CRM 示例
以下代码故意设计存在问题。它说明了潜在客户管理应用程序中常见的代码质量问题。
4.1 之前:编写不佳的代码
// bad-crm.ts
export async function go(d: any, db: any, mail: any) {
try {
let x = d.name;
let e = d.email;
let p = d.phone || "";
let s = d.status || "new";
let v = Number(d.value || 0);
let u = d.user || null;
if (!x || !e) {
return { ok: false, msg: "bad data" };
}
let old = await db.leads.findOne({ email: e });
if (old && old.status != "lost") {
old.v = old.v + v;
old.s = v > 5000 ? "hot" : old.s;
old.p = p || old.p;
await db.leads.update(old.id, old);
if (old.s == "hot") {
await mail.send(
old.email,
"Hot lead",
"Lead is hot!"
);
}
return { ok: true, data: old };
}
let a = {
name: x,
email: e,
phone: p,
status: s,
value: v,
user: u,
created: new Date(),
updated: new Date(),
};
let r = await db.leads.insert(a);
if (v > 5000) {
await mail.send(
e,
"Hot lead",
"Lead is hot!"
);
}
return { ok: true, data: r };
} catch (e) {
console.log(e);
return { ok: false, msg: "error" };
}
}
4.2 智能体应识别的问题
| 问题 | 示例 | 为何重要 | 改进 |
|---|---|---|---|
| 函数名称不清晰 | go() | 没有传达业务操作 | 如果准确描述行为,使用 createOrUpdateLead() |
| 变量不清晰 | d、x、e、p、s、v、u、a、r | 开发者必须推断每个变量的用途 | 使用领域特定名称 |
| 不安全的类型 | 输入、数据库和电子邮件服务的 any | 移除了 TypeScript 捕获许多错误的能力 | 定义接口和显式的输入类型 |
| 隐藏的业务规则 | v > 5000 | 资格阈值未解释 | 定义具名常量或文档化的业务规则 |
| 术语不一致 | status、hot、lost | 潜在客户状态和资格可能代表不同的概念 | 定义显式的潜在客户状态模型 |
| 职责混合 | 一个函数验证、查询、更新、创建、发送电子邮件和处理错误 | 单个行为难以测试 | 分离应用、领域和基础设施职责 |
| 潜在数据不一致 | 数据库更新可能在电子邮件失败之前成功 | 即使潜在客户已保存,调用者也可能收到错误 | 明确定义通知失败行为 |
| 验证薄弱 | 仅检查 x 和 e | 无效的电子邮件地址、错误的值和格式错误的字段可能进入系统 | 验证输入和业务不变量 |
| 隐式状态规则 | 除非状态为 "lost",否则现有潜在客户会被更新 | 此规则的含义被隐藏 | 命名并记录更新策略 |
| 不明确的持久化契约 | db.leads.findOne() 和 .update() 使用未文档化的形状 | 数据库假设难以验证 | 定义仓库接口 |
| 通用错误响应 | "error" | 丢失了有用的上下文,使故障排除复杂化 | 显式处理预期失败,并适当记录意外失败 |
| 响应不一致 | { ok, msg } 和 { ok, data } | 调用者必须推断每种情况下存在哪些字段 | 使用可区分的结果类型或一致的应用错误模型 |
| 可变、松散形状的数据 | old.v、old.s、old.p | 字段名称与看似持久化的模型不匹配 | 定义一致的 Lead 类型 |
| 不明确的重复策略 | 通过电子邮件合并现有记录 | 合并的确切含义被隐藏 | 文档化哪些字段可以更新以及值如何聚合 |
| 难以测试的依赖 | 数据库和电子邮件服务作为 any 传递 | 契约和失败情况未通过编译器检查 | 定义窄接口并注入依赖 |
重要: 这是一个说明性示例,不是完整的生产级 CRM 实现。重构必须首先确认现有 CRM 的实际行为、数据模型、持久化语义和业务规则。
5. 重构工作流程
按照以下工作流程改进现有的 TypeScript 代码。
第 1 步:理解上下文
在修改相关代码之前先检查它。
确定:
- 函数的用途。
- 其调用者和依赖项。
- 其输入和输出格式。
- 数据库模式或仓库契约。
- 领域实体和业务术语。
- 现有的验证和授权规则。
- 当前测试和预期行为。
- 错误处理和副作用。
必要时阅读周围模块。不要仅从函数名称推断其完整契约。
第 2 步:记录当前行为
用通俗语言描述现有逻辑。
对于上面的示例,观察到的行为是:
- 从输入读取潜在客户详细信息。
- 如果名称或电子邮件缺失,拒绝请求。
- 通过电子邮件查找现有潜在客户。
- 如果存在匹配的潜在客户且其状态不是
"lost",将提交的值添加到现有值中。 - 当结果值超过阈值时,将该现有潜在客户标记为
"hot"。 - 当提供了新的非空电话号码时,替换其电话号码。
- 更新记录,如果结果状态为
"hot",则发送通知。 - 否则,创建带有时间戳的新记录。
- 当提交的值超过阈值时,通知潜在客户。
- 返回成功或失败对象。
此描述有意区分观察到的实现行为和预期的业务行为。例如,代码在一个通知路径中测试现有记录的结果值,但在另一个路径中测试提交的值。不要将这些规则视为等同。
第 3 步:改进命名
用表达其角色的名称替换通用名称。
| 糟糕的名称 | 更好的名称 |
|---|---|
go | createOrUpdateLead |
d | leadInput |
x | leadName |
e | emailAddress,或在 catch 块中为 error |
p | phoneNumber |
s | leadStatus |
v | leadValue |
u | salesRepresentativeId |
old | existingLead |
a | newLead |
r | createdLead |
db | leadRepository 或 databaseClient,取决于其实际角色 |
mail | emailNotificationService |
改进后的名称必须准确表示该值。不要将一个值重命名为 salesRepresentative