if/else 我也来凑个热闹~

1,138 阅读9分钟

阅尽网上该话题,代码如有雷同,请轻喷~

if/else 这个话题已经讨论很久了,时不时总是能看到新的文章讨论这个话题,我也总是默默的点个赞、留个抓。不能总是让这些文章躺在收藏夹里吃灰,再者我也应该对这个议题发表一下见解了,希望能给大家带来一个新的思路

本文很多思路需要依赖高阶语法糖,请大家注意,有的语言非常灵活,写法思路非常多,像 JS 灵活的不像样呀 ε(┬┬﹏┬┬)3 ,-->java 的话有一些很酷的思路用不上

本文专注于高阶语法糖,网上那些策略、工厂、封装思了,太麻烦,不推荐用来优化 if 语句,如果有必要的话,参考资料里有大把都在说这个

参考资料:

反向案例 --> 都是越搞越复杂的典型,是负优化,大家看看对比下

我对 if/else 复杂度的理解

基于参与判断的参数个数分支语句的层级考虑,我把 if 语句按照复杂度分为:

  • 一元一次 if/else
  • 一元多次 if/else
  • 多元一次 if/else
  • 多元多次 if/else

1.1 一元一次 if/else

这是写的最多的 if 了:

if(state==1){
	print("11");
}

if(state==2){
	print("22");
}

1.2 一元多次 if/else

这样的代码大家谁都写过的 (lll¬ω¬) ,不要不承认哈~

String today = "周日";
Switch( dayForMonth % 7 ){
    case 0 : 
        today = "周日";
    case 1 : 
        today = "周一";   
    case 2 :
        today = "周二";   
    case 3 :
        today = "周三";   
    case 4 :
        today = "周四";   
    case 5 :
        today = "周五";   
    default:
        today = "周六";   
}

1.3 多元一次 if/else

