关于 skimmable
用于改进代码变更的可读性,让评审者能快速理解,同时保持行为稳定;仅处理明显无效或与类型不一致的行为。重点是简化数据结构、明确状态并缩小差异,而非增加抽象。
主要功能
- 01删除无关差异
- 02简化数据结构与 API
- 03用判别联合表达明确状态
- 04在输入边界进行断言
- 05扁平化控制流程
适用场景
- 01在评审前理清拉取请求
- 02减少类型化代码路径中的分支
- 03用明确变体替代布尔模式标记
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ckorhonen/ claude-skills --skill skimmable安装后在 Claude Code 中输入/skimmable确认已加载。
详细介绍
易于快速审阅
像一位疲惫的审阅者必须快速看一遍就理解变更那样来审阅。
优先删除代码,而不是解释代码。
保持审阅范围精简:提升可读性、状态建模和差异清晰度;除非当前行为显然已失效、多余或与类型不一致,否则不要改变产品行为。
检查清单
积极应用以下规则:
- 让代码一眼就能看懂。
- 减少代码可能表示的状态数量。
- 减少参数数量。
- 除非调用方确实需要,否则移除可选参数。
- 当行为因模式而异时,用判别联合替代布尔标志和松散的选项对象。
- 穷尽处理所有已知变体。
- 遇到未知变体时立即失败。
- 在类型化核心中信任静态类型;不要为不可能的状态添加防御性分支。
- 在加载数据、解析输入或读取外部系统时,于边界处进行断言。
- 优先使用提前返回,避免嵌套条件判断。
- 保持函数职责内聚;不要拆分成许多微小的包装函数。
- 避免巧妙的辅助函数、间接调用和泛型抽象。
- 除非有确凿需求,否则移除覆盖项、逃生口和自定义扩展点。
- 移除无用路径、临时兼容层和无关改动。
- 倾向于减少代码行数和变动因素。
工作方法
按以下顺序进行:
1. 精简差异
移除无助于 PR 主要目标的改动。
删除:
- 顺带进行的重命名
- 顺手进行的重构
- 没有行为价值的样式调整
- 对已不存在的调用方的兼容代码
- 未使用的辅助函数、类型、分支和注释
如果简化需要另行决定行为,就停下来明确指出,不要把这个决定悄悄混入可读性改进中。
2. 先简化数据形状
先简化类型,再调整控制流。
优先选择:
- 必填字段,而非可选字段
- 一种清晰的输入形状,而非多种灵活形式
- 判别联合,而非布尔值或彼此松散关联的字段
- 有名称的领域类型,而非泛型映射和字符串类型对象
逐一审视每个参数:
- 它是必需的吗?
- 能从其他值推导出来吗?
- 两个参数能合并为一个领域对象吗?
- 它只是为过于灵活的 API 设置的覆盖项吗?
3. 简化控制流
让正常路径简短清晰。
优先选择:
- 提前返回
- 直线式代码
- 尽可能只保留一层缩进
- 在使用处就近进行局部判断
避免:
- 多层嵌套的
if - 为不可能的状态设置防御性的
else分支 - 对预期值使用
try/catch - 将隐式分支散布在辅助函数中
4. 明确报错无效状态
在边界处进行断言。
在以下情况下使用断言:
- 解码外部数据
- 读取文件、环境变量或请求载荷
- 将无类型数据转换为有类型的领域值
- 检查必须成立的不变量
越过边界后,编写简单代码并信任已断言的形状。
5. 穷尽处理变体
当代码依赖某个变体时,应明确该变体并处理所有情况。
优先使用对判别字段的 switch。
如果语言支持,以穷尽性检查或断言结束。
不要静默忽略未知值。
6. 合并不必要的抽象
当小型包装函数只会掩盖实际逻辑时,将其内联。
仅在以下情况下保留辅助函数:
- 能消除真正的重复
- 能表达领域概念
- 能让调用方明显更清晰
不要仅仅为了显得整洁,就把一个易读的 20 行函数拆成 5 个 4 行的辅助函数。
首选模式
保持参数精简且严格
不良迹象:
- 参数超过 3 个
- 存在多个布尔值
- 可选回调、可选覆盖项和可选配置同时出现
- 传递对象上已有的值
优先选择:
- 字段彼此相关时,使用一个必需对象
- 存在多种模式时,使用一个判别联合
- 行为确实不同时,使用不同的函数
优先使用判别联合类型,而不是布尔值
如果布尔值会改变行为,就直接对行为进行建模。
使用:
kind: "draft" | "published"source: "cache" | "network"status: "idle" | "loading" | "success" | "error"
避免:
isDraftuseCache- 在同一对象上同时使用
isLoading、error和data,除非状态机确实允许所有组合
优先使用断言,而不是防御性代码
在边界处断言一次。内部代码信任该断言。
避免重复进行这类检查:
if (!user) returnif (!user.id) returnif (typeof user.id !== "string") return
如果类型和边界验证已经能保证对象形状,就不必再做这些检查。
优先选择一种明确的路径
如果存在默认行为,就直接将其编码进去。
除非调用方确实需要,否则不要添加 sortOrder = "desc"、strategy = "auto"、separator = ","、shouldLog = false 之类的覆盖项。
每个选项都会增加状态数量。
评审提示
评审时使用以下提示:
- 这个函数能否减少参数?
- 这个对象能否去掉可选字段?
- 这个分支能否改为提前返回?
- 这两个布尔值能否合并为一个联合类型?
- 这个辅助函数能否内联?
- 这条错误处理路径能否改为在边界处断言?
- 这段代码能否在失败时明确报错,而不是猜测?
- 这个差异能否减少文件或代码行?
- 如果评审者在 30 秒内快速浏览,是否能看出意图?
示例
减少参数并移除覆盖项
修改前:
function createReport(
userId: string,
includeDrafts = false,
sortBy: "date" | "name" = "date",
sortDirection: "asc" | "desc" = "desc",
limit?: number,
overrides?: { timezone?: string; now?: Date }
) {
const now = overrides?.now ?? new Date()
const timezone = overrides?.timezone ?? "UTC"
return buildReport({
userId,
includeDrafts,
sortBy,
sortDirection,
limit,
now,
timezone,
})
}
修改后:
type ReportScope =
| { kind: "published" }
| { kind: "all" }
function createReport(userId: string, scope: ReportScope) {
return buildReport({ userId, scope, now: new Date(), timezone: "UTC" })
}
优先选择更精简的 API。只有实际调用方有需求时才增加可配置性。
用判别联合类型替代布尔值
修改前:
type SaveState = {
isSaving: boolean
error?: string
receipt?: string
}
修改后:
type SaveState =
| { kind: "idle" }
| { kind: "saving" }
| { kind: "error"; message: string }
| { kind: "saved"; receipt: string }
第二种写法可以避免不可能的组合。
穷尽处理各种变体
修改前:
function badgeColor(status: Status) {
if (status === "success") return "green"
if (status === "error") return "red"
return "gray"
}
修改后:
function badgeColor(status: Status) {
switch (status) {
case "success":
return "green"
case "error":
return "red"
case "pending":
return "gray"
default:
return assertNever(status)
}
}
开发过程中遇到未知变体时,应明确报错。
在边界处断言,在内部信任类型
修改前:
async function loadAccount(accountId: string) {
const raw = await db.accounts.find(accountId)
if (!raw) {
throw new Error("Account not found")
}
if (typeof raw.email !== "string") {
throw new Error("Invalid account email")
}
return raw
}
function sendWelcomeEmail(account: any) {
if (!account) return
if (!account.email) return
mailer.send(account.email)
}
修改后:
async function loadAccount(accountId: string): Promise<Account> {
const raw = await db.accounts.find(accountId)
assert(raw, "Account not found")
assert(typeof raw.email === "string", "Invalid account email")
return raw as Account
}
function sendWelcomeEmail(account: Account) {
mailer.send(account.email)
}
只验证一次。保持类型化核心简单。
优先提前返回,而不是嵌套
修改前:
function visibleItems(items: Item[], user?: User) {
if (user) {
if (user.isAdmin) {
return items
} else {
return items.filter((item) => !item.hidden)
}
} else {
return []
}
}
修改后:
function visibleItems(items: Item[], user?: User) {
if (!user) return []
if (user.isAdmin) return items
return items.filter((item) => !item.hidden)
}
让代码结构更扁平。
不要把逻辑拆分成太多细小函数
修改前:
function checkout(cart: Cart) {
validateCart(cart)
const subtotal = calculateSubtotal(cart)
const tax = calculateTax(subtotal)
const total = calculateTotal(subtotal, tax)
return finalizeCheckout(total)
}
function calculateTotal(subtotal: number, tax: number) {
return subtotal + tax
}
修改后:
function checkout(cart: Cart) {
assert(cart.items.length > 0, "Cart is empty")
const subtotal = sum(cart.items.map((item) => item.price))
const tax = subtotal * TAX_RATE
const total = subtotal + tax
return finalizeCheckout(total)
}
让简单的逻辑靠近其使用位置。
PR 检查关卡
创建或更新 PR 前,请确认:
- 差异中只包含必要的更改。
- 公共 API 更严格或更简单,而不是更可配置。
- 模式都已明确建模。
- 各种变体都已穷尽处理。
- 边界处存在断言。
- 核心逻辑信任类型,而不是反复检查所有内容。
- 函数从上到下易于阅读,嵌套尽可能少。
- 参数数量较少。
- 仅在辅助函数能提升清晰度时才使用它们。
- 最终版本更短、更直白,也更易于快速浏览。
如果需要在巧妙和明确之间做选择,就选明确。
如果需要在灵活和简单之间做选择,就选简单。
如果需要在增加代码和删除代码之间做选择,就优先删除代码。