引言:为什么你的代码像一棵巨大的圣诞树?
想象一下,你正在为你的电商平台开发一个订单价格计算模块。最初,需求很简单:商品原价就是最终价格。但很快,业务部门提出了新的需求:
- 新用户享受 9 折优惠。
- VIP 用户享受 8 折优惠。
- 大促活动期间,全场 7 折。
你很自然地写出了这样的代码:
func CalculatePrice(userType string, isPromotion bool, price float64) float64 {
if userType == "NewUser" {
return price * 0.9
} else if userType == "VIP" {
return price * 0.8
} else if isPromotion {
return price * 0.7
}
return price // 默认无折扣
}
这段代码看起来没问题,但如果你再想想未来可能的需求:会员日 6 折、积分抵扣、优惠券叠加……你的 if/else
链会像一棵不断长大的圣诞树,挂满了各种复杂的逻辑分支。每一次新需求的到来,你都得修改这个函数,代码变得越来越臃肿、难以阅读和维护。
这就是所谓的 if/else
hell。它违反了软件设计中的重要原则——开闭原则(Open-Closed Principle):对扩展开放,对修改封闭。我们如何才能优雅地解决这个问题呢?答案就是——策略模式。
什么是策略模式?
策略模式是一种行为型设计模式,它定义了一系列算法(或者说策略),将每一个算法都封装起来,并使它们可以互相替换。
简单来说,它的核心思想是:将算法的实现与使用算法的客户端代码分离开来。客户端不关心具体的算法是如何实现的,它只知道通过一个统一的接口来调用它。这样,你就可以在运行时根据需要动态地选择和切换不同的算法。
想象一下你的手机支付:你可以选择微信支付、支付宝支付或银行卡支付。每种支付方式都是一种策略,它们都实现了“支付”这个功能,但具体流程不同。你作为用户,只需要选择其中一个,点击支付按钮,而不需要关心每种支付方式背后的具体代码逻辑。
策略模式的三个核心组件
策略模式由三个关键角色组成:
- 策略接口(Strategy):定义了所有具体策略必须遵循的统一接口。在 Go 语言中,这通常是一个
interface
。 - 具体策略(Concrete Strategy):实现了策略接口的类。每一种具体策略都封装了一种独立的算法或行为。
- 上下文(Context):持有对策略接口的引用,并负责调用具体策略来执行任务。它充当了客户端和具体策略之间的桥梁。
Go 语言实现:重构你的订单计算器
现在,让我们使用策略模式来重构上面的订单价格计算代码。
步骤1:定义策略接口
首先,我们定义一个名为 PricingStrategy
的接口,它有一个 Calculate
方法,用来计算最终价格。
// 1. 定义策略接口(Strategy)
// PricingStrategy 定义了所有折扣策略必须实现的接口
type PricingStrategy interface {
Calculate(originalPrice float64) float64
}
步骤2:创建具体策略
接下来,为每种折扣类型创建独立的结构体,并让它们实现 PricingStrategy
接口。
// 2. 创建具体策略(Concrete Strategy)
// NewUserDiscount 实现了新用户折扣策略
type NewUserDiscount struct{}
func (s *NewUserDiscount) Calculate(originalPrice float64) float64 {
// 关键点:每种策略只负责自己的计算逻辑
return originalPrice * 0.9
}
// VIPDiscount 实现了 VIP 用户折扣策略
type VIPDiscount struct{}
func (s *VIPDiscount) Calculate(originalPrice float64) float64 {
return originalPrice * 0.8
}
// PromotionDiscount 实现了大促折扣策略
type PromotionDiscount struct{}
func (s *PromotionDiscount) Calculate(originalPrice float64) float64 {
return originalPrice * 0.7
}
步骤3:创建上下文
最后,我们创建一个 PricingContext
结构体,它负责管理和执行策略。
// 3. 创建上下文(Context)
// PricingContext 持有并执行策略
type PricingContext struct {
strategy PricingStrategy
}
// SetStrategy 动态设置当前使用的策略
func (c *PricingContext) SetStrategy(strategy PricingStrategy) {
c.strategy = strategy
}
// GetFinalPrice 调用当前策略来计算最终价格
func (c *PricingContext) GetFinalPrice(originalPrice float64) float64 {
if c.strategy == nil {
return originalPrice // 如果没有设置策略,返回原价
}
// 关键点:上下文不关心是哪种策略,只调用接口方法
return c.strategy.Calculate(originalPrice)
}
代码使用
现在,我们的代码变得非常灵活和清晰。你可以像搭乐高积木一样,根据需要选择和组合不同的策略。
package main
import "fmt"
func main() {
// 假设商品原价为 100
originalPrice := 100.0
// 实例化上下文
context := &PricingContext{}
// 场景一:新用户下单,应用新用户折扣策略
fmt.Println("--- 场景一:新用户下单 ---")
context.SetStrategy(&NewUserDiscount{}) // 切换策略
finalPrice1 := context.GetFinalPrice(originalPrice)
fmt.Printf("原价: %.2f, 最终价格: %.2f\n", originalPrice, finalPrice1)
// 输出:原价: 100.00, 最终价格: 90.00
// 场景二:VIP 用户下单,切换到 VIP 策略
fmt.Println("\n--- 场景二:VIP 用户下单 ---")
context.SetStrategy(&VIPDiscount{}) // 轻松切换策略
finalPrice2 := context.GetFinalPrice(originalPrice)
fmt.Printf("原价: %.2f, 最终价格: %.2f\n", originalPrice, finalPrice2)
// 输出:原价: 100.00, 最终价格: 80.00
// 场景三:大促期间,VIP 用户下单,但平台规定大促折扣优先
fmt.Println("\n--- 场景三:大促期间下单 ---")
context.SetStrategy(&PromotionDiscount{}) // 再次切换策略
finalPrice3 := context.GetFinalPrice(originalPrice)
fmt.Printf("原价: %.2f, 最终价格: %.2f\n", originalPrice, finalPrice3)
// 输出:原价: 100.00, 最终价格: 70.00
// 场景四:如果未来新增了“会员日 6 折”策略,你只需要新增一个 struct,无需修改任何旧代码!
// type MemberDayDiscount struct{}...
}
优缺点分析
优点 | 缺点 |
---|---|
符合开闭原则:新增策略时,无需修改已有代码。 | 增加了类的数量:每种策略都需要一个独立的结构体,可能导致类文件增多。 |
易于扩展:添加新算法只需创建新类型并实现接口。 | 客户端需要了解所有策略:调用方(如 main 函数)需要知道所有可用的具体策略类。 |
代码解耦:算法的实现与使用完全分离,职责单一。 | 如果策略非常简单,过度设计可能带来不必要的复杂性。 |
运行时切换:可以根据不同条件动态选择和切换算法。 |
总结
策略模式是一种优雅而强大的设计模式,它帮助我们摆脱了复杂的 if/else
嵌套,使得代码结构更清晰、更易于扩展和维护。在 Go 语言中,利用接口的特性可以轻松实现这一模式。
下次当你遇到需要根据不同条件执行不同行为的场景时,不妨停下来思考一下:这里是不是可以用策略模式来重构呢?用接口和具体的实现来替代一长串的 if/else
分支,让你的代码更具弹性,为未来的扩展做好准备。