阅尽网上该话题,代码如有雷同,请轻喷~
if/else 这个话题已经讨论很久了,时不时总是能看到新的文章讨论这个话题,我也总是默默的点个赞、留个抓。不能总是让这些文章躺在收藏夹里吃灰,再者我也应该对这个议题发表一下见解了,希望能给大家带来一个新的思路
本文很多思路需要依赖高阶语法糖,请大家注意,有的语言非常灵活,写法思路非常多,像 JS 灵活的不像样呀 ε(┬┬﹏┬┬)3 ,-->java 的话有一些很酷的思路用不上
本文专注于高阶语法糖,网上那些策略、工厂、封装思了,太麻烦,不推荐用来优化 if 语句,如果有必要的话,参考资料里有大把都在说这个
参考资料:
-
博客:
-
书籍:
《代码重构》
《重构与模式》
《代码大全2》
-
反向案例:
反向案例 --> 都是越搞越复杂的典型,是负优化,大家看看对比下
我对 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);
}
把一个复杂的`3元1次` if 语句,拆成3个`1元1次` 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,那其实也是不错的,最后代码还是要自己看、给同事看的,千万不要因为优化代码而成了负担,本末倒置,写只写自己明白的