刚接触编程的时候,我和很多人一样,觉得代码里的if else越少越好,总想着用各种设计模式去优化它。但随着经验的积累,我发现事情远没有那么简单。今天就来和大家聊聊,为啥不能一股脑地用设计模式消除if else。先给大家划下重点:
1. 实现功能要恰到好处,别给代码加太多没根据的假设。
2. 过早套用设计模式,可能会让代码灵活性大打折扣,虽然这有点反直觉,但事实就是如此。
3. 状态模式和策略模式不一定是万能解药,有时候简单直接的if - else反而更直观好用。
4. 工厂模式可以在代码清理、演进的过程中自然引入,不用强行套用。

if else真的一无是处吗?

如果一个功能用三个if逻辑判断就能轻松搞定,真没必要大费周章地用设计模式封装。咱们编程的目的是高效实现功能,提高扩展性和性能,而不是单纯为了减少if else而优化。学了设计模式后,大家难免会有“拿着锤子找钉子”的心态,总想用它改造代码。可很多时候改完再看,会忍不住问自己:为啥把代码写这么复杂?明明几个if else就能解决,非要用上工厂模式和策略模式。要是状态就两三种,直接用if判断状态机状态就好,为啥非要改成状态模式,感觉就是为了封装而封装。

实际上,如果一个功能里if到处都是,那大概率啥设计模式都救不了,强行用只会让代码更混乱。想要真正优化if else,不妨画个流程图,梳理下业务流程,去掉那些多余的逻辑,这才是从根本上解决问题。

状态模式消除if else,真的好吗?

咱们来看个具体例子,购物订单创建后是等待支付状态(pending),用户支付后变成已支付(paid),只有未支付的订单能取消(cancel)。

先看看用if else实现的代码:

