为什么"不要重复"不是铁律
很多开发者把 DRY(Don’t Repeat Yourself)奉为圭臬,一看到重复代码就急着抽取公共函数。结果呢?公共函数越来越臃肿,参数列表越来越长,最终变成一个谁也不敢动的"上帝函数"。
The Pragmatic Programmer 中提出了一个更务实的原则——三次法则(Rule of Three):
当同一段逻辑被复制第三次时,才值得提取抽象。前两次的重复是可接受的,甚至是必要的。
这不是在为懒惰找借口,而是一种经过验证的工程智慧。下面用一个电商订单处理的真实案例来说明。
第一次:重复是合理的
第一个需求是处理普通订单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
func ProcessOrder(order *Order) error {
// 校验
if order.Amount <= 0 {
return errors.New("invalid amount")
}
if order.UserID == "" {
return errors.New("missing user")
}
// 计算
total := order.Amount * (1 - order.Discount)
// 持久化
db.Save(&OrderRecord{
OrderID: order.ID,
Total: total,
Status: "processed",
})
return nil
}
|
现在来了第二个需求:处理会员订单。逻辑很像,但我们还不知道未来会有多少变体。此时最好的做法就是直接复制一份代码,改改细节:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
func ProcessMemberOrder(order *Order) error {
// 校验(和上面一模一样)
if order.Amount <= 0 {
return errors.New("invalid amount")
}
if order.UserID == "" {
return errors.New("missing user")
}
// 计算(会员额外9折)
total := order.Amount * (1 - order.Discount) * 0.9
// 持久化
db.Save(&OrderRecord{
OrderID: order.ID,
Total: total,
Status: "member_processed",
})
return nil
}
|
这时候你有两个选择:提取公共函数,或者保持重复。三次法则的建议是——保持重复。
为什么?因为此时只有两个数据点,还看不出真正的共性在哪里。过早抽象往往提取出错误的抽象边界。
第二次:开始看到模式
第三个需求来了:处理企业订单。现在有了三个相似函数,模式开始浮现——校验逻辑完全一致,计算逻辑各有不同,持久化结构相同但字段值不同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
func ProcessEnterpriseOrder(order *Order) error {
// 校验(第三次复制同样的代码)
if order.Amount <= 0 {
return errors.New("invalid amount")
}
if order.UserID == "" {
return errors.New("missing user")
}
// 计算(企业阶梯折扣)
total := calculateEnterpriseTotal(order.Amount, order.Discount)
// 持久化
db.Save(&OrderRecord{
OrderID: order.ID,
Total: total,
Status: "enterprise_processed",
})
return nil
}
|
三次法则触发——现在该提取抽象了。
第三次:提取正确的抽象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
// 统一的处理流程
func ProcessOrderFlow(order *Order, calcFn func(*Order) (float64, string)) error {
if err := validateOrder(order); err != nil {
return err
}
total, statusTag := calcFn(order)
return db.Save(&OrderRecord{
OrderID: order.ID,
Total: total,
Status: statusTag + "_processed",
})
}
// 校验逻辑(只写一次)
func validateOrder(order *Order) error {
if order.Amount <= 0 {
return errors.New("invalid amount")
}
if order.UserID == "" {
return errors.New("missing user")
}
return nil
}
// 三种计算策略
func normalCalc(o *Order) (float64, string) {
return o.Amount * (1 - o.Discount), "normal"
}
func memberCalc(o *Order) (float64, string) {
return o.Amount * (1 - o.Discount) * 0.9, "member"
}
func enterpriseCalc(o *Order) (float64, string) {
return calculateEnterpriseTotal(o.Amount, o.Discount), "enterprise"
}
// 调用方保持简洁
func ProcessOrder(order *Order) error {
return ProcessOrderFlow(order, normalCalc)
}
func ProcessMemberOrder(order *Order) error {
return ProcessOrderFlow(order, memberCalc)
}
func ProcessEnterpriseOrder(order *Order) error {
return ProcessOrderFlow(order, enterpriseCalc)
}
|
注意几个关键决策:
- 只提取真正重复的部分:校验和持久化被提取,计算逻辑保持在外部
- 用函数参数而非继承:Go 没有继承,函数式策略更灵活更轻量
- 状态标记由计算函数返回:不硬编码,让策略自己决定 tag
过度抽象的反面教材
三次法则的反面是零容忍重复——也就是过早抽象。来看一个真实的反面案例:
1
2
3
4
5
6
7
|
// 过早抽象:第一次写就做了全面通用化
func ProcessOrderGeneric(order *Order, validator OrderValidator,
calculator OrderCalculator, persister OrderPersister,
notifier OrderNotifier, logger OrderLogger) error {
// 5个接口参数,但当前只有一个调用方
// ...
}
|
这个设计在只有一个调用方时引入了5个接口。每次需求变化,你都得同时修改接口和实现。抽象的成本远大于重复的成本。
更糟糕的是,当真正的第三个变体出现时,你会发现之前设计的接口假设可能是错误的——比如 validator 突然需要访问 DB,但接口签名没传 context。于是要么改接口(影响所有实现),要么在接口外打补丁(破坏抽象的完整性)。
什么时候打破三次法则
三次法则不是教条,有些场景应该在第一次就抽象:
- 基础设施代码:数据库连接、日志、配置——从一开始就该统一
- 领域核心逻辑:金额计算、权限校验——重复一次就可能产生数据不一致
- 安全相关代码:加密、认证——重复意味着遗漏漏洞的风险
1
2
3
4
5
|
// 这类代码第一次就该统一,不存在"容忍两次重复"的余地
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
|
实践建议
- 容忍短期重复:复制粘贴不可耻,可耻的是复制了五次还不提取
- 标记重复次数:在代码注释中写上
// DUP: 2nd,提醒自己和团队第三次到来时触发重构
- 需求驱动而非设计驱动:你不是预言家,等模式浮现再出手
- 提取后立即验证:跑一遍全部测试,确保三个调用方行为完全不变
结语
三次法则的本质是承认一个事实:我们没有水晶球。在只有一两个实例时,我们无法预知正确的抽象形状。容忍有限的重复,等到模式真正浮现时再出手,往往比一开始就过度设计更务实。
好的抽象来自观察,而非预测。