case when else 默认随机_如何优雅的消解[If/Else森林]

一个If/Else 不可怕,可怕的是”If/Else 森林”。 广袤的和有深度是 If/Else森林的两个主要特点。

先看看他的广袤:

if(....){
}
else if(...){
}
else if(...){
}
else if(....){
}
else if(....){
}
else if(....){
}

如果每个大括号里的代码再多点,就很难阅读了。

深度是这样的:

if (...) {
  if (...) {
  if (...) {
}
  if (...) {
}
  else {
}
} else if (...) {
}
}

果然有“深度”,让人看得眼花缭乱。当然,兼具深度和广度就有点类似以前的C语言的混乱大赛,让人神魂颠倒了。

分支对于工程师的是为来说不是最好的,就如同并发/并行对工程师来说也是很难的东西。真正简单的,易于被我们思维接受的模式是顺序/线性。所以,为了避免if/else森林现象,可以采用如下几个思路去消解:

  1. 保证每个if/else 块代码精简
  2. 不要嵌套if/else
  3. 不要使用过多的分支
  4. 不用if/else

遗憾的是,if else语句几乎是所有程序语言都支持,并且是我们一开始学习编程就最早接触到语法。他已经深入很多程序员的骨髓。所以我们渐进式的来消解if else.

保持if else 里的代码精简很简单,就是把里面的逻辑都用一个函数封装起来。比如:

if(condtion){
  function1()
}else {
  function2()
} 

消除if/else的种种技巧

消除嵌套也可以使用相同的方法,通过在函数里写新的if /else 而不是在if/else的代码块里。这样充代码视觉上避免了if/else,虽然逻辑上依然是嵌套的。

if(condtion){
  function1()
}else {
  function2()
} 

然后

def function1()={
   if(...){}
   else {}
}

你也可以用对象调用实例方法或者object 条用object方法取代普通函数。

如果你写java或者go这种比较死板的语言,你会发现,在 if/else 里,占很大一部分是判断为Null,如果不为Null,我们需要做点什么,比如

var bj = ""
if(jack!=null){
  bj = doSomething()
}

在Scala中,我们完全可以通过Option来一行完成:

var bj = ""
jackOpt.map(item=> bj=item)
//当然如果你需要默认值的话
jackOpt.map(item=> bj=item).getOrElse("")

其实,我日常工作中也非常多的用到了这个技巧。比如一个参数,paramaters:Map[String,String],我们需要拿到里面的某个key的值,下面是一个比较复杂的案例

 val startingOffsets = (parameters.get("startingOffsets") match {
      case Some(value) => Option(value)
      case None =>
        (parameters.get("binlogIndex"), parameters.get("binlogFileOffset")) match {
          case (Some(index), Some(pos)) => Option(BinlogOffset(index.toLong, pos.toLong).offset.toString)
          case (Some(index), None) => Option(BinlogOffset(index.toLong, 4).offset.toString)
          case _ => None
        }
    }).map(f => LongOffset(f.toLong))

    parameters.get("startingOffsets").map(f => LongOffset(f.toLong))

如果你使用Map的get方法,天然得到的就是一个Option,所以我们可以先进行Match操作,然后如果为None,我还可以尝试找到一些其他的候选值,如果后选值也没有,还是返回None,最后如果有值的话,统一包裹成LongOffset. 我们看第一段代码的好处非常明显,在我们日常中,我们总是可以从多个地方去拿值,而且有优先顺序,我们拿到的值最好都是Raw值,最后得到了之后统一处理,否则我们需要保证每一个分支都没有忘记用LongOffset包裹。

所以Option是消解if/else 中null值判断的一个好技巧。另外我们也看到了 match本质上和if/else一样,都是分支流程:

parameters.get("startingOffsets") match {
      case Some(value) => Option(value)
      case None =>
        。。。
    }

他等价于伪代码:

