代码坏味道识别

共 18 题
📑 题目列表 18 题
#
★★★

1. Long Method(过长函数)的危害与 Extract Method 的拆分判据中以"意图"而非"行数"切分

过长函数(Long Method)会带来哪些危害?在应用 Extract Method 重构时,应当依据什么标准来切分函数,是以代码行数还是以"意图"为依据?

  • 过长函数对可读性、可维护性与可测试性的具体危害
  • Extract Method 的拆分判据:以语义意图为边界而非行数
  • 提取后函数命名与注释的职责归属

过长函数的危害在于它把"一段连续逻辑"的多个独立意图混在一起,导致阅读者难以快速定位"某一段在做什么"、测试难以针对单一行为进行、修改时容易产生副作用并提高出错率。Extract Method 的核心判据不是行数,而是"意图"——当一段代码能用一个有意义的名字来描述它所做的事情时,就应当把它提取成独立方法,使方法名成为这段代码的注释,从而让上层函数只描述"做什么"而非"怎么做"。行数只是参考信号,真正的标准是"该方法是否只做一件事、是否能在同一抽象层级上被描述"。

依赖行数切分会导致机械切分,把本属同一意图的代码生硬拆开,反而增加跳转成本;而以意图切分则让每个方法都有一个清晰的职责与命名,符合单一职责原则在函数层面的体现。实践中常以"注释的存在"作为信号:如果一个方法里需要注释来解释某段代码,就该考虑提取。提取后方法名应能替代原注释,这既是重构也是可读性的提升。

// 重构前:一个方法承担多个意图
public void processOrder(Order order) {
    double total = 0;
    for (LineItem item : order.getItems()) {
        total += item.getPrice() * item.getQuantity();
    }
    if (total > 1000) {
        Customer c = order.getCustomer();
        if (c.getLevel().equals("VIP")) {
            c.setDiscount(0.9);
        }
    }
    this.sendInvoice(order, total);
}

// 重构后:按意图拆分
public void processOrder(Order order) {
    double total = computeTotal(order);
    applyVipDiscount(order, total);
    this.sendInvoice(order, total);
}
private double computeTotal(Order order) { /* 计算总价 */ }
private void applyVipDiscount(Order order, double total) { /* 折扣逻辑 */ }
#
★★★

2. God Class(上帝类)的识别(过多职责、过多字段、过高变更频率)与 Extract Class 的分解

什么是上帝类(God Class)?如何通过职责、字段数量与变更频率来识别它?如何用 Extract Class 对其进行分解?

  • 上帝类的三个识别信号:过多职责、过多字段、过高变更频率
  • 上帝类与上帝对象(God Object)的成因
  • Extract Class 的分解思路与职责内聚原则

上帝类是一个承担了过多职责、拥有过多字段和方法的类,往往成为系统的"中枢"并被大量其他类依赖,导致其变更频繁、难以测试、难以复用。识别信号包括:类名很难用一个职责概括、字段和方法数量远超同类、同一个类因不同原因频繁变更(发散式变化)、以及被大量类依赖。分解时用 Extract Class 把相互关联的字段与方法按职责聚成新的类,让原类只保留"协调者"角色,新类各司其职。

上帝类本质是职责没有内聚,把多个不相关的概念塞进一个类里。分解的关键是"按变更原因分组"——把同一批因相同原因而变更的字段和方法划为一组,这样每个新类都只因为一个原因而改变,符合单一职责原则。分解后还应通过依赖注入让上帝类引用新类,从而降低耦合、提升可测试性。

#
★★★

3. Feature Envy(依恋情结)中方法大量使用另一类的数据,信号指向 Move Method

什么是 Feature Envy(依恋情结)?它通常出现在什么场景?当方法大量使用另一类的数据时,为什么应该考虑 Move Method 重构?

  • Feature Envy 的定义:方法对另一类数据的"依赖"胜过自身
  • 判断依据:方法访问的字段与调用方法中,来自其他类的比例
  • Move Method 的移动方向:把方法移到数据所属的类

