别盲目用设计模式替代if else,真没那么香!

后端 潘老师 3个月前 (02-05) 83 ℃ (0) 扫码查看

刚接触编程的时候,我和很多人一样,觉得代码里的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。在实际编程中,我们要根据业务需求、代码的可维护性、可扩展性和性能等多方面因素综合考虑,选择最适合的解决方案。简单的问题用简单的方法解决,复杂的问题再考虑使用设计模式,这样才能编写出高质量、易维护的代码。

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


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/13250.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】