关于 smell
smell 是面向开发者的代码分析 skill,可检查架构、设计、代码异味和复杂度。它会结合代码上下文验证候选问题,并生成已确认发现的 Markdown 报告。
主要功能
- 01架构与依赖扫描
- 02代码异味和设计检查
- 03算法复杂度热点审查
- 04结合上下文验证问题
- 05生成带时间戳的 Markdown 报告
适用场景
- 01审查整个代码库或指定模块
- 02检查近期改动中的设计问题
- 03排查依赖关系和复杂度热点
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ smallnest/ goal-workflow --skill smell安装后在 Claude Code 中输入/smell确认已加载。
附带文件README.mdtest-prompts.json
详细介绍
Smell — 架构坏味道检测器
分析代码库,找出违反软件架构原则、反模式、代码“坏味道”以及算法复杂度热点。生成一份全面且可执行的 Markdown 报告。
知识库: 此技能汇集了源自行业研究与实践的架构模式、反模式、代码坏味道和算法复杂度启发式方法,其中包括 Martin Fowler / Kent Beck 编写的经典代码坏味道目录(由 refactoring.guru 分类为:臃肿者、面向对象滥用者、变更阻碍者、冗余者、耦合者)。
任务
- 明确范围——询问用户要分析项目的哪一部分(整个项目、特定模块或最近的更改)
- 使用
find、grep和Agent(Explore 子 Agent)扫描代码库,收集候选信号和证据 - 在确认发现之前,根据上下文、调用方、历史记录、工作负载和测量结果验证候选项
- 生成详细的 Markdown 报告,并保存到
tasks/smell-report-[timestamp].md - 向用户概述已确认的发现,并单独列出候选项
步骤 1:明确分析范围
询问用户:
What scope should I analyze?
A. Entire project (thorough, may take time)
B. Specific module/directory: [please specify]
C. Only recently changed files (git diff)
D. Only architectural-level issues (skip low-level code smells)
如果用户未指定,小型项目(< 100 个文件)默认选择 A,大型项目默认选择 C。
步骤 2:收集证据
使用 Explore 子 Agent(通过 Agent,并设置 subagent_type: "Explore")扫描代码库中的架构模式和反模式。并行进行多项探索:
探索命令
并行运行以下任务,以高效收集证据:
- 项目结构扫描: 绘制目录树,识别架构风格(分层、模块化单体、微服务等)
- 依赖分析: 查找 import/include 模式,检查循环依赖,识别耦合热点
- 模块/组件扫描: 识别上帝对象(超过 500 行的文件),检查内聚性和单一职责原则违反情况
- 模式检测: 查找已知反模式特征(静态依赖、服务定位器滥用、抽象泄漏)
- 测试扫描: 检查测试覆盖模式、测试文件位置、测试与代码的比例
- 命名与清晰度扫描: 标记误导性名称、过于通用的名称(Manager、Helper、Util)以及不一致的命名规范
- 复杂度扫描: 检测算法复杂度热点——嵌套循环、N+1 查询、重复扫描、循环内排序、渲染路径中耗费大量资源的重复计算
关键启发式方法
启发式方法只是候选信号,而非已确认的发现。行数、嵌套层级、命名或 Big-O 匹配结果,必须结合代码的职责、调用方、变更历史、工作负载和有意设置的约束进行验证。不得仅依据阈值判定严重程度。
| 类别 | 异味 | 检测启发式 |
|---|---|---|
| 架构 | 大泥球 | 没有清晰的目录结构;所有内容都在根目录或单一平铺目录中;没有关注点分离 |
| 架构 | 违反分层边界 | 内层导入外层;基础设施代码位于领域/核心层 |
| 架构 | 缺少架构 | 没有 src/、lib/、core/ 分离;SQL 与 UI 代码内联;HTTP 处理程序与业务逻辑混杂 |
| 架构 | 分布式单体 | 微服务共享数据库;服务无法独立部署 |
| 架构 | 贫血领域模型 | 模型/实体类只有 getter/setter,没有行为;所有逻辑都在服务中 |
| 架构 | 不必要的 CQRS | 为简单的 CRUD 操作分别创建读/写模型;复杂度徒增 |
| 架构 | 过度分层架构 | 层级/层次过多,只增加了没有实际价值的透传代码 |
| 架构 | 过度抽象 | 间接层/接口/泛型太多,导致难以跟踪代码 |
| 架构 | 面向未来的架构 | 为可能永远不会出现的需求预留推测性的灵活性(试图预测未来) |
| 架构 | 技术爱好者式架构 | 仅仅因为技术新颖,就在生产环境采用亮眼但未经验证的技术,而非因为它适用 |
| 架构 | 杀鸡用牛刀式架构 | 用重量级架构/技术解决简单问题 |
| 架构 | 云图/Visio 架构 | 图表与实际代码和运行时情况脱节 |
| 耦合 | 循环依赖 | 模块 A 导入 B,B 又导入 A;可通过导入图分析检测 |
| 耦合 | 内容耦合 | 一个模块直接访问另一个模块的内部/私有成员 |
| 耦合 | 公共耦合 | 全局变量/共享可变状态过多;滥用单例 |
| 耦合 | 标记耦合 | 只需要几个字段,却传递大型数据结构 |
| 内聚 | 上帝对象 | 单个类/模块 > 500 行;> 20 个公有方法;处理互不相关的关注点 |
| 内聚 | 散弹式修改 | 单个更改需要触及跨越多个无关模块的 5+ 个文件 |
| 内聚 | 特性嫉妒 | 某方法调用其他类方法的次数多于调用自身类方法的次数 |
| 内聚 | 数据泥团 | 同一组三个或更多参数反复出现在多个方法签名中 |
| 设计 | 泄漏的抽象 | 通过接口暴露实现细节(数据库查询、HTTP 调用) |
| 设计 | 静态依赖 | 过度使用静态方法;静态状态导致无法测试 |
| 设计 | 滥用服务定位器 | 传递 DI 容器,而非使用恰当的构造函数注入 |
| 设计 | 违反 SOLID | 违反 SRP、违反 OCP(针对类型的 switch/if-else 链)、违反 ISP(臃肿接口) |
| 设计 | Switch 语句 | 针对类型代码的相同 switch/if-else 链出现在多个位置;应使用多态 |
| 设计 | 拒绝继承 | 子类继承了不使用的方法/字段,或重写这些方法并抛出异常/不执行任何操作 |
| 设计 | 接口不同的替代类 | 两个类做相同的事,但方法名称不同 |
| 设计 | 并行继承体系 | 在一个继承体系中创建子类,就必须在另一个体系中创建对应子类 |
| 设计 | 推测性通用化 | 存在未使用的抽象类、钩子、参数或泛型,只为“未来需求”而设(YAGNI) |
| 设计 | 不完整的库类 | 因第三方类缺少所需方法而对其进行包装/打补丁 |
| 内聚 | 发散式修改 | 一个模块因许多无关原因而被修改(与散弹式修改相反) |
| 内聚 | 数据类 | 只有字段和 getter/setter、没有行为的类(贫血数据包) |
| 内聚 | 懒惰类 | 类/模块做的事情太少,不足以证明其存在的合理性 |
| 耦合 | 不恰当的亲密关系 | 两个类过多地访问彼此的私有/内部部分 |
| 耦合 | 消息链 | 调用链过长,例如 a.getB().getC().getD()(违反迪米特法则) |
| 耦合 | 中间人 | 只将每次调用转发给另一个类的类 |
| 代码 | 临时字段 | 实例字段仅在某些情况下设置/使用,其他时候为空 |
| 代码 | 重复代码 | 相同/相似逻辑出现在 3+ 个位置;复制粘贴模式 |
| 代码 | 过长方法 | 方法 > 50 行;嵌套过深(> 3 层) |
| 代码 | 参数列表过长 | 参数 > 4 个的方法 |
| 代码 | 基本类型偏执 | 使用字符串/整数代替领域类型(例如,使用 string email 而非 Email 类型) |
| 代码 | 魔法数字/字符串 | 使用没有命名常量的硬编码字面值 |
| 代码 | 用注释掩盖问题 | 用过多注释解释糟糕的代码,而不是重构 |
| 代码 | 死代码 | 未使用的导入、不可达代码、注释掉的代码块 |
| 测试 | 没有测试 | 模块的测试覆盖率为零 |
| 测试 | 测试与实现耦合 | 测试断言内部实现细节,而非行为 |
| 测试 | 测试速度慢 | 测试执行真实 I/O、数据库调用、网络请求,且未使用模拟 |
| 命名 | 名称含糊 | 过度使用 Manager、Handler、Processor、Helper、Util、Service、Data、Info,但缺少上下文 |
| 命名 | 命名不一致 | 混用 snake_case 和 camelCase;同一概念采用不同命名模式 |
| 可读性 | 深层嵌套(箭头反模式) | 循环/条件嵌套 > 3 层;向右漂移、难以追踪的“箭头”形状 |
| 复杂度 | 嵌套循环(O(n^2)+) | 循环内嵌套循环;for 中嵌套 forEach;map 中嵌套 map;嵌套迭代暗示多项式复杂度 |
| 复杂度 | 重复线性扫描 | 在循环中使用 includes()/indexOf()/.find();在列表上进行 O(n*m) 成员检查,而不是使用 Set/Map |
| 复杂度 | 循环内排序 | 在迭代代码中调用 .sort() 或 sorted();本可只排序一次,却反复执行 O(n log n) 排序 |
| 复杂度 | N+1 查询模式 | 在循环内进行数据库/API/HTTP 调用;每次迭代都执行 fetch/query/execute/findMany,而不是批量处理 |
| 复杂度 | 渲染路径重复计算 | 组件渲染主体中出现 .filter().map().sort() 链;昂贵的转换未使用记忆化 |
| 复杂度 | 两两比较 | 嵌套迭代比较每个元素与其他所有元素;本可用排序加双指针实现 O(n log n),却使用 O(n^2) |
| 复杂度 | 不必要的重复计算 | 相同的昂贵计算重复执行且未缓存;缺少 useMemo/memo/惰性求值 |
| 复杂度 | 错误的数据结构 | 使用数组,而 Set/Map 可提供 O(1) 查找;适合使用 Queue/Heap/Stack 时却使用 List |
步骤 3:生成报告
确定发现及证据
分三个阶段处理每个候选项:
- 候选项检测: 静态模式、文件指标和依赖扫描只会生成候选项。
- 上下文验证: 阅读实现及相关调用方;检查变更频率、输入规模、运行时频率、框架限制、是否为生成代码或供应商代码,以及现有缓解措施。
- 确认发现: 将根因、受影响路径、故障/变更场景和修复方向相同的候选项合并为一项发现。
按独立根因统计发现,绝不能按其涉及的原则数量统计。使用一个主要原则,并可选填相关原则。SOLID 是一个总括标签;如果证据支持更具体的视角,应将 SRP、OCP 或 DIP 用作主要标签,不要另外创建一项独立的 SOLID 发现。
11 项原则标准矩阵
| 原则 | 确认依据 | 常见误报/限制 |
|---|---|---|
| SOLID | 设计问题涉及多个 SOLID 视角,或无法可靠地归入更具体的视角 | 不要重复创建特定的 SRP/OCP/DIP 发现 |
| DRY | 同一业务规则或知识必须在多处修改 | 语法相似,但预期会独立演进 |
| KISS | 额外层、间接层或机制增加了成本,却没有带来可观察到的收益 | 能消除实际复杂性的小型抽象 |
| YAGNI | 未使用的扩展点、参数、适配器或推测性的需求 | 现有边界或变更所必需且经过测试的接缝 |
| SRP | 多个彼此独立的变更原因,有职责或变更历史作为依据 | 仅凭文件大小或方法数量 |
| 开闭原则 (OCP) | 添加已知变体时,反复修改稳定的分支逻辑 | 简单且局部的条件判断 |
| 依赖倒置原则 (DIP) | 高层策略直接依赖具体基础设施,影响替换或测试 | 为单一且稳定的实现添加接口 |
| 组合 | 继承导致非预期耦合、拒绝行为,或变化维度无法分离 | 机械地替换所有有效的继承关系 |
| 关注点分离 | 业务策略、I/O、呈现或持久化关注点跨边界渗透 | 有意设计得很精简的边界适配器 |
| 快速失败 | 无效输入、状态或依赖一直传播,直到远处的操作才失败 | 有意采用的聚合、重试或延迟验证语义 |
| 先测量 | 性能、规模或优化方面的论断缺乏基线或有代表性的工作负载 | 被静态分析报告为瓶颈的复杂度 |
对于每项已确认的发现,分别记录证据强度(Measured、Observed 或 Inferred)和置信度(High、Medium 或 Low/Candidate)。证据强度不代表严重程度。
严重程度标准
- 严重: 涉及正确性、可靠性、安全性、数据一致性或经测量的系统级影响;通常需要高置信度。
- 警告: 影响范围明确,或反复产生变更/运行时成本,并带来具体的维护或运行时后果。
- 建议: 局部、低频或影响有限的改进,且有证据支持。
- 需要测量的候选项: 影响未知的静态信号;不计入严重程度总数,并单独列在报告章节中。
按以下结构生成报告:
# Architecture Smell Report
**Project:** [project-name]
**Scope:** [scope description]
**Date:** [date]
**Analyzer:** smell skill (Ducc)
---
## Executive Summary
[2-3 paragraph summary of confirmed findings only: architectural style detected, overall health assessment, and top 3-5 critical issues. Mention candidates separately.]
---
## Architectural Style Detected
[Identify the architectural style: Layered, Modular Monolith, Microservices, Hexagonal, Clean Architecture, or Big Ball of Mud]
### Style Expectations vs. Reality
| Expectation | Reality | Status |
|-------------|---------|--------|
| [e.g., Clear layer separation] | [what was found] | ✅/⚠️/🔴 |
---
## Findings by Category
### 🔴 Critical Issues (Must Fix)
[Issues that fundamentally undermine architecture]
### 🟡 Warnings (Should Fix)
[Issues that degrade maintainability but don't block function]
### 🔵 Suggestions (Nice to Fix)
[Minor improvements that would increase quality]
### Candidates Requiring Measurement
[Static candidates whose runtime impact, change frequency, or workload is not yet established. These do not count toward severity totals.]
---
## Detailed Findings
### Finding #1: [Title]
- **Category:** [Architecture/Coupling/Cohesion/Design/Code/Testing/Naming/Complexity]
- **Severity:** 🔴 Critical / 🟡 Warning / 🔵 Suggestion
- **Anti-Pattern:** [Name of anti-pattern]
- **Location:** [file:line references and relevant callers]
- **Confidence:** [High/Medium]
- **Evidence strength:** [Measured/Observed/Inferred]
- **Failure or change scenario:** [Concrete scenario]
- **Primary principle:** [Most specific principle]
- **Related principles:** [Explanatory only; do not count separately]
- **Description:** [What was found and why it's a problem]
- **Evidence:** [Code, dependency, history, or measurement]
- **Measured/observed impact:** [Reach, frequency, consequence, or baseline]
- **Recommendation:** [Smallest justified refactoring]
- **Verification:** [How to prove behavior and impact]
---
## Dependency Graph Analysis
[Summary of module dependencies, circular dependencies found, coupling hotspots]
---
## Module Health Scorecard
| Module | Lines | God Object Risk | Coupling | Cohesion | Test Coverage | Health |
|--------|-------|----------------|----------|----------|---------------|--------|
| [name] | [N] | [Low/Med/High] | [Low/Med/High] | [Low/Med/High] | [% or N/A] | 🟢/🟡/🔴 |
---
## Smell Distribution
Count only deduplicated confirmed findings by their primary category. Related principles and candidates do not affect these totals.
| Category | Count | Critical | Warning | Suggestion |
|----------|-------|----------|---------|------------|
| Architecture | [N] | [N] | [N] | [N] |
| Coupling | [N] | [N] | [N] | [N] |
| Cohesion | [N] | [N] | [N] | [N] |
| Design | [N] | [N] | [N] | [N] |
| Code | [N] | [N] | [N] | [N] |
| Testing | [N] | [N] | [N] | [N] |
| Naming | [N] | [N] | [N] | [N] |
| Complexity | [N] | [N] | [N] | [N] |
---
## Refactoring Roadmap
Order work by impact, confidence, dependency sequence, and verification cost—not by principle count or smell name. Put only high-confidence, verifiable findings in Immediate Actions; for candidates, recommend the next measurement instead of a rewrite.
### Immediate Actions (This Sprint)
1. [Actionable fix 1]
2. [Actionable fix 2]
### Short-Term (1-3 Months)
1. [Structural improvement 1]
2. [Structural improvement 2]
### Long-Term (3-12 Months)
1. [Architectural transformation 1]
2. [Architectural transformation 2]
---
## Appendix: Anti-Pattern Reference
[A condensed reference of anti-patterns checked, with brief descriptions]
步骤 4:保存并呈现
将报告保存到 tasks/smell-report-[YYYY-MM-DD-HHmm].md,并向用户提供简要摘要。
反模式知识库
本节记录此技能所了解的架构反模式和代码异味。
架构反模式
万能大泥球
最常见的事实标准架构。结构杂乱、规模庞大,无法看出任何架构。其特征包括:
- 远隔元素之间随意共享信息
- 重要状态全局化或重复存储
- 结构已面目全非,或从未定义过
- 反复采用权宜之计修补(“用胶带和铁丝凑合”)
- 成因:时间压力、成本、经验不足、复杂性、变化、规模
- 补救措施: 定义架构边界,逐步重构,应用 SHEARING LAYERS、KEEP IT WORKING
分布式单体
必须一起部署的微服务。症状包括:
- 服务共享数据库
- 服务调用形成同步链
- 变更需要协调部署
- 补救措施: 解耦数据存储,引入异步消息传递,明确界定上下文
贫血领域模型
领域对象只有 getter/setter(数据包),所有逻辑都放在服务中。违反了:
- “告知,而非询问”原则
- DDD 中的丰富领域模型模式
- 补救措施: 将行为移入领域对象,仅在跨聚合操作时使用领域服务
上帝对象
知道太多或做得太多的类。其特征包括:
- 超过 500 行或有超过 20 个公共方法
- 处理无关的职责
- 难以单独测试
- 违反单一职责原则
- 补救措施: 将内聚的方法组提取到专用类中
泄漏的抽象
暴露实现细节的抽象。迹象包括:
- 接口方法以实现命名(例如,
SaveToPostgres、FetchFromRedis) - 调用方捕获特定于实现的异常
- 通过抽象暴露配置细节
- 补救措施: 从调用方的角度设计接口,隐藏实现细节
静态依赖
过度使用静态方法/状态。问题包括:
- 无法测试(无法模拟静态调用)
- 隐式依赖
- 静态状态存在线程安全问题
- 补救措施: 使用依赖注入,将无状态静态方法改为实例方法
滥用服务定位器
使用服务定位器而非依赖注入。问题包括:
- 隐式依赖(依赖在构造函数中不可见)
- 出现运行时错误而非编译时错误
- 测试困难
- 补救措施: 使用构造函数注入,在组合根注册依赖
违反层边界(整洁/洋葱/六边形架构)
在分层架构中:
- 整洁架构: 外层(框架)渗入内层(用例、实体)
- 洋葱架构: 基础设施问题进入领域核心
- 六边形架构: 业务逻辑与特定适配器耦合,而非与端口耦合
- 补救措施: 应用依赖倒置,定义清晰的端口接口
过度使用 CQRS
将 CQRS 用于简单的 CRUD。迹象包括:
- 为简单的数据访问分离读写模型
- 在事件无法增加业务价值时使用事件溯源
- 不必要的复杂性
- 补救措施: 仅在读写模型确实不同或扩展需求不同时使用 CQRS
垂直切片污染
在垂直切片架构中:
- 切片之间相互耦合(一个功能直接调用另一个功能)
- 共享服务类削弱切片的独立性
- 补救措施: 使用事件/消息进行跨切片通信;必要时复制简单逻辑
软件架构十大误区
一组描述过度设计和设计不足的架构级反模式。它们的共同点是:架构脱离了实际需求和现实。 另一个极端(架构过少)同样是不良信号。
过度分层/多层架构
“层层叠叠。”添加超出问题所需的层级:
- 每一层都只是将调用转发给下一层,没有任何转换或价值
- 一个简单的读取操作就需要跨越 4 层访问 6 个以上的类
- 补救措施: 合并纯转发层;只保留承担实际职责的层
过度抽象
层层叠加抽象,直到代码难以理解:
- 对只有一种实现的场景,使用过多接口、泛型、工厂和间接调用
- 不经过多次跳转就无法看出实际运行的是什么
- 解决方法: 内联只有一种实现的抽象;只在确实存在变化的地方抽象(三次原则)
面向未来的架构
为无法预测的假想未来需求构建解决方案:
- 设置无人使用的扩展点、插件系统和配置选项
- 大多数推测性的灵活性都是无用功——与推测性通用化和 YAGNI 密切相关
- 解决方法: 针对当前已知需求构建;等出现真正的第二种情况时再增加灵活性
技术爱好者式架构
因为架构师喜欢某种新颖或时髦的技术,就将其投入生产:
- 未经验证就采用未经证实的技术,没有确认它是否适合该问题或能否扩展
- 追逐趋势而非稳定性
- 解决方法: 根据实际需求评估技术;优先选择经过验证的工具;先做原型,再做决定
过度设计的架构
用与问题不成比例的架构和技术解决简单问题:
- 为只有少数用户的 CRUD 应用使用微服务、事件溯源和 k8s
- 解决方法: 让架构的复杂程度与问题规模相匹配(KISS);从简单方案开始,在确有必要时再演进
云端 / Visio 架构
只存在于漂亮图表中、与代码和运行时现实脱节的“架构”:
- 图表与实际部署不符;图中的方框和箭头在代码中找不到对应
- 解决方法: 确保架构文档以实际系统为依据,并经过实际系统验证
关于另一个极端的说明: 完全缺乏架构(没有边界、没有结构)同样是一种坏味道——参见 泥球 和缺失架构。欠设计和过度设计都是失败。
耦合与内聚坏味道
循环依赖
模块 A → 模块 B → 模块 A。可通过以下方式检测:
- 分析导入关系图
- 出现“初始化前无法访问”错误
- 解决方法: 提取共享接口或公共模块,应用依赖倒置
内容耦合
一个模块直接修改另一个模块的内部状态。迹象:
- 跨模块边界直接访问字段
- 滥用
friend/ 包级私有访问 - 解决方法: 使用公共 API,封装内部状态
公共耦合(全局状态)
多个模块依赖共享的全局可变状态:
- 全局变量、带可变状态的单例
- 环境上下文(例如,
CurrentUser静态属性) - 解决方法: 使用参数传递、依赖注入,并显式表示状态
标记耦合
只需要少数几个字段,却传递整个数据结构:
- 函数接收大型 DTO,却只使用其中一个字段
- 解决方法: 创建针对性参数或更小的接口(ISP)
散弹式修改
单一变更需要修改许多文件:
- 添加一个字段就要改动不同模块中的 5+ 个文件
- 解决方法: 整合相关行为,应用单一职责原则
特征迷恋
某个方法使用其他类的方法多于自身的方法:
- 方法多次调用
other.foo()、other.bar()、other.baz(),却很少调用自身方法 - 解决方法: 将该方法移到它所迷恋的类中
数据泥团
同一组字段在多个地方反复出现:
(street, city, zip)出现在 5 个方法签名中- 解决方法: 将其提取为值对象
发散式变化
同一个模块 / 类因许多无关原因而反复修改(与散弹式修改相反):
- 同一个类中,“数据库变更时我总要改这三个方法,UI 变更时又要改那两个方法”
- 解决方法: 根据变化维度拆分类(单一职责原则)
不恰当的亲密关系
两个类与彼此的内部实现纠缠过深:
- 访问另一个类的私有字段、紧密的双向引用
- 解决方法: 将方法/字段移到所属的类中,提取共享类,或改用委托
消息链
冗长的导航链,例如 a.getB().getC().getD().doThing():
- 客户端与整个对象图耦合;违反迪米特法则
- 解决方法: 隐藏委托——在第一个对象上添加一个方法,用于返回客户端所需的内容
中间人
一个类几乎把所有工作都委托给另一个类:
- 大多数方法只是转发调用;增加了间接层,却没有带来价值
- 解决方法: 移除中间人,让客户端直接与实际对象交互(内联委托)
平行继承体系
每次在一个继承体系中添加子类时,都必须在另一个继承体系中添加一个子类:
Shape/ShapeRenderer、Employee/EmployeePermission同步增长- 解决方法: 合并继承体系,或让一个继承体系引用另一个,而不是让两者镜像对应
代码级坏味道
过长方法
- 方法超过 50 行(或根据语言特点自行设定标准)
- 嵌套超过 3 层
- 混杂多个抽象层级
- 解决方法: 在相同抽象层级提取方法,并组合使用
参数列表过长
- 参数超过 4 个的方法
- 控制行为的布尔标志
- 解决方法: 引入参数对象、拆分方法、移除标志参数
重复代码
- 在 3 个或更多地方出现相同或近乎相同的逻辑
- 仅有细微变化的复制粘贴代码
- 解决方法: 提取共享方法,应用模板方法或策略模式
基本类型偏执
使用基本类型而非领域类型:
- 用
string表示 Email、PhoneNumber、URL - 用
int表示 Money、Age、Quantity - 使用
decimal,却未指定 Currency - 解决方法: 创建包含验证和行为的值对象
魔法数字/字符串
- 没有说明的硬编码字面值
- 使用
if (status == 3),而不是if (status == Status.COMPLETED) - 解决方法: 提取命名常量或枚举
用注释遮丑
- 解释代码在做什么的注释(代码应能自我说明)
- 被注释掉的代码块
- 不断累积却未解决的“TODO”注释
- 解决方法: 重构以使代码清晰,删除无用代码,并将 TODO 作为问题跟踪
深层嵌套(箭头反模式)
循环和条件语句嵌套过深,导致代码不断右移,形成“箭头”形状:
if { if { for { if { ... } } } }—— 很难追踪任意时刻哪些条件成立- 通常在一个函数中缩进超过 3 层
- 解决方法: 使用守卫子句/提前返回,将嵌套块提取为方法,反转条件,或用多态替换条件判断
死代码
- 未使用的导入、变量、函数
- 无法到达的分支
- 版本控制中的注释掉的代码
- 解决方法: 删除它(如有需要,git 历史会保留代码)
数据类
一个只有字段和 getter/setter,却没有有意义行为的类:
- “数据包”,其他类会从中取出数据,并从外部进行操作
- 在类级别上与贫血领域模型密切相关
- 解决方法: 将处理数据的行为移入该类(“告诉,不要询问”)
懒惰类
一个已不再承担足以证明其存在价值的工作的类/模块:
- 重构后遗留下来的类/模块,或从未发展起来的抽象
- 解决方法: 将其内联到调用方中,或合并继承体系
推测性通用性
为假想的未来需求而添加的抽象、钩子、参数或泛型:
- 未使用的抽象基类、未使用的参数、“以防万一”的配置
- 违反 YAGNI 原则
- 解决方法: 移除未使用的抽象;等出现真实的第二个用例时再添加
临时字段
仅在某些情况下设置/使用,其他时候为空的实例字段:
- 仅在某个算法运行期间填充的字段,其余时间会让读者感到困惑
- 改进方法: 将该字段及使用它的方法提取到独立类中(提取类/引入方法对象)
测试异味
没有测试
- 测试覆盖率为零的模块
- 没有单元测试的业务逻辑
- 改进方法: 先编写特征测试,再添加行为测试
测试与实现耦合
- 断言内部方法调用、私有状态或实现细节的测试
- 在重构但行为未变时就会失败的测试
- 改进方法: 通过公共 API 进行测试,断言行为而非实现
测试环境依赖
- 未使用模拟对象,依赖文件系统、网络、数据库或系统时钟的测试
- 非确定性测试(不稳定测试)
- 改进方法: 使用测试替身、控制环境、使用依赖注入(DI)
复杂度异味(算法反模式)
静态复杂度模式只是候选项,并不能证明存在瓶颈。请先测量:
- 确认实际输入规模、调用频率、I/O 延迟,以及该路径是否属于热点路径。
- 优先使用性能分析器、有代表性的基准测试、查询日志、跟踪记录或明确的操作计数。
- 只有在工作负载和影响足以证明问题时,才将候选项判定为发现。
- 修复后,使用相同的工作负载重新测量;没有基线时,不要声称性能有所提升。
对于每个复杂度候选项,请说明要测量什么、何时判定为发现以及何时不应标记。保留下文的 Big-O 分析和正确性检查,但不要仅凭语法推断严重程度。
嵌套循环(O(n^2) 及更差)
两个或更多循环相互嵌套,产生多项式复杂度。
- 检测: 在一个
for/while中嵌套另一个for/while;在forEach/map中嵌套forEach/map;循环中包含另一个循环(任意深度) - 影响: 双重嵌套为 O(n^2),三重嵌套为 O(n^3);数据规模中等时,复杂度就会急剧增长
- 改进方法:
- 为内层集合构建 Map/Set 索引 → O(n+m)
- 使用排序加双指针方法 → O(n log n)
- 在迭代前对数据分组/分桶
- 对区间/范围问题使用扫描线算法
- 正确性检查: 顺序是否重要?是否存在重复键?原逻辑取的是第一个、最后一个还是所有匹配项?
N+1 查询模式
循环体内执行数据库查询、API 调用或 I/O 操作。
- 检测: 在任何循环结构内使用
fetch()/axios()/query()/execute()/findMany()/findOne()/findUnique()/select()/where() - 影响: 产生 1 + N 次往返,而不是 1 次;网络延迟会随项目数量增加
- 改进方法:
- 按 ID 批量获取:
SELECT * FROM x WHERE id IN (...),然后在内存中联接 - 使用 ORM 预加载/
include/preload/DataLoader - 使用接受数组的批量 API 端点
- 保留:授权筛选条件、租户隔离、排序、分页和错误语义
- 按 ID 批量获取:
- 正确性检查: 不要获取原有逐项逻辑不会授权访问的记录;保留缺少记录时的行为
重复线性扫描(缺少索引)
在循环中执行线性搜索(includes、indexOf、.find、in_array),而使用 Set/Map 可实现 O(1) 查找。
- 检测: 在循环体内使用
.includes()/.indexOf()/.find()/.findIndex()/in_array()/contains() - 影响: 复杂度为 O(n*m),而不是 O(n+m)——每次迭代都会扫描整个集合
- 改进方法: 在循环开始前构建一个
Set(用于成员判断)或Map(用于键→值查找) - 正确性检查: 转换为 Set 后,相等语义是否会改变?注意 JavaScript 对象的身份比较与值比较,以及 Python 的可哈希性
循环内排序
在循环体内排序,导致不必要地重复执行 O(n log n) 的工作。
- 检测: 在任何迭代代码块内使用
.sort()/sorted()/sort() - 影响: O(k * n log n),而非 O(n log n)——排序重复执行 k 次
- 解决方法:
- 在循环外排序一次
- 如果需要增量获取前 K 项,则维护一个堆(PriorityQueue)
- 使用二分查找,在有序集合中插入
- 正确性检查: 每个中间有序状态是否会被外部观察到?比较器是否依赖循环局部状态?
渲染路径重复计算(UI 复杂度)
在 UI 组件的渲染主体中执行开销较大的数据转换(filter→map→sort 链),并在每次渲染时重新计算。
- 检测: React/Vue/Svelte 组件函数主体内的
.filter().map().sort().reduce()链;JSX/TSX 中function Component()或const Component = () =>内部 - 影响: 即使输入未变,每次状态变化都会重新推导;处理大型集合时会出现卡顿
- 解决方法:
- 使用
useMemo/computed/derived,并设置正确的依赖数组 - 将推导逻辑移至选择器、加载器或服务端
- 对长列表进行虚拟化(窗口化)
- 仅当子组件渲染受影响时,才稳定回调和对象属性
- 使用
- 正确性检查: 依赖数组必须包含每个语义输入;记忆化不得掩盖对可变输入的修改
两两比较
使用双重嵌套迭代,将每个元素与其他所有元素进行比较。
- 检测: 两个嵌套循环遍历相同或相似的集合,并比较元素对
- 影响: 配对、重叠检测、冲突检查、最近邻场景的复杂度为 O(n^2)
- 解决方法:
- 使用排序 + 双指针进行配对/范围匹配
- 使用扫描线检测区间重叠
- 使用空间哈希或网格分桶处理邻近关系
- 使用并查集处理连通性
- 正确性检查: 顺序稳定性;相等情况下的决胜规则
不必要的重复计算(缺少记忆化)
相同的纯计算在输入相同的情况下重复执行,且未使用缓存。
- 检测: 热路径中使用相同参数重复调用相同函数;重复执行开销较大的转换;递归调用未使用记忆化
- 影响: 浪费线性/多项式级工作;递归斐波那契模式尤其糟糕(使用记忆化可将 O(2^n) 降至 O(n))
- 解决方法: 添加记忆化/缓存并进行适当的失效处理;酌情使用
lru_cache/memoize/useMemo
错误的数据结构
针对访问模式使用了次优的数据结构。
- 检测:
- 频繁检查成员关系时使用 Array/List → 应使用 Set
- 按键值查找时使用 Array/List → 应使用 Map/Object
- 使用
shift()/pop(0)将 Array 用作队列(每次出队 O(n))→ 应使用合适的 Queue - 在 Array 中进行有序插入(每次插入 O(n))→ 应使用 Heap
- 解决方法: 替换为复杂度与访问模式相匹配的数据结构:
- Set → O(1) 的 has/add/delete
- Map → O(1) 的 get/set
- Heap → 用于优先级操作的 O(log n) push/pop
- Queue/Deque → O(1) 的 enqueue/dequeue
不应标记的情况
- 冷路径: 只在启动、加载配置或 N 很小(< 100)时运行的复杂度问题通常不值得修复
- 有意的权衡: 清晰易读的 O(n) 代码;改用 O(n log n) 只会增加复杂度,且没有可衡量的收益
- 已优化: 已使用 Map/Set;已实现批量加载;已使用记忆化
设计原则违规
使用第 3 步中的规范矩阵作为报告依据。这些设计检查用于完善候选问题的检测,不会为每条原则各自创建一条问题。
- SOLID 总则: 仅当多个 SOLID 方面的问题有相同根因,或无法可靠地使用更具体的分类时才使用。
- SRP: 确认存在彼此独立的变更原因;仅凭规模不足以判定。
- OCP: 确认因实际存在的变体而需要反复修改;不要用推测性的多态替换简单的局部条件判断。
- LSP: 子类型必须保持基类契约的前置条件、后置条件和不变量。报告为
SOLID/LSP。 - ISP: 确认客户端依赖了自己不会使用的方法。报告为
SOLID/ISP。 - DIP: 确认高层策略与具体机制耦合,并因此损害测试或替换能力;仅有一个实现并不意味着必须使用接口。
- DRY: 消除重复的共享知识,而非恰好相似的语法。
- KISS 和 YAGNI: 拒绝缺乏证据支持其未来变化或作用的抽象。
- 组合: 当继承暴露了不需要的行为,或将彼此独立的变化维度绑定在一起时,优先考虑组合;这不是机械规则。
- 关注点分离: 确认策略、表示、持久化或 I/O 泄漏到了预期边界之外。
- 快速失败: 在无效状态的源头附近检测它,同时保留既定的错误、重试、事务和清理语义。
- 先测量: 将未经测量的优化主张降级为候选问题,而不是再报告一条“原则违规”。
面向对象滥用(摘自 Fowler / refactoring.guru)
Switch 语句(类型码条件判断)
基于类型码或枚举进行分支的重复 switch / if-else 链:
- 相同的条件结构在多个位置重复出现
- 添加新类型时必须修改每个 switch(违反 OCP)
- 解决方法: 用多态(Strategy/State)替换条件判断,或用子类替换类型码
拒绝继承
子类继承了自己不需要的方法或字段:
- 重写继承的方法,使其抛出异常、不执行任何操作或执行无关操作
- 表明继承关系不合理
- 解决方法: 将未使用的成员下移,或用委托替代继承
接口不同的备选类
两个类执行相同的职责,但暴露的方法名称不同:
- 可互换的类使用
sort()和arrange()、getUser()和fetchUser()等不同方法 - 解决方法: 统一接口(重命名方法、提取公共超类/接口)
不完整的库类
第三方库类缺少所需方法,且无法修改:
- 围绕该库散布辅助函数或复制粘贴的包装代码
- 解决方法: 引入 Foreign Method,或用适配器/本地扩展类包装该类
其他 refactoring.guru 代码异味已在上文对应的主题章节中介绍: 发散式变化、数据类、惰性类、推测性泛化、 临时字段、平行继承体系、不当亲密关系、 消息链和中间人。
边界情况与回退方案
| 情况 | 处理方式 |
|---|---|
| 用户未指定范围 | 对于超过 200 个文件的代码库,默认检查近期变更(git diff);否则进行完整分析 |
| 项目没有明确的架构 | 根据证据报告“泥球”(Big Ball of Mud),并建议逐步重构 |
| 空项目/单体仓库 | 报告架构分析需要代码;请用户指定模块 |
| 不支持的语言 | 报告一般的结构观察结果;注明特定语言的检查有限 |
| 报告文件路径冲突 | 在文件名后追加 -2、-3 等 |
| 用户希望快速检查 | 仅运行 Critical 级别的扫描,跳过 Code 和 Naming 类别 |
| 用户只想检查一个类别 | 仅针对该类别进行分析,跳过其他类别 |
报告输出示例
🔍 Architecture Smell Analysis Complete
Project: goal-workflow
Style: Modular Monolith (with some layering violations)
Files Analyzed: 47
Health: 🟡 Fair
Critical: 3 | Warnings: 6 | Suggestions: 9
🔴 Critical Issues:
1. Anemic Domain Model — `models/` classes have only getters/setters,
all logic in `services/`. Violates DDD Rich Domain Model principle.
2. N+1 Query Pattern — `services/order.ts:142` fetches user per order in loop;
should batch-load users by IDs (O(n*m) → O(n+m)).
3. Static Cling — `util/ApiClient.ts` uses all static methods,
making consumer code untestable.
🟡 Warnings:
1. God Object — `services/workflow.ts` at 847 lines handles too many concerns
2. Nested Loop O(n^2) — `analytics.ts:89` pairwise comparison of events;
sort+two-pointer would be O(n log n)
3. Leaky Abstraction — `repositories/user.ts` exposes MongoDB query syntax
4. Duplicated Code — validation logic duplicated across 4 controllers
5. Circular Dependency — `auth` ↔ `user` modules depend on each other
6. Magic Numbers — ~23 hardcoded values without named constants
Full report: tasks/smell-report-2026-05-27-1530.md