Feature Envy 指一个方法大部分时间都在访问另一个类的数据或调用另一个类的方法,而对自己所在类的数据很少使用,仿佛"嫉妒"别的类。这表明该方法的职责本质上属于数据所在的那个类,而不是当前类。此时应使用 Move Method 把方法搬移到被依赖的类中,让实现与数据同处一个类,从而提升内聚、降低耦合。

判断时可以用"方法所访问的字段与调用的方法中,来自哪个类更多"作为信号。若大部分来自其他类,就说明方法的归属错了。Move Method 后往往还需要把相关参数一并整理,甚至进一步用 Extract Method 拆出更小的行为。这也是"数据与操作它的行为应在一起"这一面向对象原则的具体落地。

#
★★★

4. Shotgun Surgery(霰弹式修改)中一处需求变更需改动散落多处的代码,指向职责聚合

什么是 Shotgun Surgery(霰弹式修改)?它与发散式变化的区别是什么?为什么它暗示需要职责聚合?

  • Shotgun Surgery 的定义:一处变更需改动散落多处的代码
  • 与 Divergent Change 的区别:修改方向不同
  • 化解手段:把散落逻辑聚合到单一职责处(Move Method/Field、Extract Class)

Shotgun Surgery 指"一处需求变更需要修改散落于多个类或方法中的代码",每次改动都要"打一枪换一个地方",容易遗漏、易出错。它暗示散落的逻辑实际上属于同一个职责,应当被聚合到一起。化解手段是使用 Move Method、Move Field、Extract Class 等把相关行为集中到一个类,让未来的同一类变更只在单一位置发生。

与 Divergent Change(发散式变化)恰好相反:发散式变化是"一个类承担多个不同原因的变更",而霰弹式修改是"一个原因的变更被分散到多个类"。两者都指向职责归属错误,只是修改方向不同。聚合后,同一类变更的触发点集中,改动量减少,遗漏风险降低。

#
★★★

5. Divergent Change(发散式变化)与 Parallel Inheritance Hierarchies(平行继承体系)的识别与化解

什么是发散式变化(Divergent Change)和平行继承体系(Parallel Inheritance Hierarchies)?如何识别它们并分别化解?

  • Divergent Change:一个类因多个不同原因而变更
  • Parallel Inheritance Hierarchies:某一类新增子类时另一类也需相应新增子类
  • 化解手段:Extract Class / 用组合与委派替代强制的平行继承

发散式变化指"一个类因多个不同原因而发生变化",说明它承担了多个不相关的职责,化解手段是使用 Extract Class 按不同变更原因拆分。平行继承体系指"当你要为某个类添加子类时,必须同时为另一个类添加子类",两套继承层次被强制绑定,改动时容易遗漏。化解手段是让平行的类通过组合、委派或接口进行关联,而不是强制同步扩充子类。

发散式变化的核心是"多个变更轴"集中在同一个类上,违反了单一职责;平行继承体系则是"多个变更轴"被强制同步到两套继承树上,遇到新增类型时必须同时改动两处。判断平行继承的重要信号是"霰弹式修改"——给一个类加子类时总得给另一个类也加子类。化解时用组合替代继承,让一个对象持有另一个对象,从而打破强制同步。

#
★★★

6. Introduce Parameter Object 与 Preserve Whole Object 的取舍中参数列表过长 vs 传递整个对象

当参数列表过长时,引入参数对象(Introduce Parameter Object)和保留整个对象(Preserve Whole Object)两种重构如何取舍?

  • Introduce Parameter Object:把一组相关参数封装成新对象
  • Preserve Whole Object:把整个对象传给方法而非拆散字段
  • 两者的适用场景与取舍边界