if(parameters.get("startingOffsets") === Some(value)) {
     Option(value)
}
else if(parameters.get("startingOffsets") === None){
      case None =>
}

match 语句相比较而言,写法上会更有优势些,更清晰些。不过如果也很容易发生"match case 森林"的问题。不过match case 还有一个更高阶的用法可以极大的简化复杂if/else, 依然是上面的代码:

(parameters.get("binlogIndex"), parameters.get("binlogFileOffset")) match {
          case (Some(index), Some(pos)) => Option(BinlogOffset(index.toLong, pos.toLong).offset.toString)
          case (Some(index), None) => Option(BinlogOffset(index.toLong, 4).offset.toString)
          case _ => None
}

这里,binlogIndex和binlogFileoffset属于组合参数,我们需要将这两个参数设置组合成一个新的参数,他们都存在两种情况,有或者没有,所以,理论上我们需要处理四种组合,不过在很多实际场景里,并不需要你枚举所有组合。在上面的例子,binlogIndex必须存在,binlogFileOffset 则可以存在或者不存在,通过match case 变得极度简单。我们看看如果改写成if/else会是啥样的:

val binlogIndex = parameters.get("binlogIndex").getOrElse(null)
val binlogFileOffset = parameters.get("binlogFileOffset").getOrElse(null)

if(binlogIndex!=null && binlogIndex!=null){
 Option(BinlogOffset(index.toLong, pos.toLong).offset.toString)
}else if(binlogIndex!=null && binlogIndex==null)){
 Option(BinlogOffset(index.toLong, 4).offset.toString)
}else {
  None
}

上面的代码来源我的一个项目:

spark-binlog​github.com

现实生活中,其实会更复杂一些,我们看看下面的代码:

private def getPartitionOffsetsRanger(kafkaOffsetReader: AdHocKafkaOffsetReader, uniqueGroupId: String):
  (Map[TopicPartition, Long], Map[TopicPartition, Long]) = {

    if (sourceOptions.contains(AdHocKafkaSourceProvider.STARTING_TIME_OPTION_KEY)
      && sourceOptions.contains(AdHocKafkaSourceProvider.ENDING_TIME_OPTION_KEY)) {
      (kafkaOffsetReader.fetchStartingOffsetsByTime(
        sourceOptions.get(AdHocKafkaSourceProvider.STARTING_TIME_OPTION_KEY).get,
        sourceOptions.get(AdHocKafkaSourceProvider.TIME_FORMAT_OPTION_KEY).get),
        kafkaOffsetReader.fetchEndingOffsetsByTime(
          sourceOptions.get(AdHocKafkaSourceProvider.ENDING_TIME_OPTION_KEY).get,
          sourceOptions.get(AdHocKafkaSourceProvider.TIME_FORMAT_OPTION_KEY).get))
    } else if (sourceOptions.contains(AdHocKafkaSourceProvider.STARTING_TIME_OPTION_KEY)
      && !sourceOptions.contains(AdHocKafkaSourceProvider.ENDING_TIME_OPTION_KEY)) {
      (kafkaOffsetReader.fetchStartingOffsetsByTime(
        sourceOptions.get(AdHocKafkaSourceProvider.STARTING_TIME_OPTION_KEY).get,
        sourceOptions.get(AdHocKafkaSourceProvider.TIME_FORMAT_OPTION_KEY).get),
        getPartitionOffsets(kafkaOffsetReader, endingOffsets))
    } else if (!sourceOptions.contains(AdHocKafkaSourceProvider.STARTING_TIME_OPTION_KEY)
      && sourceOptions.contains(AdHocKafkaSourceProvider.ENDING_TIME_OPTION_KEY)) {
      (getPartitionOffsets(kafkaOffsetReader, startingOffsets),
        kafkaOffsetReader.fetchEndingOffsetsByTime(
          sourceOptions.get(AdHocKafkaSourceProvider.ENDING_TIME_OPTION_KEY).get,
          sourceOptions.get(AdHocKafkaSourceProvider.TIME_FORMAT_OPTION_KEY).get))
    } else if (sourceOptions.contains(AdHocKafkaSourceProvider.RECENT_OPTION_KEY)) {
      (kafkaOffsetReader.fetchRecentNumOffsets(
        sourceOptions.get(AdHocKafkaSourceProvider.RECENT_OPTION_KEY).get.toLong),
        getPartitionOffsets(kafkaOffsetReader, endingOffsets))
    } else {
      (getPartitionOffsets(kafkaOffsetReader, startingOffsets), getPartitionOffsets(kafkaOffsetReader, endingOffsets))
    }

  }

