mcpField
S
smell

smell

github.com/smallnest测试与质量296775 次安装6 周前更新
代码仓库

关于 smell

smell 是面向开发者的代码分析 skill,可检查架构、设计、代码异味和复杂度。它会结合代码上下文验证候选问题,并生成已确认发现的 Markdown 报告。

主要功能

  1. 01架构与依赖扫描
  2. 02代码异味和设计检查
  3. 03算法复杂度热点审查
  4. 04结合上下文验证问题
  5. 05生成带时间戳的 Markdown 报告

适用场景

  1. 01审查整个代码库或指定模块
  2. 02检查近期改动中的设计问题
  3. 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 分类为:臃肿者、面向对象滥用者、变更阻碍者、冗余者、耦合者)。


任务

  1. 明确范围——询问用户要分析项目的哪一部分(整个项目、特定模块或最近的更改)
  2. 使用 find、grep 和 Agent(Explore 子 Agent)扫描代码库,收集候选信号和证据
  3. 在确认发现之前,根据上下文、调用方、历史记录、工作负载和测量结果验证候选项
  4. 生成详细的 Markdown 报告,并保存到 tasks/smell-report-[timestamp].md
  5. 向用户概述已确认的发现,并单独列出候选项

步骤 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")扫描代码库中的架构模式和反模式。并行进行多项探索:

探索命令

并行运行以下任务,以高效收集证据:

  1. 项目结构扫描: 绘制目录树,识别架构风格(分层、模块化单体、微服务等)
  2. 依赖分析: 查找 import/include 模式,检查循环依赖,识别耦合热点
  3. 模块/组件扫描: 识别上帝对象(超过 500 行的文件),检查内聚性和单一职责原则违反情况
  4. 模式检测: 查找已知反模式特征(静态依赖、服务定位器滥用、抽象泄漏)
  5. 测试扫描: 检查测试覆盖模式、测试文件位置、测试与代码的比例
  6. 命名与清晰度扫描: 标记误导性名称、过于通用的名称(Manager、Helper、Util)以及不一致的命名规范
  7. 复杂度扫描: 检测算法复杂度热点——嵌套循环、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:生成报告

确定发现及证据

分三个阶段处理每个候选项:

  1. 候选项检测: 静态模式、文件指标和依赖扫描只会生成候选项。
  2. 上下文验证: 阅读实现及相关调用方;检查变更频率、输入规模、运行时频率、框架限制、是否为生成代码或供应商代码,以及现有缓解措施。
  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)
复杂度异味(算法反模式)

静态复杂度模式只是候选项,并不能证明存在瓶颈。请先测量:

  1. 确认实际输入规模、调用频率、I/O 延迟,以及该路径是否属于热点路径。
  2. 优先使用性能分析器、有代表性的基准测试、查询日志、跟踪记录或明确的操作计数。
  3. 只有在工作负载和影响足以证明问题时,才将候选项判定为发现。
  4. 修复后,使用相同的工作负载重新测量;没有基线时,不要声称性能有所提升。

对于每个复杂度候选项,请说明要测量什么、何时判定为发现以及何时不应标记。保留下文的 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 端点
    • 保留:授权筛选条件、租户隔离、排序、分页和错误语义
  • 正确性检查: 不要获取原有逐项逻辑不会授权访问的记录;保留缺少记录时的行为
重复线性扫描(缺少索引)

在循环中执行线性搜索(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

评论

支持 Markdown