Introduce Parameter Object 是把一组逻辑上相关的参数(如 startDate、endDate、room)封装成一个新对象传入,减少参数个数、提升可读性、便于复用。Preserve Whole Object 是当调用方已经持有某个对象时,直接把整个对象传给方法,而不是把它的多个字段拆开传递,从而避免"依赖对象内部字段"的耦合。取舍上:若参数彼此紧密相关且会在多处出现,用参数对象;若参数恰好来自某个已存在的对象,且方法需要该对象的多个字段,用保留整个对象;但若方法只需要对象的个别字段,传整个对象反而会引入不必要的耦合,此时应传字段或用参数对象。

两者目标都是"减少过长的参数列表",但机制不同。参数对象是"新造一个聚合",保留整个对象是"复用已有对象"。取舍的关键是"依赖面":传整个对象会让方法依赖对象全部公开接口,若方法只用到少量字段,这种依赖是过宽的,应优先用参数对象或传具体字段。实践上常先看调用方是否已持有相关对象,再决定是否再造参数对象。

#
★★★

7. 坏味道的组合诊断中上帝类、过长方法与依恋情结共现时如何识别根因并整体重构,而非逐点修补?

当上帝类、过长方法和依恋情结同时出现时,应如何识别根因并整体重构,而不是一个个单独修补?

  • 坏味道组合背后的共同根因:职责归属错误
  • 整体重构的思考顺序:先定位职责边界,再逐层拆分
  • 逐点修补的弊端

上帝类、过长方法和依恋情结往往相伴而生,它们共同指向同一个根因:职责没有正确归属,导致大量逻辑被塞在一个类里,方法过长、又不断去访问其他类的数据。正确做法是先从整体定位"这个类真正负什么责、哪些字段/方法属于其他职责",即先做职责的重新划分,再用 Extract Class 把内聚的字段方法组独立成类,用 Move Method 把依恋别类数据的方法搬走,用 Extract Method 拆解过长方法。三者是同一重构思路下的配套动作,而非相互独立的修补。

逐点修补会导致"按下葫芦浮起瓢":只拆长方法不解决职责混乱,只搬方法不解决上帝类。整体重构的关键是"先画职责边界图":把类按"变更原因"或"数据归属"分组,确定每个新类的单一职责,再自上而下地应用 Extract Class 和 Move Method。这样一次重构同时解决多个坏味道,且每一小步都有测试验证。

#
★★

8. Move Method / Move Field 的职责再分配判断中依据"谁用得最多"

如何判断一个方法或字段应移动到哪个类?"谁用得最多"这一依据如何应用?

  • Move Method / Move Field 的适用场景
  • 移动依据:被访问频率、数据归属、依赖方向
  • 移动后的整理(参数、访问方法)

Move Method 和 Move Field 用于把职责放错位置的成员移到更合适的类。判断依据是"谁用得最多":如果某个方法或字段被另一个类大量访问,说明它更该属于那个类;同时要考虑数据归属——方法应靠近它操作的数据,字段应靠近使用它的方法。移动后还要清理原访问关系,必要时把原类对它的引用改为通过新类访问。

"谁用得最多"是直观判据,但需结合"数据归属":即使某个方法被当前类调用较多,若它操作的数据全在另一个类,仍应移动。移动时还要注意依赖方向,避免造成循环依赖。移动后应相应地调整测试,保证行为不变。这个重构的本质是"把职责放到离数据最近的地方"。

#
★★

9. Inline Method / Inline Class(内联)作为过度抽象的反向重构手段

什么是 Inline Method 和 Inline Class?它们为何是过度抽象的反向重构手段?

  • Inline Method:把简单方法体直接内联到调用处
  • Inline Class:把类合并回使用者,消除冗余抽象
  • 应用于过度抽象/过度封装的场景

