消除if else的几个技巧

发布时间 2024-01-01 21:14:42作者: 李若盛开

if else这样的控制流几乎在每个编程语言中都会存在。在go里面也不例外。但是如果代码里面充斥着许多的if else,那么对于阅读代码的人很难清除代码表达的的意思,特别是在if else里面嵌套很多层的if else。如下:

if condition {
  if condition2 {
      if condition4{
        if condition6{
         //...
        }
      } else condtion5{
      
      }
  } else condition3 {
  
  }
}

比如定义了一个函数,这个函数接收一个价格参数,根据不同的用户等级,有不同的打折方案。

首先我们想到的代码可能是如下:

func discount(price float64) string {
  // 用户是超级vip
  if superVip {
      return price*0.7
  }
  //钻石vip
  if diamondVip {
      return price*0.8
  }
   //黄金vip
  if goldenVip {
      return price*0.9
  }
}

第一个:使用策略模式+map

//定义一个策略接口
type PriceStrategy interface {
// 根据用户等级获取价格
  discount(price float64) float64
}

// 定义j结构体来一一实现接口
type SuperVipPriceStrategy struct{}

// 实现接口 只处理用户是超级vip的事情
func (g SuperVipPriceStrategy ) discount(price float64) string {
 
    return price*0.7
 
}

type DiamondVipPriceStrategy struct{}

// 实现接口 只处理用户是超级vip的事情
func (g DiamondVipPriceStrategy ) discount(price float64) string {
 
    return price*0.8
 
}
//...其他等级的实现是类似



//准备一个map存放所有额策略
var strategyMap = make[string]PriceStrategy

func init(){
 //初始化所有的策略
 strategyMap["superVip"] = &SuperVipPriceStrategy{} 
 strategyMap["diamond"] = &DiamondVipPriceStrategy{} 
 //...
}

type User struct {
  Id int
  //等级
  level string
  //...
}
func main(){

  var orderPrice = 99.1
  //准备一个用户,世纪业务应该是获取当前登录用户信息
  u := &User{
    Id:1,
    Level: "superVip",
  }
  g := strategyMap[u.Level]
  g.discount(orderPrice)
}

为了方便管理所有的策略实现,可以定义一个用来管理策略的专属上下文Context

type PriceStrategyContext struct {
 //存放策略的map
  m map[string]PriceStrategy
  
}

func NewPriceStrategyContext(m map[string]PriceStrategy) *PriceStrategyContext {
  return &PriceStrategyContext{
    m: m,
  }
}

//定义一个添加新的策略的方法
func (c PriceStrategyContext)register(pg PriceStrategy,name string){
  c.m[name] = pg
  
}

//根据名称获取一个策略
func (c PriceStrategyContext)get(name string) PriceStrategy {
  return c.m[name]
}

另一种Context

type PriceStrategyContext struct {
  PriceStrategy
}
//设置一个策略
func (c *PriceStrategyContext)SetPriceStrategy(riceStrategy PriceStrategy) {
  c.priceStrategy=priceStrategy
}
//执行策略
func (c *PriceStrategyContext)  exec(price float64) float64{
  return c.priceStrategy.discount(price)
}

第二个:表驱动

这个方法是把策略先保存到数据库表里面,通过数据库表的方式来存取。

比如数据库表是这样的:id(主键),name(策略的名称),impl(策略的实现),当查询出来之后,通过反射创建策略的对象。

/根据 type 创建对象
func NewObject(source any) any {
  t := releft.TypeOf(source)
  return reflect.New(t)
}

第三个:不需要特别处理的提前返回

if condition1 {
 if condition2{
   //...
 }
}

修改为

if !condition1 {
 return 
}

if !condition2{
 return 
}
//... 真正需要处理的逻辑

第四个:责任链模式

比如业务里面有许多的校验规则,当然简单的可以使用validator打tag的方式进行校验。

但是有时候随着业务的复杂性增加,使用validator这种通用校验的方式变得不那么适用,可能每个业务的校验规则都不一样,那么每次都要进行很多if else的判断,会让代码量爆炸的,最后再来看山一样的代码心态都要崩了。

比如对一个订单信息进行校验,包含如下的几个校验:

价格的校验
订单个数的校验
商品总数的校验
会员等级的校验
活动时间的校验

一般的写法我想大家已经脑补出很多的if else了。但是责任链模式可以帮助你从无数多的if else里面解脱出来。责任链模式适合条件灵活多变的场景。

下面直接使用责任链的方式来实现复杂的校验:

//需要校验的信息
type OrderCheckConditions struct{
  Price float64
  OrderCount int
  ProductCount int
  UserLevel string 
  ActivityTime time.Time
}

//定义一个校验的通用接口
type OrderChecker interface{
  Check(c *OrderCheckConditions) bool
}

//定义具体的校验
type PriceChecker struct{}

func (p *PriceChecker)Check(c *OrderCheckConditions) bool {
    return c.Price>0
}

type OrderCountChecker struct{}

func (oc *OrderCountChecker)Check(c *OrderCheckConditions) bool {
    return c.OrderCount>0 && c.OrderCount<=2
}
//...其他的校验类似

由于需要所有的校验都是true的时候,业务流程才可以往下面走,所以需要聚合所有的校验结果。

//责任链
func chain(c *OrderCheckConditions, checkers ...Checker) bool {
  for _,checker := range checkers {
    if !checker.check(c) {
     return false
    }
  }
  return true
}

第五个:使用assert断言

testify这个包里面定义了许多断言的方法,引入testify

go install -u github.com/stretchr/testify

使用

import (
  "testing"
  "github.com/stretchr/testify/assert"
)

func TestEqual(t *testing.T) {

  var a string = "Hello"
  var b string = "Hello"

  assert.Equal(t, a, b, "The two words should be the same.")

}
//或者这样
func TestEqual2(t *testing.T) {
 // assertions 可以调用很多断言的方法
  assertions := assert.New(t)
  assertion.Equal(a, b, "")
  // ...
}

第六个:optional(可选模式)

这个在java里面已经内置了Optional这个类型,go里面还没有支持这个类型,但是实现的思路是比较简单的。有兴趣的同学可以参考这个实现:

https://github.com/ThisIsClark/go-optional

在写函数时,也可以使用这样的思想

//获取某个配置的时候,如果是空的给默认值
someName := config.OptionalString("name", "def")