参数多了 ヾ(´・ω・`)ノ

if(state==1 && age > 18){
	print("11");
}

if(state==2 && age < 5){
	print("22");
}

1.4 多元多次 if/else

if 里嵌套 if,一直到很多层

if(state==1 && age > 18){
	if(age > 18){
    	print("11-18");
        return;
    }
	print("11");
}

if(state==2){
	if(age < 5){
    	print("22-5");
        return;
    }
	print("22");
}

这种嵌套若是有个5-6层,那是很恐怖的。更有甚者,同级别的 if 还有多个分支语句。这样的代码,理解起来不光是别人,就是自己都很困难。这种代码就是、就是别人说的屎山,所以既不要给别人挖坑,也不要给自己挖坑 ο(=•ω<=)ρ⌒☆

下面的代码看着是不是很讨厌,是的必须讨厌,所以不要再写这样的代码了,往往坑的都是自己~

if(a){
	if(a.b){
    	if(a.b.1){
        	...
        }
        if(a.b.2){
        	...
        }
    }
    
    if(a.c){
    	...
    }
}

外国人管这种代码叫『航天飞机风格』,属于祖传代码,请勿修改的那种

if 语句优化思路

上面之所以这么划分 if 语句的复杂度,是因为我希望能像算法那样,根据明确的影响复杂度的因子来优化。我们可以从减少判断参数分支降维上来考虑

根据以往的项目经验,我多少有一点思路,说实话,捋清楚这些还是花了一些时间的。任何事只要需要大量思考的,从来不会很快 (~o ̄3 ̄)~ ,尤其是想写一篇有点深度的文章时,时间总是刷刷的流走,而字还没码多少 o(〃'▽'〃)o

1. 减少判断参数

减少判断参数 --> 这个是可以实现的,当然不是说真的不判断这个参数了,而是指可以把这个参数拿到 if 之外,这样就可以减少 if 的复杂度了

这一点是基于条件参数只有一个分支语句,这样的参数完全可以优化掉、或者下层分支合并。而且往往是判断参数的个数决定了分支语句的层级,看代码 ->

案例代码:

if(state==1){
	if(age>18){
    	if(name.equals("A")){
        	print(name);
        }
    }
}

对于这样的代码,我们可以这样做:

  • 合并判断参数,减少分支层级:
if(state==1 && age>18 && name.equals("A")){
	print(name);
}

例子代码看着简单、好做,很多人觉得没必要、不用学、我肯定知道!
但实际上,很多时候,我们还是会时不时的写出上面案例的代码出来
因为很多时候条件判断有些复杂干扰了我们书写逻辑
  • 另一种做法,拆分判断参数:
void main(){
	if(state!=1) return;
    if(age<=18) return;
    if(!name.equals("A")) return;

	print(name);
}

把一个复杂的`31次` if 语句,拆成3个`11次` if 语句,算是下面降维的一种了
写 if 并不是 low 的体现,只要我们写的代码好理解、优雅好看如艺术品般,多几个 if 也无妨~

2. 分支降维

向上面那样简单的 if ,一般都不是我们头疼的重点,那些复杂如这样的才是我们头疼的

if(state==1){
	if(age>18){
    	if(name.equals("A")){
        	...
        }
        if(name.equals("B")){
        	...
        }
    }
    
    if(age<5){
    	...
    }
}

if(state==2){
	...
}

对于这样的代码,我就不说网上那些策略、工厂、封装思路了,太麻烦。我们使用高阶语法糖,采用《代码大全2》中推荐的表驱动思路,把条件判断和分支执行语句都写成 function 函数存到 map 里,声明在类头部

这样逻辑查找起来也简单,还把最重要的核心逻辑从流程语句之中提炼出来,改变了我们挨个跟方法才能找到核心逻辑的传统思路

这段代码大家看个意思,基本就是我要说的了,大家体验下,想想一个上千行代码的主类中,我们使用这样的思路提炼出核心逻辑,看着爽不爽

class Test{

	// 3个参数的判断
	val maps = mutableMapOf<(id: Int, age: Int, state: Int) -> Boolean, () -> Unit>	
    (
    	// 分支1-->
		{ id: Int, age: Int, state: Int ->
			id in 1..100 && age < 10 && state == 1 } to { print("aa") },

		// 分支2-->
		{ id: Int, age: Int, state: Int ->
			id in 101..200 && age == 18 && state == 2 } to { print("bb") },
    )

	// 提供一个方法根据参数条件获取分支执行语句
    private fun test1(id: Int, age: Int, state: Int): () -> Unit {
        for ((k, v) in maps) {
            if (k(18, 8, 1)) {
                return@test1 v
            }
        }
        return {}
    }

    @RequiresApi(Build.VERSION_CODES.N)
    private fun test2(id: Int, age: Int, state: Int): () -> Unit {
        maps.forEach { (k, v) ->
            if (k(18, 8, 1)) {
                return@test2 v
            }
        }
        return {}
    }

	fun main(name: Int, age: Int, state: Int){
        test1(18, 8, 1)()
        test2(18, 8, 1)()	 
    } 
}

当然讨厌上面这么写的话大家可以继续使用 if,只要看着简单明了不就行了嘛~

if(id>=0 && id<=100 && age<10 && state==1){
	print{"AA"}
    return
}

if(id>=101 && id<=200 && age==18 && state==2){
	print{"BB"}
    return
}

实际 code 中还是有多技巧讲究的 ~ 这里我总结下大家的做法 o( ̄ε ̄*)

代码技巧

// 原始代码:
if(falg){
    someMethod()
}

// 优化代码:
falg && someMethod()
// 原始代码:
if(code ==='202'||code ==='203'||code ==='204'){
    someMethod()
}

// 优化代码:
if(['202','203','204'].includes(code)) someMethod()

三木运算度

三木运算度的这种写法挺长见的,希望大家用好这个,的确可以让代码看着舒服的多,也能少些不少代码

(state==1 && age>18) ? fun1(name,age) : fun2(name,age);

when、switch

我想这个大家都是耳熟能详的了~,虽然有的人说这样写 low,但是只要方便阅读、或是只能写成这样,那也是极好的~ ( ╯▽╰)

when(age){
	1 -> print("AA")
	2 -> print("BB")
	3 -> print("CC")
}
switch (name) {
	case commodity.phone: 
		console.log(9999)
		break
	case commodity.computer: 
		console.log(15999)
		break
	case commodity.television: 
		console.log(1999)
		break
	case commodity.gameBoy: 
		console.log(2500)
		break
}

let

利用语法糖的便利,我们可以对那些有很多 if 语句,前一个 if 分支计算结果是下一个 if 分支输入的这种 if,实现伪链式调用,我觉得这样代码起码要容易理解多了 o(〃'▽'〃)o

这里用的 let 是 kotlin 的语法糖,高阶语言中都能找到替代方案,希望给大家能提供一份思路

fun test3(test: Test): Int {

	test
		.let {
        	// 第一个分支处理
			if (test.name.equals("AA")) test.age = age + 1
			if (test.name.equals("BB")) test.age = age + 2
			return@let test.age }
		.let {
        	// 第二个分支处理
			return@let if (it > 20) it * 20 + 188 else it * 5 + 88 }
		.let { price: Int ->
        	// 最后执行语句
			print("最终价格$price")
            return@test3 price }

	return -1
}

正则表达式

js 这个代码我是真的看不懂,不过利用正则的思路绝对是我看到最赞的 d=====( ̄▽ ̄*)b

const CheckPrice = () => {
  const showMessage = () => {
    console.log('打开活动页面')
  }
  return new Map ([
   [/^phone_[0-4]$/, () => {
     console.log('活动时间价格是8888')
   }],
   [/^phone_[5-9]*$/, () => {
     console.log('其他时间10000')
   }],
   [/^phone_.*$/, () => {
    showMessage()
   }],
  ])
}
const showPrice = (name, date) => {
  [...CheckPrice()].forEach(([key,value])=> {
    key.test(`${name}_${date}`) ? value.call() : ''
  })
}
let date = [{date: '11-11', key: 1}, {date: '12-28', key: 9}]
let target = date.find((item, index) => item.date === '12-28')
showPrice('phone', target.key)

一维数组

数组在 if 语句优化中大有可为呀,数组下标代表不同分支

1. 今天周几

原始代码:

String today = "周日";
Switch( dayForMonth % 7 ){
    case 0 : 
        today = "周日";
    case 1 : 
        today = "周一";   
    case 2 :
        today = "周二";   
    case 3 :
        today = "周三";   
    case 4 :
        today = "周四";   
    case 5 :
        today = "周五";   
    default:
        today = "周六";   
}

优化代码:

var weekday = String[]{"周日","周一","周二","周三","周四","周五","周六"};
String today = weekday [ dayForMonth % 7 ];

二维数组

二维数组里,每一个维度都可以代表一个层级的分支语句

1. 这个月有几天

原始代码:

if(1 == iMonth) {iDays = 31;}
else if(2 == iMonth) {iDays = 28;}
else if(3 == iMonth) {iDays = 31;}
else if(4 == iMonth) {iDays = 30;}
else if(5 == iMonth) {iDays = 31;}
else if(6 == iMonth) {iDays = 30;}
else if(7 == iMonth) {iDays = 31;}
else if(8 == iMonth) {iDays = 31;}
else if(9 == iMonth) {iDays = 30;}
else if(10 == iMonth) {iDays = 31;}
else if(11 == iMonth) {iDays = 30;}
else if(12 == iMonth) {iDays = 31;}

优化代码:

  • 外层的维度判断是不是闰年
  • 内存的维度就是每个月的天数
const monthDays = [
  [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
  [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
]

function getMonthDays(month, year) {
  let isLeapYear = (year % 4 === 0) && (year % 100 !== 0 || year % 400 === 0) ? 1 : 0
  return monthDays[isLeapYear][(month - 1)];
}

console.log(getMonthDays(2, 2000))

2. 复杂参数判断

利用二维数组的交叉实现复杂、有规律的逻辑判断,下面这样的代码,判断条件可以成块参与业务逻辑的,可以用这个思路

原始代码:

if( (a && !c ) || (a && b && c)){
    category = 1 ;     
}else if( (b && ! a) || (a && c && !b)){
    category = 2 ;     
}else if ( c && !a && !b){
    category = 3 ;     
}else {
    category = 0 ;     
}

优化代码:

static int categoryTable[2][2]={
    //  !b!c   !bc   b!c   bc
        0,     3,    2,    2, // !a    
        1,     2,    1,    1, // a
}

category = categoryTable[1][0];

多个一维数组

有时候我们写二维或者多维数组不是这么方便或者不好写的时候,不妨把复杂的多维数组拆成多个一维数组,一样可以实现目的,每一个数组代表一个层级的 if 分支语句

1. 判断学生成绩等级

优化代码:

int [] grade = {59,79,84,89,94,100}; 
String [] level = {"F","E","D","C","B","A"},

public String getLevel (int grade){
    for( int i = 0 ; i < grade.length ; i++){
        if(grade <= grade[i]){
            return  level[i];
        }
    }
}

表驱动

表驱动来自于《代码大全2》这本书里,建议在类的开始声明 Map 来承载 复杂的业务逻辑,这自然也可以用于 if 语句的优化,key、value 依靠语法糖都可以存放 Function 函数,一般 key 装枚举、字符串、标记,value 装 Function 函数的做法比多

1. 单层 Map

这个例子拓展下大家的思路,也挺实际的,这样写看着其实挺舒服的~

优化代码:

def add(a,b):
    return a+b

def minus(a,b):
    return a - b

func_dict = {'+':add,'-':minus}
print(func_dict['+'](1,2))

2. 双层 Map

有时候 value 里装单个 Function 不够,我们需要装很多参数,声明对象太麻烦了也没必要,再加一层 Map 是个比较好的选择

下面是段 Data 代码大家看看意思

优化代码:

var data = <String, Map>{
	"A": {
		"name": "AA",
		"action1": (name) => print(name + "/AA"),
        "action2": (name) => print(name + "/CC"),
	},
	"B": {
		"name": "BB",
		"action1": (name) => print(name + "/AAAAA"),
        "action2": (name) => print(name + "/CCCCC"),
	},
};
  
var action = data["A"]["action"];
action("kk");

第二层 Map 里的 action 还可以写更多,action 即可以是判断条件,也可以是分支执行语句,也可以选择再加一层 Map 进来,大家看个意思,不一定就真的能找到应用场景,反正我是没碰到,纯是自己瞎想的~

var data = <String, Map>{
	"A": {
		"name": "AA",
        "select1": (name) => name.equals("AA"),
		"action1": (name) => print(name + "/AA"),
        "select2": (name) => name.equals("BB"),
        "action2": (name) => print(name + "/CC"),
	},
	"B": {
		"name": "BB",
		"action1": (name) => print(name + "/AAAAA"),
        "action2": (name) => print(name + "/CCCCC"),
	},
};

// 这样写 if 也能简单很多
Map map = data[state]
if(map[select1]) map[action1]()
if(map[select2]) map[action2]()

3. key、value 都装 Function 函数

这个就是前面说的那个例子了,这里再放一遍,kotlin 语法声明这里太麻烦了,不同语言的语法糖这里写法也是有很大差异的,肯定 js 写起来最爽,Data 也可以,反正 Kotlin 写起来最讨厌

class Test{

	// 3个参数的判断
	val maps = mutableMapOf<(id: Int, age: Int, state: Int) -> Boolean, () -> Unit>	
    (
    	// 分支1-->
		{ id: Int, age: Int, state: Int ->
			id in 1..100 && age < 10 && state == 1 } to { print("aa") },

		// 分支2-->
		{ id: Int, age: Int, state: Int ->
			id in 101..200 && age == 18 && state == 2 } to { print("bb") },
    )

	// 提供一个方法根据参数条件获取分支执行语句
    private fun test1(id: Int, age: Int, state: Int): () -> Unit {
        for ((k, v) in maps) {
            if (k(18, 8, 1)) {
                return@test1 v
            }
        }
        return {}
    }

    @RequiresApi(Build.VERSION_CODES.N)
    private fun test2(id: Int, age: Int, state: Int): () -> Unit {
        maps.forEach { (k, v) ->
            if (k(18, 8, 1)) {
                return@test2 v
            }
        }
        return {}
    }

	fun main(name: Int, age: Int, state: Int){
        test1(18, 8, 1)()
        test2(18, 8, 1)()	 
    } 
}

4. js 的 Map

js 语法太灵活了,还能这么写,我第一次看到流泪了~ 〒▽〒

const commodity = new Map([
  [{name: 'phone', date: '11-11'}, () => {
    console.log(9999)
    // do Something
  }],
  [{name: 'phone', date: '12-12'}, () => {
    console.log(9888)
    // do Something
  }],
  [{name: 'phone', date: '06-18'}, () => {
    console.log(9799)
    // do Something
  }],
  [{name: 'phone', date: '09-09'}, () => {
    console.log(9699)
    // do Something
  }],
]);
const showPrice = (name, date) => {
  [...commodity].forEach(([key,value])=> {
    key.name === name && key.date === date ? value.call() : ''
  })
}
showPrice('phone', '12-12')

其他思路

1. 注解驱动

很多框架中都能看到这种模式的使用,比如常见的 Spring MVC。这个模式的重点在于实现。现有的框架都是用于实现某一特定领域的功能,例如 MVC。故业务系统如采用此模式需自行实现相关核心功能。主要会涉及反射、职责链等技术。具体的实现这里就不做演示了

2. 事件驱动

写个 Handler 来处理,我觉得这么说的人真是蛋疼 ━━∑( ̄□ ̄*|||━━

3. Optional

Optional 是 java 8 引入的,可以用于 null 的判断,我个人觉得实际使用体验不是很友好,不仅写者麻烦,也看着麻烦,但是大家应该知道可以通过 Optional 做,遇到合适的场景还是不吝一用的

优化代码:

Optional<String> strOptional = Optional.of("Hello World!");
strOptional.ifPresentOrElse(System.out::println, () -> System.out.println("Null"))

4. 断言

思路就是有一个 if 条件就写一个断言抛出异常,然后同一接收处理,我觉得吧断言用在这里真不合适

最后

前面码字花了些时间,码到最后了,我想说,对于上面的内容,大家能接收多少就多少,能用多少就多少,切忌不可因为要优化而硬席写自己不熟悉、没搞懂的代码,那样绝对事与愿违~,即便只能写 when、switch,那其实也是不错的,最后代码还是要自己看、给同事看的,千万不要因为优化代码而成了负担,本末倒置,写只写自己明白的