Inline Method 是 Extract Method 的逆操作,当一个方法体足够简单、方法名并不比实现更清晰,或方法只被调用一次时,直接把它内联回调用处,减少间接层。Inline Class 是 Extract Class 的逆操作,当一个类职责过薄、只被一个类使用、且没有独立的变更理由时,把它合并回使用者,消除冗余的抽象层。两者都是"过度抽象"的修正手段。

过度抽象造成"间接层过多":方法只是转发、类只是包装,阅读成本超过收益。判断标准是"这个间接层是否带来了清晰度和复用价值"——若方法名不比实现更清晰、类只有一个使用者且与使用者同生命周期,就应内联。内联减少跳转、简化结构,是"少即是多"的重构。

#
★★

10. Replace Magic Number with Symbolic Constant 与 Replace Type Code with Subclasses 的应用

什么是魔法数字?Replace Magic Number with Symbolic Constant 和 Replace Type Code with Subclasses 分别适用于什么场景?

  • 魔法数字的定义与危害
  • Replace Magic Number with Symbolic Constant:用命名常量替代魔法数字
  • Replace Type Code with Subclasses:用类型编码字段的分支替换为多态子类

魔法数字是代码中直接出现、含义不明的字面量。Replace Magic Number with Symbolic Constant 把这种字面量替换为有意义的命名常量,如 double PI = 3.14159,提升可读性、便于统一维护。当类型编码(如用一个 int 字段表示员工类型)被用于 switch 分支时,可应用 Replace Type Code with Subclasses:把每个类型变成一个子类,用多态替代条件分支,让行为随类型分散到各子类中。

两者针对不同层级:符号常量解决"字面量含义不明",是多态化前的简单手段;类型编码替换为子类是更深入的重构,消除基于类型编码的分支逻辑。选择时看类型编码是否影响行为:若只是数据标识,用常量或枚举即可;若类型决定行为分支,则用子类或多态更优。

#
★★

11. Data Clumps(数据泥团)的识别与封装为值对象

什么是数据泥团(Data Clumps)?如何识别它并封装为值对象?

  • Data Clumps 的定义:一组字段/参数反复结伴出现
  • 识别信号:参数列表、字段组中重复出现的相同组合
  • 封装为值对象(Value Object)的价值

数据泥团指"一组数据总是结伴出现",比如 namestreetcity 反复出现在多个方法的参数或类的字段中,常见的判断是"如果删掉其中一个,其余还有意义吗?"——没有意义就说明它们是一团。应封装为一个值对象(如 Address 类),替代重复的参数列表和字段组合,让数据与相关操作(如校验、格式化)附着在对象上,减少重复并提升可维护性。

值对象是"不可变、按值相等"的对象。把数据泥团封装成值对象后,"参数列表过长"也一并缓解,且能承载该数据相关的行为与校验逻辑。识别时关注"字段组是否总一起出现、是否一起传递",一旦确认就应提取,而不是等到数据量变大再处理。

#
★★

12. Refused Bequest(被拒绝的遗赠)中子类不用父类成员,暗示继承误用,应以组合替代

什么是被拒绝的遗赠(Refused Bequest)?它为什么暗示继承被误用?如何用组合替代?

  • Refused Bequest 的定义:子类不使用父类部分成员
  • 成因:继承不是"是一个"关系,而是为了复用
  • 化解:以组合替代继承,或把不需要的部分移出

被拒绝的遗赠指子类只继承父类的一小部分成员、却"拒绝"了父类的大部分接口,通常子类只需要些许字段或方法。这暗示继承被误用——继承本应表达"是一个"的关系,这里却只是贪图复用,导致子类暴露了父类的无关接口。化解手段是:若子类确实不该继承父类全部,则用组合替代继承,把需要的成员作为字段注入;或把父类中不被需要的部分提取到别处,让继承关系更贴近真实语义。

判断标准是"子类是否真能视为父类的一种"——若答案是否定的,就不要继承。组合优先于继承能避免子类承担父类无关的职责与接口。若子类大部分成员不用、只复用少量,通常组合+委托更合适;若只是少数成员不用,可考虑把父类细分后调整继承关系。