func CreateOrder() Order { // 在数据库中创建订单 return Order{ State: Pending, } } func PaidOrder(order Order) error { // 未支付的订单才能支付 if order.State != Pending { return errors.New("order state is not pending") } // 更新订单状态到数据库 order.State = Paid //... return nil } func CancelOrder(order Order) error { // 只有订单状态为Pending时才能取消 if order.State != Pending { return errors.New("order state is not pending") } // 更新订单状态到数据库 order.State = Cancel //... return nil } 

因为订单状态少,这样实现既简洁又直观。但要是非要用状态机模式封装,虽然能去掉if判断状态的代码,却会引入更复杂的代码。

下面是状态机实现后的代码:

// OrderState定义订单状态行为 type OrderState interface { Paid(order *Order) error Cancel(order *Order) error GetState() int } // PendingState待支付状态 type PendingState struct{} func (s *PendingState) Paid(order *Order) error { // 更新订单状态到数据库 order.OrderState = &PaidState{} fmt.Println("订单已支付") return nil } func (s *PendingState) Cancel(order *Order) error { // 更新订单状态到数据库 order.OrderState = &CanceledState{} fmt.Println("订单已取消") return nil } func (s *PendingState) GetState() int { return Pending } // PaidState已支付状态 type PaidState struct{} func (s *PaidState) Paid(order *Order) error { return errors.New("订单已支付,不能再次支付") } func (s *PaidState) Cancel(order *Order) error { return errors.New("订单已支付,不能取消") } func (s *PaidState) GetState() int { return Paid } // CanceledState已取消状态 type CanceledState struct{} func (s *CanceledState) Paid(order *Order) error { return errors.New("订单已取消,不能支付") } func (s *CanceledState) Cancel(order *Order) error { return errors.New("订单已取消,不能重复取消") } func (s *CanceledState) GetState() int { return Cancel } // CreateOrder创建一个新订单,默认状态为Pending func CreateOrder() *Order { return &Order{ OrderState: &PendingState{}, } } // PaidOrder支付订单 func PaidOrder(order *Order) error { return order.OrderState.Paid(order) } // CancelOrder取消订单 func CancelOrder(order *Order) error { return order.OrderState.Cancel(order) } 

乍一看,代码好像变得“高大上”了,但仔细分析就会发现问题。

符合单一职责原则(SRP)?未必!

状态机模式下,每个状态类只处理相关逻辑,理论上修改“已支付”状态逻辑,不会影响其他状态。但用if else实现时,修改PaidOrder方法,改动范围也不大,所以这个所谓的“好处”并不明显。

代码更具扩展性?不一定!

有人说状态机模式符合开闭原则(OCP),新增状态或行为时,不用修改现有代码,直接添加新状态类就行。但实际真的如此吗?比如增加退款操作,就得在OrderState接口添加RefundedState方法,每个状态实现类都得增加这个方法。

有人可能会说,可以增加一个通用错误状态类来解决。但如果每个错误状态的报错文案不同且无规律,这个通用实现就没用了。

相比之下,if else实现增加新状态时,改动范围往往比想象的小。比如增加支付订单能转成已发货状态,确认收货后完成订单的功能,用if else实现,不需要改动之前的代码,直接增加新操作就行。

func ShippedOrder(order Order) error { // 只有订单状态为Paid时才能发货 if order.State != Paid { return errors.New("order state is not paid") } // 更新订单状态到数据库 order.State = Shipped //... return nil } func CompleteOrder(order Order) error { // 只有订单状态为Shipped时才能完成 if order.State != Shipped { return errors.New("order state is not shipped") } // 更新订单状态到数据库 order.State = Complete //... return nil } 

要是增加支持先发货再支付,最终完成订单的功能呢?只需要增加一个map维护操作的有效状态,再增加一个验证订单状态的方法就行。

var ( orderAction2ValidState = map[OrderAction][]State{ OrderActionPay: {Pending}, OrderActionShip: {Paid}, OrderActionComplete: {Shipped}, OrderActionCancel: {Pending}, } ) func ValidOrderState(order Order, action OrderAction) bool { validStates, ok := orderAction2ValidState[action] if!ok { return false } for _, state := range validStates { if order.State == state { return true } } return false } 

改造原有代码后,支持先发货再付款,只需要修改orderAction2ValidState就可以。

var ( orderAction2ValidState = map[OrderAction][]State{ OrderActionPay: {Pending, Shipped}, OrderActionShip: {Paid}, OrderActionComplete: {Shipped, Paid}, OrderActionCancel: {Pending}, } ) 

这么看来,状态机模式的开闭原则并没有给代码修改带来实际好处。

避免复杂的条件判断?没做到!

状态机模式声称能避免复杂的条件判断,让代码更清晰。但通过map改造if else后,状态维护在orderAction2ValidState里,通过action就能对应状态,条件判断并不复杂。

代码可读性更强?不见得!

状态机模式下,逻辑是结构化了,但想看支付逻辑,还得理解状态机的封装逻辑,不如直接看PaidOrder函数直观。

易于调试和测试?也不尽然!

状态机模式说每个状态行为封装在各自类中,方便单独测试和调试。但测试ShippedOrder时,用if else实现反而少了构造状态机的代码,测试起来更方便。

策略模式消除if else,也有“坑”!

策略模式和工厂方法也常用来消除if else,效果如何呢?看个例子,不同用户有不同的打折策略:普通用户不打折,VIP用户打八折,SVIP用户打五折。

先用直译的方式实现:

func CalculatePrice(user User, price float64) float64 { // 8折 if user.CustomerType == VIP { return price * 0.8 } // 5折 if user.CustomerType == SVIP { return price * 0.5 } return price } 

通过提前返回,代码看起来挺简洁。这个时候,有人可能想用策略模式和工厂模式封装。

先定义折扣策略接口:

type DiscountStrategy interface { Calculate(price float64) float64 } 

然后不同用户实现不同折扣策略:

type RegularUserDiscount struct{} func (r *RegularUserDiscount) Calculate(price float64) float64 { return price // 无折扣 } type VIPUserDiscount struct{} func (v *VIPUserDiscount) Calculate(price float64) float64 { return price * 0.8 // 8折 } type SVIPUserDiscount struct{} func (s *SVIPUserDiscount) Calculate(price float64) float64 { return price * 0.5 // 5折 } 

通过用户类型调用不同折扣策略计算:

var ( DiscountStrategyMap = map[CustomerType]DiscountStrategy{ Regular: &RegularUserDiscount{}, VIP: &VIPUserDiscount{}, SVIP: &SVIPUserDiscount{}, } ) func CalculatePrice(user User, price float64) float64 { strategy := DiscountStrategyMap[user.CustomerType] return strategy.Calculate(price) } 

根据函数是“第一公民”的特性,还能进一步简化代码:

type CalculateHandle func(float64) float64 func (c CalculateHandle) Calculate(price float64) float64 { return c(price) } func VIPDiscountCalculate(price float64) float64 { return price * 0.8 } func RegularDiscountCalculate(price float64) float64 { return price * 0.9 } func SVIPDiscountCalculate(price float64) float64 { return price * 0.5 } 

这样通过类型转换实现DiscountStrategy接口:

var ( calculateFunc = map[CustomerType]CalculateHandle{ VIP: VIPDiscountCalculate, SVIP: SVIPDiscountCalculate, Regular: RegularDiscountCalculate, } ) func CalculatePrice(user User, price float64) float64 { handle, ok := calculateFunc[user.CustomerType] if!ok { return price } return handle.Calculate(price) } 

封装后代码结构化了,看起来后续增加新用户类型,只需要增加相应的折扣计算方法就行。但问题来了!

代码存在过多假设

产品提出新需求,SVIP打完折后满300减30,VIP用户满500减30。这和之前的封装预想不同,之前是按用户类型抽象折扣策略,现在满减策略是按折扣类型抽象,不同的抽象角度让代码理解和修改都变得困难。

站在“现在”看“现在”,别盲目预测未来

有人可能会说,要是一开始按折扣配置方式封装就好了。但这就像炒股,我们没办法预知未来。现实需求也是如此,很多设计模式是站在“上帝视角”解决问题,对于已有项目重构优化可能有用,但对于需求不确定的产品,过度假设和抽象会限制代码灵活性。过早使用设计模式,后续需求可能得迁就之前的设计,反而不利于扩展。

不做超出功能的封装

还是用if else实现,从折扣角度进行封装:

type Discount struct { // 折扣率 DiscountRate float64 } func (d *Discount) Calculate(price float64) float64 { return price * d.DiscountRate } type FullDiscount struct { // 满多少钱可以减 TargetPrice float64 // 减多少钱 Discount float64 } func (f *FullDiscount) Calculate(price float64) float64 { if price >= f.TargetPrice { return price - f.Discount } return price } func getDiscounts(customerType CustomerType) []DiscountStrategy { if customerType == VIP { return []DiscountStrategy{ &Discount{DiscountRate: 0.8}, &FullDiscount{TargetPrice: 500, Discount: 30}, } } if customerType == SVIP { return []DiscountStrategy{ &Discount{DiscountRate: 0.5}, &FullDiscount{TargetPrice: 300, Discount: 30}, } } return []DiscountStrategy{ &Discount{DiscountRate: 1}, } } func Calculate(user User, price float64, discount Discount) float64 { // 获取折扣策略,这个可以支持配置 strategies := getDiscounts(user.CustomerType) // 给价格应用折扣策略 for _, strategy := range strategies { price = strategy.Calculate(price) } return price } 

这种方式保留了if else,但修改了构造折扣的方式。代码清晰易懂,能快速知道不同用户的折扣,还支持折扣链构造,完美实现当前需求,也没有引入多余假设。

如果后续折扣要支持配置,修改getDiscounts的构造就行,还能在局部进行策略缓存优化,不会影响折扣计算逻辑。这里假设做缓存是因为缓存属于技术层面,不依赖业务变化,即使假设不成立,也不影响需求迭代,和对业务需求的假设不同。

结构化代码能够减少修改带来的错误

当业务需求稳定,需要优化性能时,如果代码结构混乱,确实很难下手。这个时候合理的封装就很有必要,重构代码增加缓存优化时,也能避免出现BUG。通过抽象出getDiscounts,给它增加单元测试,改变配置看获取的折扣是否符合预期,就能保证计算金额的准确性。这样代码更“可测试”,性能优化也更方便,测试范围缩小,编写测试用例也更简单。

工厂模式,别着急用!

工厂模式和策略模式类似,如果产品方向不明确,没有确定的抽象,先别急着用。就像KubernetesproxyProvider,最初v1.0.0版本直接依赖Proixeriptables具体实现,等第一版功能完整后,才开始抽象出Provider接口。

// Provider is the interface provided by proxier implementations. type Provider interface { config.EndpointSliceHandler config.ServiceHandler config.NodeHandler config.ServiceCIDRHandler Sync() SyncLoop() } 

它不是一开始就进行抽象,而是先实现功能,在隔离上下层时总结出抽象方式。对于业务代码也是如此,一开始就创建抽象会限制上层业务变化。

工厂模式通过OOP思想内聚相同内容,开发时不用刻意套用。随着代码发展,自然会用到。还是以KubernetesProvider为例,抽象出Provider后,增加IPVSnftables时,通过createProxier实例化,就用到了工厂模式的思想。

func (s *ProxyServer) createProxier(...) (proxy.Provider, error) { var proxier proxy.Provider if config.Mode == proxyconfigapi.ProxyModeIPTables { if dualStack { proxier, err = iptables.NewDualStackProxier() } else { proxier, err = iptables.NewProxier() } } else if config.Mode == proxyconfigapi.ProxyModeIPVS { if dualStack { proxier, err = ipvs.NewDualStackProxier() } else { proxier, err = ipvs.NewProxier() } } else if config.Mode == proxyconfigapi.ProxyModeNFTables { if dualStack { proxier, err = nftables.NewDualStackProxier() } else { proxier, err = nftables.NewProxier() } } return proxier, nil } 

这种写法虽然不是标准的工厂模式,但直观明了。如果恰到好处能完成需求了,那我们没必要为了还未出现的需求去做后续的假设。

正确看待设计模式与if else

通过上面的例子可以看出,设计模式虽然强大,但并非解决所有if else问题的万能钥匙。在实际编程中,我们要根据具体情况来选择合适的方案。

设计模式是前人在大量实践中总结出来的经验,它们提供了通用的解决方案,能解决一些常见的软件设计问题。比如当业务逻辑变得复杂,if else嵌套过多,代码难以维护时,合理运用设计模式可以让代码结构更清晰,提高可维护性和扩展性。但这并不意味着我们要在所有情况下都使用设计模式,很多时候简单的if else语句就足以完成任务,而且它们更直观、更易于理解和调试。

在决定是否使用设计模式时,我们需要考虑以下几点:
1. 业务复杂度:如果业务逻辑简单,用if else就能清晰地表达逻辑,那就没必要使用设计模式。比如一个简单的用户权限判断,根据用户角色(普通用户、管理员)执行不同操作,用if else即可。但如果业务逻辑非常复杂,涉及多个状态的转换、多种策略的选择,并且这些逻辑会频繁变动,那么设计模式可能是更好的选择。
2. 代码的可维护性:设计模式的目的之一是提高代码的可维护性,但如果使用不当,反而会增加代码的复杂性。在使用设计模式时,要确保团队成员都能理解和维护代码。如果一个设计模式过于复杂,只有少数人能理解,那么在后续的开发和维护中可能会带来问题。
3. 可扩展性:设计模式通常能提高代码的可扩展性,当需求发生变化时,更容易进行修改和扩展。但在某些情况下,简单的if else也可以通过合理的代码结构和注释来实现较好的扩展性。例如,在一个简单的配置文件读取逻辑中,通过if else判断不同的配置项,只要代码结构清晰,后续添加新的配置项也不会太困难。
4. 性能影响:设计模式可能会引入一些额外的开销,如对象的创建、方法的调用等。在对性能要求较高的场景中,需要谨慎考虑设计模式的使用,确保不会对系统性能产生负面影响。

总结

设计模式和if else都有各自的适用场景,不能盲目地用设计模式替代if else。在实际编程中,我们要根据业务需求、代码的可维护性、可扩展性和性能等多方面因素综合考虑,选择最适合的解决方案。简单的问题用简单的方法解决,复杂的问题再考虑使用设计模式,这样才能编写出高质量、易维护的代码。

你在平时的编程中,有没有遇到过类似的困惑呢?是如何解决的呢?欢迎在评论区分享你的经验和想法。