这个是if/else森林的典范。因为也涉及到参数条件组合,所以写起来确实比较复杂,可读性也比较差,如果按我们前面方式进行if/else消除,可以得到如下代码:

private def getPartitionOffsetsRange(kafkaOffsetReader: AdHocKafkaOffsetReader, uniqueGroupId: String):
  (Map[TopicPartition, Long], Map[TopicPartition, Long]) = {
    val dateFormat = sourceOptions.get(TIME_FORMAT_OPTION_KEY)

    def fetchStartingOffsetsByTime(time: String) = {
      kafkaOffsetReader.fetchStartingOffsetsByTime(time, dateFormat.get)
    }

    def fetchEndingOffsetsByTime(time: String) = {
      kafkaOffsetReader.fetchEndingOffsetsByTime(time, dateFormat.get)
    }

    (sourceOptions.get(RECENT_OPTION_KEY), sourceOptions.get(STARTING_TIME_OPTION_KEY), sourceOptions.get(ENDING_TIME_OPTION_KEY)) match {
      case (Some(rok), _, _) =>
        (kafkaOffsetReader.fetchRecentNumOffsets(rok.toLong), getPartitionOffsets(kafkaOffsetReader, endingOffsets))
      case (None, Some(left), Some(right)) =>
        (fetchStartingOffsetsByTime(left), fetchEndingOffsetsByTime(right))
      case (None, Some(left), None) =>
        (fetchStartingOffsetsByTime(left), getPartitionOffsets(kafkaOffsetReader, endingOffsets))
      case (None, None, Some(right)) =>
        (getPartitionOffsets(kafkaOffsetReader, startingOffsets), fetchEndingOffsetsByTime(right))
      case (None, None, None) =>
        (getPartitionOffsets(kafkaOffsetReader, startingOffsets), getPartitionOffsets(kafkaOffsetReader, endingOffsets))
    }

  }

可读性是不是好了很多了? 这段代码示例来源于我的一个项目

spark-adhoc-kafka​github.com

前面提到的几个算是比较高阶的消解方法,其实我平时也会用一些额外的小技巧避免if else,比如提前return就是一种。我们来看看。

if(a==0){
  doA()
}else {
  doB()
}

我们看看如何把这个if/else 消解掉。我们可以将条件放到doA/doB里去,下面是改写后的逻辑

def doA(a:Long) = {
 if(a==0) return;
 doRealA()
}

def doB(a:Long) = {
 if(a!=0) return
 doRealB()
}

doA(a)
doB(a)

本质上,我们将if /else的逻辑分解到了方法里面,这样我们就可以实现doA()/doB串行执行了,至于doA/doB的是否执行的逻辑,由doA/doB自己管理。上面的逻辑还可以这么写:

if(a==0) return doA();
return doB()

我们用return 替代了else,使得代码看起来更像串行的,也容易理解些。

最后我们来总结下,如果你想避免if/else森林问题,如果你又想要使用if/else,那么,请遵循以下原则:

  1. 保证每个if/else 块代码精简
  2. 不要嵌套if/else
  3. 不要使用过多的分支

具体技巧有使用函数来避免if/else的嵌套,使用return来优化函数组合调用。如果你尽量不用if/else,那么善用

  1. Option
  2. Match case

但是我们依然要避免Option/Match Case也森林化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值