#
★★

13. "魔法数字"与"魔法字符串"的治理边界中何时抽常量,何时引入配置?

魔法数字和魔法字符串何时应抽成常量,何时应引入配置?治理边界是什么?

  • 常量与配置的区分:行为固定 vs 运行期可调
  • 抽常量的适用条件:含义固定、全系统一致、无需外部调整
  • 引入配置的适用条件:随环境/客户变化、需外部调整

治理边界的关键是"该值是否随环境或业务变化"。含义固定、全系统一致、变更频率低、且由代码逻辑决定的值,应抽为命名常量,如 final int MAX_RETRY = 3。而会随环境(开发/生产)、客户、部署或业务需要调整、需要外部人员修改的值,应引入配置(如配置文件、环境变量、配置中心),而不是硬编码或写死在代码里。判断时问"谁会改这个值、多久改一次、是否分环境"——只在代码内改且极少改,抽常量;需要外部改或分环境,用配置。

过度抽常量也可能导致"常量泛滥",把一次性使用的字面量也抽出来反而增加噪音。合理边界是:常量面向"语义命名"与"统一维护",配置面向"外部可调"与"环境差异"。两者结合使用,避免把本该配置的值硬编码进常量,也避免把稳定常量无谓地放进配置。

#
★★

14. 重复代码(Clone)的四种类型(Type-1 精确复制、Type-2 重命名/参数化、Type-3 近似、Type-4 语义级)各自如何检测与消除?

重复代码(Clone)的四种类型分别是什么?各类型如何检测与消除?

  • Type-1 精确复制、Type-2 重命名/参数化、Type-3 近似、Type-4 语义级
  • 各类型的检测手段与消除策略
  • 语义级重复的难度与处理

四种重复代码类型为:Type-1 是逐字完全相同的代码块,可用文本比对工具直接检测,消除手段是 Extract Method 提取公共方法;Type-2 是仅在标识符名、类型或空隙上不同的代码,可用词法/语法分析工具检测,消除手段是参数化后提取公共方法;Type-3 是结构相似但语句有增删改的近似重复,需用树/图结构比对工具检测,消除较难,可能需要适度抽象或容忍;Type-4 是结构完全不同但语义相同(实现方式不同,功能一致)的语义级重复,无法靠文本工具自动检测,主要靠人工评审与代码责任分配来消除。

检测难度从 Type-1 到 Type-4 递增:Type-1/2 可完全工具化,Type-3 需更智能的语法分析,Type-4 需结合领域知识与人工判断。消除策略也随类型调整:直接提取、参数化提取、抽象接口,到语义级需重新设计职责划分。值得注意的是,Type-3/4 过度消除可能引入不必要的抽象,需权衡"重复 vs 抽象"的成本。

#
★★

15. 坏味道检测的自动化边界中哪些可工具化、哪些依赖人工评审,两者如何分工配合?

坏味道检测中,哪些可以自动化工具实现,哪些依赖人工评审?两者如何分工配合?

  • 可工具化的坏味道:语法/结构层面的可量化信号
  • 依赖人工的坏味道:涉及语义、设计意图、领域判断
  • 自动化与人工的分工配合

可工具化的坏味道通常具有明确定义、可量化、侧重结构或语法特征,如重复代码(Type-1/2)、过长方法、过长参数列表、魔法数字、圈复杂度、未使用变量、God Class 的规模指标等,可由静态分析工具(如 SonarQube、Checkstyle、PMD、ESLint)自动扫描。依赖人工评审的坏味道涉及语义、设计意图与领域知识,如 Feature Envy 的"谁用得最多"、被拒绝的遗赠的继承语义、Type-4 语义级重复、发散式变化的变更原因归属等,这些需要理解业务与设计才能准确判断。分工上:自动化工具做"第一遍粗筛",把可疑点标注出来并给出量化指标,人工评审聚焦于自动化结果中需要语义判断的部分,并复核工具报告中误报较多的类别,两者结合提高效率与准确率。

