DRY、KISS、YAGNI 原则

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

1. DRY 与 WET 的边界中哪些重复是必要的(如测试用例、模板代码、安全检查样板)

DRY 与 WET 的边界在哪里?哪些重复是必要的(测试用例、模板代码、安全检查样板)?

  • DRY 与 WET 的概念
  • 必要重复的类型
  • 判断边界

DRY(Don't Repeat Yourself)指"每个知识/逻辑在系统中只有一份单一权威来源",WET(Write Everything Twice)指重复写。但并非所有重复都该消除,存在"必要重复":测试用例(故意与实现分离,验证独立);模板代码(boilerplate,框架/语言样板,如标准 HTTP 处理结构,抽抽象反而增加复杂度);安全检查样板(安全边界应显式重复,避免隐式魔法);跨边界的数据(如 API 契约在各端重复,但应由 IDL 单一来源)。边界判断:DRY 针对"知识/逻辑/意图",而非"文本/样式"。若重复的是"同一份可变逻辑",应 DRY;若重复是"独立存在的实例/必要的显式",应保留 WET。

滥用 DRY 会把"看似相同实则独立"的代码强行合并,造成错误抽象。判断核心是"重复的是知识还是产物":同一知识单源,必要产物(测试、样板、安全显式)可重复。误解 DRY 正是"重复代码 vs 错误抽象"问题的根源。

#
★★★

2. Dan North 的"重复 vs 抽象"决策树中区分重复代码(结构重复)、重复意图(语义重复)、复制粘贴(无意重复)

说明 Dan North 关于"重复 vs 抽象"的决策树,区分结构重复、语义重复与无意重复?

  • 三种重复类型
  • 各自的处理方式
  • 决策树思想

Dan North("DRY 与 Rule of Three")指出要做区分:重复代码(结构重复)——代码文本相似但表达不同意图,不应盲目合并,因为合并会造成错误耦合;重复意图(语义重复)——不同代码表达同一逻辑/意图,才应抽象合并(这是真正的 DRY 目标);复制粘贴(无意重复)——直接复制粘贴的相同代码,应合并。决策树:先问"这是同一意图吗?"是→抽象合并;否→再看"是结构相似但意图不同吗?"是→保留(可能需重构命名);复制粘贴→合并。核心是"按意图而非文本判断重复"。

决策树的核心是"意图"而非"文本"。结构相似但意图不同(如两个都计算金额但语义不同)不应合并,否则错误抽象。语义重复(同一业务规则多处实现)才需单源。区分三者是 DRY 正确应用的关键。

#
★★★

3. Rule of Three(第三次出现时抽象)的边界;过早抽象(WET → DRY)的代价与案例

Rule of Three(第三次出现时抽象)的边界是什么?过早抽象(WET → DRY)的代价与案例?

  • Rule of Three 规则
  • 过早抽象的代价
  • 案例

Rule of Three:代码出现第三次时才抽象。第一次按直觉写,第二次复制,第三次才值得抽取公共抽象——因为此时才积累足够信息判断"真正的共同点与变化点"。过早抽象(第一/二次就抽象)的代价:抽象基于不充分信息,可能提炼出错误的共同点,导致抽象与真实需求不匹配、难以维护、过度设计;且抽象层增加理解成本。案例:网上商城两个模块各有一段"计算折扣"逻辑看似相同,第二个出现时立即抽取公共 DiscountService,但实际两处折扣规则不同(一个按会员、一个按活动),合并后被迫加大量分支,反而复杂。按 Rule of Three 等第三个真实案例出现再抽象,能基于真实变体设计。

Rule of Three 是"抽象时机"的工程实践。过早抽象是 YAGNI 的违反——为未验证的"共同点"预付成本。等三个真实出现,才能识别"哪些不变、哪些变",抽象才准确。它平衡 DRY 与过度设计。

#
★★★

4. 抽象时机与团队规模中 5 人团队与 50 人团队对抽象阈值的差异

抽象时机与团队规模有何关系?5 人团队与 50 人团队对抽象阈值有何差异?

  • 团队规模对抽象的影响
  • 抽象阈值差异
  • 权衡

团队规模影响抽象阈值的"成本收益":抽象/DRY 的收益是"多处修改只改一处、减少不一致",但其成本是"抽象层复杂度、理解门槛、耦合"。5 人小团队:代码库小、沟通成本低、改动面窄,抽象收益有限,宜降低抽象阈值(少抽象、多 KISS),避免为复用而抽象导致过度设计。50 人大团队:代码库大、多人并行、改动冲突多、沟通成本高,重复代码的"改一处漏一处"风险大,宜提高抽象阈值(更积极 DRY、建立公共库/单一来源),因为重复的维护成本随团队规模放大。本质上,抽象阈值 = 重复带来的成本 vs 抽象带来的复杂度,团队越大重复成本越高。

团队规模改变的是"重复的代价"。大团队里重复代码的同步成本高、冲突多,所以更应 DRY;小团队里抽象的管理成本可能超过收益,倾向 KISS。抽象阈值应随团队规模与领域复杂度动态调整。

#
★★★

5. 框架代码与业务代码的相似度判断;何时抽公共模块(lib、kit)、何时保留各自独立

如何判断框架代码与业务代码的相似度?何时抽公共模块(lib、kit)、何时保留独立?

  • 相似度判断
  • 抽公共模块的时机
  • 独立保留的时机

判断框架/业务代码相似度,先看"重复的是知识还是只是结构":若两处是同一业务规则、同一领域逻辑,应抽公共模块;若只是"长得像"但业务语义不同(如两个模块各自的价格规则),应保留独立。抽公共模块(lib/kit)的时机:出现多个真实调用方、共享的是稳定领域知识、抽取不引入强耦合、抽象能减少不一致。保留独立的时机:共享内容极少、语义不同、抽取会引入耦合或过度抽象、或两个模块演进方向不同。核心原则:"共享稳定知识"抽 lib、"偶然相似"保留独立;避免把"公共"当成"垃圾场"。

判断核心是"相似的本质"——知识共享 vs 结构相似。知识共享(同一规则)应单源,结构相似(不同规则)保留。抽 lib 要谨慎,避免把不同演进方向的代码强行合并成脆弱的公共模块。

#
★★★

6. 模板代码(boilerplate)的处理中 Lombok、JUnit Extension、Kotlin 委托属性、Macro

如何处理模板代码(boilerplate)?举 Lombok、JUnit Extension、Kotlin 委托属性、Macro 的例子?

  • 模板代码的成因
  • 各语言的消除手段
  • 权衡

模板代码(boilerplate)是重复的、无业务含义的样板结构,处理方式是"减少手写样板":Java 用 Lombok (@Getter/@Builder/@Data) 自动生成 getter/setter/构造器;JUnit 用 Extension 把测试通用逻辑(如资源初始化、参数化)抽成可复用扩展,避免每个测试重复样板;Kotlin 用委托属性(by lazy/by delegate)简洁地实现属性逻辑(如懒加载),避免手写样板;C/C++/Rust 等用 Macro 编译期生成代码消除样板。权衡:这些工具/语法减少样板提升简洁性,但引入"魔法"(隐式生成、编译期展开)可能降低可读性与调试难度。应选择"样板消除收益 > 魔法成本"的技术。

boilerplate 是"重复的机械结构",DRY 但不必每次都手写。Lombok、JUnit Extension、Kotlin 委托、Macro 都是"用声明/元编程消除样板"的手段。权衡是"简洁 vs 魔法":样板过多累赘,魔法过多难懂,应取平衡。

#
★★★

7. 跨语言共享的"逻辑重复"——Java 与 Kotlin/Go/Scala 同步实现,如何通过 IDL 单一来源解决

跨语言共享的"逻辑重复"(Java 与 Kotlin/Go/Scala 同步实现)如何通过 IDL 单一来源解决?

  • 跨语言逻辑重复
  • IDL 单一来源
  • 代码生成

跨语言项目(如 Java 后端 + Go 服务 + Scala 组件)中,同一接口/数据结构常需在多种语言重复实现,导致"逻辑重复"——改一处需求要同步改多处,易漏改。解决方式:用 IDL(接口描述语言,如 Protobuf、Thrift、OpenAPI)作为单一来源,定义接口、数据结构、契约,再通过代码生成器自动生成各语言的代码(gRPC/Protobuf 生成 Java/Go/Python 等)。这样"契约"只定义一次,各语言实现由生成器产出,避免手工重复与不一致。IDL 单一来源正是 DRY 在跨语言场景的落地:知识(契约)单源,各端产物自动生成。

跨语言 DRY 的关键是"契约单源 + 代码生成"。IDL 定义一次,各语言代码生成,消除手工同步重复。这比"各语言各写一份"更可靠。注意:IDL 只解决"契约/结构"重复,业务逻辑仍需各语言实现,但契约的一致性是核心。

#
★★

8. "Shotgun Surgery" 与 "Divergent Change"中识别是真正的耦合还是合理的分层

识别 Shotgun Surgery(霰弹枪式修改)与 Divergent Change(发散式变化),判断是真正耦合还是合理分层?

  • 两个坏味道的概念
  • 判断耦合 vs 分层
  • 重构方向

Shotgun Surgery(霰弹枪式修改):一个变化需要修改多个类,分散且易漏改——提示"关注点被分散到多处",应凝聚(把相关逻辑收拢)。Divergent Change(发散式变化):一个类因多个不同原因而变化——提示"一个类承载多个职责",应拆分(SRP)。判断"真正耦合还是合理分层":若一个变化确实需要跨多个明确分层的位置修改(如新增字段要改接口、实现、持久化、测试),这是合理的分层(各层职责不同);若修改散落在无关联的同类代码中,则是 Shotgun Surgery。若一个类因多个不相关原因而变,是 Divergent Change;若因多个相关细化需求而变,可能是合理细化。

两个坏味道方向相反:Shotgun 是"一变化多处改"(需聚合),Divergent 是"一处在多因变"(需拆分)。判断关键:变化是否"本质跨层"(合理)还是"同类散落"(紧张)。分层是合理的——跨层修改是职责分工,同层散落才是坏味道。

#
★★

9. "magic strings" 替换为常量的真实收益(编译期检查 vs 字符串拼写错误)

把 magic strings 替换为常量的真实收益是什么?对比编译期检查与字符串拼写错误?

  • magic strings 的危害
  • 常量的编译期检查
  • 真实收益

magic strings(魔法字符串)散落在代码中的问题:拼写错误在编译期/运行期不会暴露,直到运行时才报错或静默出错;无单一来源,改一处漏一处;语义不清晰。替换为常量(或枚举)的真实收益:编译期检查——常量/枚举的引用在编译期验证,拼错常量名立刻报错,而字符串拼错要到运行期才暴露;单一来源——改值只改一处;语义清晰——常量名描述含义。枚举比常量更强,还能做类型检查与穷尽分支。收益的本质是"把错误从运行期提前到编译期,并建立单一来源"。

magic string 的隐患是"无编译期保障"。用常量/枚举获得编译期检查:拼错立即失败,而非运行期。这符合易错性(fail-fast)与 DRY(单一来源)。真实收益是"编译期安全 + 语义化 + 单一来源"。

#
★★

10. speculative generality(投机式抽象)的识别与回滚路径

如何识别投机式抽象(speculative generality)?对应的回滚路径是什么?

  • 投机式抽象的特征
  • 识别信号
  • 回滚方式

投机式抽象(speculative generality)指为"虚构的未来"提前做的抽象——接口只有一个实现、抽象类只有一个子类、参数含未用的变体、配置项未使用。识别信号:接口/抽象类只有一个实现(无多态需求);参数/字段只用于一半分支;为"未来可能"预留的钩子从未被使用;抽象层无实际收益。回滚路径:把抽象"展平"——移除多余接口,让唯一实现直接使用;删除未用参数/配置;合并只有一个子类的抽象类;把不必要的委托改回直接调用。回滚虽麻烦但必要,避免抽象层成为永久负担。原则是"保留当下真实需求,删除投机猜测"。

投机式抽象是 YAGNI 的违反,识别靠"是否只有一个实现/是否有真实的多态需求"。回滚是"逆向简化":删多余抽象、展平层级。识别 + 及时回滚防止抽象层膨胀,保持 KISS。

#
★★

11. KISS 原则的工程实践中简单设计 vs 过度简化,如何识别过度工程并安全回滚?

KISS 原则的工程实践是什么?如何区分简单设计与过度简化,识别过度工程并安全回滚?

  • KISS 的含义
  • 简单 vs 过度简化
  • 识别过度工程与回滚

KISS(Keep It Simple, Stupid)要求用最简单直接的方式解决问题,避免不必要的复杂度。简单设计 vs 过度简化:简单是"满足当前需求的最少复杂度"——该有的抽象、错误处理、边界都保留;过度简化是"省略了必要的健壮性"——缺错误处理、边界、可读性,只是"看起来少"。识别过度工程:抽象层过多、为现实中不存在的变体设计、过度泛化、过度装饰、为指标而优化。安全回滚:先加测试固化现有行为,再逐步移除多余抽象、简化结构、删除投机代码,每次回滚后验证测试通过,保持可回退。KISS 落地靠"以最小必要复杂度实现真实需求"。

KISS 的灵魂是"复杂度用于真实需求"而非"炫技"。简单 ≠ 偷懒,过度简化牺牲正确性;过度工程牺牲简洁性。回滚关键:先测试固化,再逐步简化,验证不失。这是复杂度的"预算"管理。

#
★★

12. YAGNI 原则的工程实践中避免投机式开发,何时不做比做更正确?

YAGNI 原则的工程实践是什么?何时不做比做更正确?

  • YAGNI 的含义
  • 避免投机式开发
  • 不做的时机

YAGNI(You Aren't Gonna Need It)指"不要为当前需求之外、可能永远用不到的功能/抽象投资"。工程实践:优先实现当前需求,不提前做"未来可能"的扩展、兼容、配置、重载;每加一个功能都问"现在真的需要吗"。不做比做更正确的时机:为"可能"的需求(如未来客户会要、未来会扩展)而设计和开发,而这些需求的正确形态未知,做了反而限制未来、增加维护成本、增加测试面。此时"不做"更正确——因为需求没来,做了就是投机;需求真来了,基于真实需求重新设计比基于猜测的提前设计更准确。YAGNI 与"预留接口"矛盾时,倾向 YAGNI。

YAGNI 的核心是"信息不足时不做"——投机开发基于猜测,未来需求到达时形态往往不同,提前做的假设反而成负担。是否做,取决于"需求是否真实、明确"。不做是"把复杂度的投资推迟到信息充分时"。

#
★★

13. DRY vs KISS 的张力中何时接受重复以保持简单;DRY/KISS/YAGNI 三原则冲突时如何权衡?

DRY 与 KISS 有何张力?何时接受重复以保持简单?三原则冲突时如何权衡?

  • DRY 与 KISS 的张力
  • 接受重复的时机
  • 三原则权衡

DRY 与 KISS 有张力:DRY 追求"消除重复"(可能引入抽象层),KISS 追求"保持简单"(可能倾向保留重复)。张力在于"抽象消除重复但增加复杂度"。何时接受重复以保持简单:当重复的代价小于抽象的成本时——如仅两处、且抽取的抽象复杂、共享点是"可能独立演化"的逻辑。权衡 DRY/KISS/YAGNI:以 YAGNI 为底线(不为投机抽象),以 KISS 为大体导向(满足当前需求的最小复杂度),在"重复确实造成维护负担"时用 DRY(Rule of Three 判断)。三原则不矛盾:YAGNI 限制抽象时机,KISS 限制抽象复杂度,DRY 在真实重复时消除。冲突时,倾向"简单 + 真实需求"。

三原则的权衡是"以真实需求为锚"。YAGNI 拒绝投机,KISS 限制复杂度,DRY 在真实重复时应用。当 DRY 与 KISS 冲突时,检查重复是否真实造成维护成本:是则 DRY,否则 KISS(保留重复)。Rule of Three 帮助判断。

#
★★

14. DRY 的边界中重复代码 vs 错误抽象?

DRY 的边界在哪里?如何区分"重复代码"与"错误抽象"?

  • 重复代码与错误抽象
  • DRY 的边界
  • 判断

DRY 的边界是"避免为了消除重复而制造错误抽象"。错误抽象(wrong abstraction)指把"看似相同实则不同"的代码强行合并,导致一个抽象被多个不同语义的调用方共用,被迫加分支、加参数、加条件,最终抽象比重复更糟。区分:重复代码是"两处逻辑相同(同一意图)",应合并;错误抽象是"两处逻辑只是形状相似但语义不同",合并后需大量分支区分,不如保留独立。判断标准:合并后是否引入"if 分支区分调用方"?是否出现"一个参数控制行为"?抽象是否承载了多个不同语义?若是,则可能是错误抽象。DRY 的边界是"合并不改变语义、不引入过度耦合"。

重复 vs 错误抽象是 DRY 应用的两难。真实重复(同意图)应 DRY;相似但不同语义,强行 DRY 产生错误抽象。判断核心是"合并后是否需分支区分语义"——若需,则抽象错误,应保留独立或重新设计。这是 Rule of Three 与 Dan North 决策树的价值。

#
★★

15. 复杂度预算中团队如何为变更设定"简单性预算"并在评审中把关,防止 KISS 沦为口号?

团队如何为变更设定"简单性预算"并在评审中把关,防止 KISS 沦为口号?

  • 复杂度预算的概念
  • 评审把关
  • 防止 KISS 口号化

复杂度预算(complexity budget)定义为"每次变更允许引入的复杂度上限",用于防止复杂度持续累积。团队实践:为变更设定复杂度预算——每次变更必须说明"引入的复杂度"与"带来的收益"的平衡,新增抽象/依赖/配置必须在预算内并被证明确实必要;评审时把"是否过度设计"列为明确检查项,要求提交者解释每个抽象、接口、参数、依赖的真实用途;用"最小改动解决当前问题"作为评审标准;对不符合 KISS 的变更打回并要求简化。把 KISS 从口号落地为"可检查的评审标准 + 预算管理",防止复杂度静默累积。

复杂度预算把 KISS 变得可度量、可执行。核心是"复杂度不能免费累积"——每次变更需论证复杂度合理性。评审把关是落地手段:要求解释每个抽象的必要性,防止投机与过度设计。预算管理防止 KISS 沦为口号。

#
★★

16. 配置与脚本的单一来源中多环境配置、CI 步骤与文档中的重复如何用模板或生成消除?

配置与脚本的单一来源:多环境配置、CI 步骤、文档中的重复如何用模板或生成消除?

  • 配置重复
  • 模板与生成
  • 单一来源

多环境配置、CI 步骤、文档中常出现重复:同一配置在多环境(dev/test/prod)重复、同一 CI 步骤在多个 pipeline 重复、文档与代码/配置重复。消除方式:用模板/生成器实现单一来源——配置用模板(如 YAML 模板 + 变量)统一生成多环境配置,CI 用可复用步骤/模板(如 GitHub Actions 的 composite action、Jenkins 共享库)一次定义多处引用,文档用代码生成(从配置/代码生成文档,如 OpenAPI 生成 API 文档)或文档即代码(AsciiDoc 引源码)。原则:把"知识"(配置源、CI 逻辑、真实行为)放在单一权威源,通过模板/生成派生各环境的产物,避免手工复制导致的不一致。

配置与脚本的重复是 DRY 的常见盲区。单一来源 + 模板/生成解决"多环境、多 pipeline、文档"的重复。核心是"权威源一处,产物自动生成"。这减少手工同步错误,保证一致性。

#

17. YAGNI 与可扩展性的平衡中为未来留接口 vs 为现在写代码

YAGNI 与可扩展性如何平衡?为未来留接口 vs 为现在写代码?

  • YAGNI 与可扩展性的张力
  • 留接口 vs 写代码
  • 平衡原则

YAGNI 说"不为未来投机",可扩展性说"为未来留余地",两者张力在于"预留多少"。平衡原则:为现在写代码,但为"可预见的真实变化"留清晰接口——区分"确定性需求"(几乎必然发生,如系统会增长)与"投机需求"(可能不发生)。对确定性需求,可留适度扩展点(如接口、配置化);对投机需求,交给 YAGNI 不做。折中:不过度投资抽象,但保持代码"易于演进"——如依赖抽象(DIP)、模块化、清晰命名,使未来改动成本低,而非提前实现未来功能。关键:留"可扩展的结构"而非"提前的功能"。

YAGNI 反对的是"提前实现功能",不反对"保持可演进的结构"。正确姿态是为现在写代码,同时让代码结构(依赖抽象、低耦合)天然易于扩展,而非为假想需求预埋功能。可演进性 vs 投机实现的区分是平衡核心。

#

18. DRY 的复用层级中函数、模块与服务?

DRY 的复用层级有哪些?函数、模块与服务如何选择?

  • 复用层级
  • 函数/模块/服务的选择
  • 层级与代价

DRY 的复用有多个层级:函数(最小的复用单元,如公共工具函数,复用"单段逻辑");模块/类(复用"一组相关逻辑",如公共库、通用组件,复用"能力");服务(复用"跨系统的能力",如独立微服务、共享 API,复用"对外的业务能力")。选择依据:复用范围与耦合代价——函数耦合最轻、成本最低,适合跨模块小逻辑;模块居中,适合组织内共享能力;服务最重,适合跨系统、跨团队共享,但引入网络与治理成本。原则:尽量用最低层级满足复用需求,避免为"小逻辑"强行上服务。层级越高,复用收益越大但耦合/成本也越大。

复用层级选择是"复用需求的成本收益"。函数轻、服务重。应"从低往高":能函数复用就别上服务,避免过度设计。服务复用适合"真实的跨团队能力共享",否则用模块/函数即可。这是 DRY 与 KISS 的平衡。

#

19. 重复造轮子 vs 引入依赖中内部工具函数与成熟第三方库之间的 DRY 边界与决策依据?

重复造轮子 vs 引入依赖:内部工具函数与成熟第三方库之间的 DRY 边界与决策依据?

  • 自研 vs 第三方
  • DRY 边界
  • 决策依据

重复造轮子 vs 引入依赖的决策:一般原则是"成熟第三方库优先"——因为第三方库经过大量使用验证、有社区维护、功能全面,避免重复实现(DRY 的更高层:不重复造轮子)。但引入依赖有代价:版本管理、安全风险、大小、兼容性、升级负担。决策依据:功能是否成熟稳定且主流(如日期、字符串、JSON 库,用第三方);是否核心业务逻辑(核心业务应自研,避免第三方限制);依赖规模与维护风险(小功能、仅需几行,可自研工具函数而非引入大依赖);许可证与安全。当"自研会重复实现大量成熟功能"时,用第三方;当"第三方过度侵入或功能简单"时,自研。边界是"核心给自研、通用给第三方"。

DRY 的边界是"不重复造轮子"但不"盲目引入依赖"。第三方库若成熟且适用,用第三方避免重复实现;若功能简单、核心、或第三方过重,自研工具函数。决策看"复用第三方 vs 自研的维护成本与风险"。