本文围绕日常开发中最常用的四条工程原则KISS(保持简单)、DRY(勿重复自己)、YAGNI(你不会需要它)与LOD(最少知识原则)展开,说明它们如何共同约束"复杂度的无控制增长"。先从过度设计、提前优化、重复代码三类常见陷阱的成因讲起,再分别拆解四条原则的核心含义、适用边界与典型反例;随后用对照与关系图帮助建立"何时简化、何时抽象、何时先别做"的判断直觉;最后给出一份可直接用于代码评审的检查清单,帮助团队在评审现场快速识别架构与实现中的工程风险。目标不是背诵口号,而是把极简原则变成可执行的决策习惯。
架构设计最容易踩的坑,往往不是"不会做复杂系统",而是"把简单问题做成了复杂系统"。业务一开始只需要一个清晰的主流程,却在迭代中被一层层策略框架、配置中心、抽象工厂和"将来可能用得上"的扩展点淹没;代码一开始只是几处相似逻辑,却在复制粘贴与错误抽象之间反复摇摆。KISS、DRY、YAGNI、LOD之所以常被并列讨论,是因为它们分别从可读性、一致性、时机感、依赖边界四个方向,把复杂度往下压。它们不是互斥规则,而是一套互补的约束:KISS管"别写难懂的",DRY管"别写两遍的",YAGNI管"别写还没用的",LOD管"别让模块知道太多"。
零. 为什么需要"极简原则"?三类陷阱从哪里来?
很多团队并非缺少设计能力,而是设计欲过强、反馈周期过长。过度设计常常来自对不确定性的恐惧,需求还在变,就提前预留了大量扩展点,接口还只有一个实现,就先抽出三层抽象,业务规则尚未稳定,就开始建模"通用平台"。其根源往往是把"未来可能需要"误当成"现在必须建设",又缺少成本核算,多出来的抽象会同时增加理解成本、改动成本与调试成本。
提前优化则更隐蔽。它常披着"性能专业度"的外衣:在没有profiling证据前就引入缓存、异步、分片、自定义连接池。为了"少做几次计算"把清晰逻辑拆成难维护的技巧写法。根因通常是把局部微观指标抬到高于交付与可维护性,或把"感觉慢"当成"已经慢"。真正的问题未必出在那几毫秒上,而出在调用链、数据结构或IO模式上,但代码已经先复杂了。
重复代码的成因则更日常,赶工期时复制粘贴最快。相似却不等价的业务被强行抽公共方法后又怕牵一发动全身,于是继续复制。缺少共享边界与模块职责,导致校验、转换、错误码映射在多个服务里各写一份。重复的真正危险不只是多敲了几行字,而是同一业务规则有多个真相源:修一处漏一处,线上行为开始分叉。
可以把三类陷阱与四条原则的对应关系粗略看成一张决策地图:过度设计主要用YAGNI与KISS约束。重复逻辑用DRY约束,但要用KISS防止"错误抽象"。模块间过度耦合与"链式调用深处字段"用LOD约束。提前优化则要求先测量,再让KISS/YAGNI拦住多余技巧。