自动化擅长"选择题"(可量化、有明确模式),人工擅长"判断题"(需语义与上下文)。合理分工是让工具负责可量化的扫描与门槛拦截,人工负责设计层面的判断与工具误报的甄别。同时不断用人工评审结果校准工具的规则,降低误报率,让两者形成良性循环。

#

16. 坏味道与"何时不重构"的权衡,稳定且低变更频率的代码可容忍坏味道

什么时候可以容忍坏味道而不重构?稳定且低变更频率的代码为什么不必着急重构?

  • 基于变更频率的权衡:重构应服务于"未来变化"
  • 稳定代码的重构风险与收益
  • 机会主义重构的原则

重构的收益来自"未来会变化的代码"——只有代码还会被修改时,改善其结构才能持续降低改动成本。对于稳定、低变更频率、且行为已通过测试验证的代码,即使存在坏味道,也往往不值得冒险重构,因为重构本身有引入回归的风险,而收益几乎为零。此时应记录为技术债,待该代码进入活跃变更期时再处理。

这是"机会主义重构"思想:只在靠近要修改的代码时、且改动收益明确时才重构,而不是追求"全代码库零坏味道"。判断依据是变更热点——用版本历史找出高频变更区域,优先治理这些区域的坏味道;对长期不变的代码,容忍坏味道是理性的成本决策。

#

17. 如何用 AI 工具批量识别坏味道,同时防止误报淹没人工评审?

如何用 AI 工具批量识别坏味道?如何防止误报淹没人工评审?

  • AI 工具在坏味道识别中的优势与局限
  • 误报的来源与代价
  • 过滤策略:优先级排序、置信度、人工抽样复核

AI 工具(如代码分析 LLM、语义克隆检测)能批量扫描代码库,识别语义级重复、潜在 Feature Envy 等传统工具难覆盖的坏味道,覆盖范围广、速度快。但 AI 基于模式推断,缺乏完整上下文,会产生误报,且报告量大,若不处理会淹没人工评审。防止误报淹没的关键是:为报告设置置信度与严重度排序,让高置信度、影响大的问题优先;对低置信度结果做人工抽样复核以校准规则;把 AI 结果与静态分析结果融合交叉验证;并限制单次评审的批量规模,让人工聚焦在最有价值的部分。

AI 的价值在于"扩大候选集",人工的价值在于"精判"。应把 AI 当作前置筛选器而非最终裁决者,配合置信度阈值、优先级排序、抽样校验和反馈闭环,持续降低误报率。同时用变更热点信息为 AI 结果加权,让"高风险区域"的坏味道优先覆盖。

#

18. 坏味道治理的优先级中高变更频率区域的坏味道为何优先处理,如何用变更热点定位?

为什么高变更频率区域的坏味道要优先处理?如何用变更热点(change hotspot)来定位?

  • 变更频率与重构收益的关系
  • 变更热点分析的方法与工具
  • 优先级排序的实践

重构的核心收益是"降低未来改动的成本",因此高变更频率区域的坏味道对成本影响最大,应优先处理。低频变更区域的坏味道即使存在,也极少被触碰,重构收益低。变更热点可通过版本控制历史分析:统计每个文件/类的提交次数、修改行数、关联缺陷数,找出"频繁修改且改动量大"的高热区域,再聚焦这些区域的坏味道进行治理。

变更热点分析把"代码结构"与"演进成本"结合,用历史数据回答"哪里改得最多、最贵"。实践上可由工具或脚本从 Git 等 VCS 的提交记录中生成热点热力图,优先覆盖热点内的高危坏味道。这与"机会主义重构"一致:重构发生在最有机会节省成本的地方,而非盲目全库铺开。