一个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森林现象,可以采用如下几个思路去消解:
- 保证每个if/else 块代码精简
- 不要嵌套if/else
- 不要使用过多的分支
- 不用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-binloggithub.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-kafkagithub.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,那么,请遵循以下原则:
- 保证每个if/else 块代码精简
- 不要嵌套if/else
- 不要使用过多的分支
具体技巧有使用函数来避免if/else的嵌套,使用return来优化函数组合调用。如果你尽量不用if/else,那么善用
- Option
- Match case
但是我们依然要避免Option/Match Case也森林化。