如上图所示,面对工期紧张与需求不稳定,开发者容易出现过度预埋扩展、复制粘贴产生多份逻辑源、主观盲目优化三类问题,业界分别以YAGNI+KISS、DRY、先测量再优化的编程思想加以约束。但倘若机械教条地套用上述原则,又极易催生依赖蔓延、深层耦合的架构弊病,此时需LOD规范对象间交互边界,收敛不必要依赖,以此平衡代码简洁度与模块解耦目标。
一. KISS,简单不是简陋,而是"下一步仍然好改"
KISS(Keep It Simple, Stupid)强调:在满足当前需求的前提下,选择最直白、最少概念的实现 。这里的"简单"不是偷工减料,而是让读者用最少的上下文就能回答三个问题:这段代码在做什么、为什么这样做、改动时会影响哪里 。当一段逻辑需要阅读者先理解一套自定义状态机框架才能看懂"其实就是if/else审批流",KISS就已经被违反了。
KISS在落地时最常见的误区,是把"少写代码"等同于"简单"。一行正则塞满业务规则、一个巨大函数塞满所有分支、或者用技巧压缩控制流,字符数少了,认知负担反而上升。真正的简单,通常表现为清晰命名、浅层控制流、显式边界与可本地理解的数据变换。另一个误区是把"没有抽象"当成KISS:当同一规则已经稳定出现第三次,并且变体确实一致时,适度抽取反而是降低复杂度;KISS反对的是为不确定未来制造概念,不是反对一切结构化。
下面这段对比很典型。第一种写法用策略注册中心表达"按类型计费",概念完整,但对当前只有两种类型的业务来说偏重;第二种写法更直接,也更容易在评审里被读懂。
python
# 过度设计:为两种计费方式搭策略框架
class PricingStrategy(Protocol):
def price(self, order: Order) -> Decimal: ...
class StrategyRegistry:
def __init__(self):
self._map: dict[str, PricingStrategy] = {}
def register(self, kind: str, strategy: PricingStrategy) -> None:
self._map[kind] = strategy
def price(self, order: Order) -> Decimal:
return self._map[order.kind].price(order)
# KISS:当前需求只需要两种规则时,直写更清楚
def price_order(order: Order) -> Decimal:
if order.kind == "retail":
return order.amount * Decimal("1.00")
if order.kind == "wholesale":
return order.amount * Decimal("0.85")
raise ValueError(f"unsupported order kind: {order.kind}")
当类型增加到五六种、规则开始共享前置校验、并且变化频率升高时,再引入策略也不迟。KISS的价值就在于把抽象的时机推迟到证据足够之时。
二. DRY,消灭的是"知识重复",不是"字符相似"
DRY(Don't Repeat Yourself)常被误解成"看到相似代码就抽方法"。更准确的说法是:系统中每一份知识(业务规则、不变量、映射关系)都应有单一、明确、权威的表达 。两段代码长得很像,但若语义不同,一个校验下单库存、一个校验售后库存。强行合并会制造更离谱的耦合。反之,散落在三处的同一折扣规则才是真正该DRY的对象。
重复代码通常沿两条路径生长。一条是水平复制,多个模块各自写一遍手机号校验、金额分转元、错误码翻译。另一条是演化分叉,最初同一函数被复制后,一边修了边界条件,另一边没修,表面上"差不多",实际上已经是两套规则。DRY要解决的是后者的根,而不是追求字面零重复。
实践中有一条很实用的经验,先问"变的时候会不会一起变"。若两处逻辑会因同一业务决策同步修改,就值得抽取。若只是碰巧结构相似,保留各自表达往往更安全。抽取时也应优先抽"规则与转换",而不是过早抽"巨型工具类杂物间"。
python
// 知识重复:折扣规则出现了两份真相
function checkoutPrice(amount: number, vip: boolean): number {
return vip ? amount * 0.9 : amount;
}
function quotePrice(amount: number, vip: boolean): number {
return vip ? amount * 0.9 : amount; // 另一处又写一遍
}
// DRY:单一真相源
function applyVipDiscount(amount: number, vip: boolean): number {
return vip ? amount * 0.9 : amount;
}
需要警惕"DRY洁癖"。把仅出现两次、且很快会分叉的逻辑强行公共化,会让函数参数变成一堆flag,最终比重复更难读。这时应让KISS回来仲裁:宁可短暂重复,也不要错误抽象。
三. YAGNI,把"可能需要"从当前迭代里删掉
YAGNI(You Aren't Gonna Need It)直接对抗提前建设。它的尖锐之处在于大多数"将来一定要用的扩展点",最后要么用不上,要么用上时形态已经变了,预埋设计反而成了改造阻力。YAGNI不否定长期规划,而是否定把规划误当成当前代码里的实体。路线图可以写在文档和里程碑里。只有被当前验收标准证明需要的行为,才值得变成类、表字段、配置项与分支。
YAGNI与提前优化是近亲。没有证据就做缓存、消息总线、多级降级、可插拔规则引擎,都属于"现在支付复杂度税,换取不一定发生的收益"。更稳妥的顺序是:先让正确路径简单跑通,用监控与压测找到真实瓶颈,再做针对性增强。这样每一次变复杂都有账单可对。
一个常见场景是"通用导出服务"。当前只要导出CSV,却同时做了Excel/PDF、模板市场、插件沙箱与分布式任务编排。若产品下一季度仍只导出CSV,这些前置投资就是纯负债。YAGNI的落地话术很务实:在设计评审里问"这个扩展点删掉后,哪个当前用例会失败?"答不上来,就先别建。
python
// YAGNI违反,当前只需CSV,却预埋一堆格式
type Exporter interface {
ExportCSV(rows []Row) ([]byte, error)
ExportXLSX(rows []Row) ([]byte, error)
ExportPDF(rows []Row) ([]byte, error)
}
// YAGNI,先做当前需要的
func ExportCSV(rows []Row) ([]byte, error) {
// ...
return buf.Bytes(), nil
}
等第二个真实格式需求出现、且导出链路稳定后,再引入接口通常成本更低,因为那时你已经知道变化点在哪里。
四. LOD,少知道一点,系统才松得开
LOD(Law of Demeter,最少知识原则)关注模块的"社交范围":一个对象应尽量只与直接朋友通信,而不是顺着调用链去摸很远的内部结构。经典味道是order.getCustomer().getAddress().getCity()这类列车式写法,调用方不只依赖Order,还间接依赖Customer与Address的内部形状。任何一环结构调整,调用方都可能被迫修改。
LOD的工程意义,是把封装从"私有字段"推进到"稳定能力"。比起暴露结构让别人层层取值,不如提供意图明确的方法:order.shippingCity()或由应用服务组装好需要的视图模型。这样变化被关在门内,外部只认能力契约。LOD也适用于服务间:一个接口不应要求调用方理解你的三级内部表关联,再拼装出业务结果,应提供符合用例的API。
python
// 违反LOD,调用方知道了过深的对象图
String city = order.getCustomer().getAddress().getCity();
// 更符合LOD,让"知道如何取城市"留在合适边界内
String city = order.shippingCity();
// 或在应用层:
String city = shippingInfoService.cityOf(order.getId());
LOD不是禁止一切间接访问,而是提醒,每次多知道一个陌生人,就多一条耦合边。架构上的"分层清晰、依赖单向、接口面向用例",背后常常就是LOD在起作用。
五. 四条原则如何一起用,冲突时听谁的
原则在真实项目里会发生张力。DRY推动抽取,KISS担心抽取过度。YAGNI推迟建设,业务方催促"顺便把扩展做了"。LOD要求封装,调试时又想直接把内部结构掏出来。因此需要一张可对照的心智表,而不是把口号并列背诵。
| 原则 | 核心问题 | 典型收益 | 常见误用 |
|---|---|---|---|
| KISS | 有没有更直白的表达? | 可读、可改、可评审 | 用技巧换短代码;拒绝一切结构化 |
| DRY | 是否存在多份业务真相? | 改一处处处一致 | 把"长得像"当成"是同一个" |
| YAGNI | 删掉后当前需求是否仍满足? | 少负债、快交付 | 把合理的边界设计也删掉 |
| LOD | 是否依赖了不该知道的内部? | 低耦合、改动局部化 | 为封装而封装,API 粒度过碎 |
再看三类陷阱与原则的主攻方向,便于评审时快速定位。
| 陷阱 | 常见表象 | 优先用什么管 | 评审时一句探针 |
|---|---|---|---|
| 过度设计 | 空接口、空扩展点、过早平台化 | YAGNI、KISS | "当前哪个用例依赖它?" |
| 提前优化 | 无指标缓存/异步/特殊算法 | YAGNI、KISS | "慢点证据在哪?" |
| 重复代码 | 多处同规则、修复不同步 | DRY(辅以KISS) | "会不会因同一决策一起变?" |
它们的协作可以概括成一条很短的决策链:先用YAGNI删掉当前不需要的,再用KISS选择直白表达。当同一知识出现多份权威拷贝时用DRY收敛,跨模块取值或依赖开始"钻地道"时用LOD收回边界。若抽取后可读性明显下降,就退回重复或缩小抽象范围,这时KISS拥有一票否决权。整理后的思维链应该为:

总结
四大工程原则并非独立的口号式规则,而是一套可落地、可互补的代码设计决策体系,在实际开发中存在明确的协作决策逻辑。
开发迭代中可遵循"先删减冗余、再简化实现、后收敛重复、最后规范依赖"的思维链,以YAGNI剔除无效超前设计,以KISS保障代码简洁直观,以DRY统一业务规则源头,以LOD收敛模块耦合关系,且当原则适用出现冲突时,以代码可读性为核心的KISS原则拥有最终否决权。