当前位置 博文首页 > 嘁,都2020了,你咋还在单纯的使用if-else

    嘁,都2020了,你咋还在单纯的使用if-else

    作者:行舟客 时间:2021-09-05 17:43

    在高级语言中,基本上都提供了像if-elseswitch-case 这样的条件语句,方便大伙进行判断——引导程序走向。我们在写程序时,常常需要指明两条或者更多的执行路径,使得程序执行时,能够选择其中一条路径,去执行相应的语句,产生对应的结果 —— 这也是条件语句在程序中的作用。


    if-else的例子

    各位在初学C语言时,应该都写过这样一个程序:输出每个月的天数

    //C语言代码片段
    int Days(int months, int years){
    	int days;
    	if(months==1 || months==3 || months==5 || months==7 || months==8 || months==10 || months==12){
    		days=31;
    	}else if(months==2){
    		if((years%4==0 && years%100!=0) || years%400==0){
    			days=29;
    		}else{
    			days=28;
    		}
    	}else if(months==4 || months==6 || months==9 || months==11){
    		days=30;
    	}else{
    		printf("输入错误!请重新输入:\n");
    		Days(months,years);
    	}
    	return days;
    }

    这个程序虽是“耳熟能详”的,但后来看着未免感觉有些【繁琐】,多层if-else的嵌套不仅使得可读性降低,还会大大影响程序运行的效率。。。


    if-else的问题

    从上面就可以看出,if-else判断语句使用起来非常简单,但是在稍微复杂的逻辑场景下,对if-else的频繁使用(或说:滥用)就会容易导致整个项目的可读性和可维护性大大降低。

    我们可以试想一下,如果项目中出现了一种新的情况,那么我们要在原有的代码基础上继续增加if-else。但是需求是不会减少的。这样恶性循环下去,原本的几个if-else可能在更新了几个版本后变成了几十个,这可真是令人哭笑不得的事。
    (当然,现在也许你的公司会有硬性要求,或者开发模板,那就恭喜你了…)

    从设计模式的角度考虑,if-else简直具有了“坏”代码具有的一切:

    • 数据和实现逻辑强耦合
    • 扩展麻烦,维护性低

    改善if-else

    if-else并非是需要全部被代替的,确切的说,我们现在只能去不断的改善它,使他运行的更为【流畅】。

    短路符号和三元表达式
    前几天笔者还在群里说这两个:短路符号,又叫“逻辑运算符”,在一些简单的场景下,我们完全可以用它来代替if-else(尤其是那些需要“几个条件同时满足”的场景下):
    比如这个——判断一个数是不是2的幂

    //c++代码片段
    class Solution {
    public:
      bool isPowerOfTwo(int n) {
      	//如果一个数是 2 的次方数的话,那么它的二进数必然是最高位为1,其它都为 0 ,
      	//那么如果此时我们减 1 的话,则最高位会降一位,其余为 0 的位现在都为变为 1,
      	//那么我们把两数相与,就会得到 0
        return (n > 0) && (!(n & (n - 1)));
      } 
    };

    我们也可以用三元符号来代替if-else,它是几乎最合适的计算机判断符号(笔者自认为!),尤其适用于多条件复合判断(一层嵌套一层)。不过需要注意的是,大量的三元运算符却容易影响代码的可读性:

    比如——判断 n! 结果尾数中零的数量

    //java代码片段
    public class Solution {
      public int trailingZeroes(int n) {
      	//不断递归
        return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5);
      }
    }

    当然,我们还有一种改进方法:如果每种条件下代码逻辑比较多,也可以考虑提前跳出来结束函数——这是借鉴了for循环。

    说说switch-case

    switch-case是语言自身提供的另一种条件语句,它和if在本质上并没有什么区别,只是代码看上去会更简洁。比如——判断年龄:

    goodswitch(age){
    	case 10:
    		break;
    	case 20:
    		break;
    	case 30:
    		break;
    	//...
    }

    但是switch-case无法从根本上解决多个相似条件下需要多次重复的问题。


    表驱动法

    这个是笔者最为推崇的一种写法,它几乎在大数据量判断、范围区别处理等问题上都有解决方案!

    现在让我们再来看文章开头那道题:输出每个月有多少天
    我们不妨转换一下思路,每个月份对应一个数字,而月份都是按顺序排列的,所以我们是否可以用一个数组来存储天数,然后用下标来访问?

    //javascript 语法片段
    const month=new Date().getMonth(),
    	year=new Date().getFullYear(),
    	isLeapYear=year%4==0 && year%100!=0 || year%400==0;
    
    const monthDays=[31,isLeapYear ? 29 : 28,31,30,31,30,31,31,30,31,30,31];
    const days=monthDays[month];

    哦,这个代码运行起来可简单多了——至少看起来是这样。

    还有上面判断年龄的代码,我们也可以这样写:

    //JavaScript 语法片段
    ages=[10,20,...];
    funs=['a1','a2',...];
    for(let i in ages){
    	if(age==ages[i]){
    		funs[i]();
    	}
    }
    function a1(){
    }
    function a2(){
    }
    //...

    看了两个例子,想必你对【表驱动法】有了了解:

    表驱动法就是一种编程模式,从表里面查找信息而不使用逻辑语句。事实上,凡是能通过逻辑语句来选择的事物,都可以通过查表来选择。对简单的情况而言,使用逻辑语句更为容易和直白。但随着逻辑链的越来越复杂,查表法也就愈发显得更具吸引力。——《代码大全》

    使用表驱动可不像if-else那样“轻松”,我们需要先思考两个问题:

    如何从表中查询数据?如果if-else判断的是范围,该怎么查?查什么?(数据?索引?)

    基于这两个问题,有人将依据表驱动的查询分为三种:

    直接访问索引访问阶梯访问

    1、直接访问表
    笔者最近按照母亲的“旨意”跑了一趟保险公司,发现这个保险费率非常麻烦——它会根据年龄、性别、婚姻状态等不同情况变化。看着上面输出日期的程序想一下,如果你用逻辑控制解构(if or switch)来表示不同费率,那会有多麻烦!(事实上,你的代码可能会像八爪鱼一样…)

    我们能够知道,这里的【年龄】是个范围!没法用数组或者对象来做映射。这有两种解决方案:直接访问表 or 阶梯访问表。笔者决定先试试“直接访问表”的方式,并找到了两种方法:

    复制信息从而能够直接使用键值:我们可以给 1-17 年龄范围的每个年龄都复制一份信息,然后直接用 age 来访问,同理对其他年龄段的也都一样。这种方法在于操作很简单,表的结构也很简单。但有个缺点就是会浪费空间,毕竟生成了很多冗余信息。(不建议使用)转换键值,如果我们把年龄范围转换成键呢?这样就可以直接来访问了,唯一需要考虑的问题就是有些情境下年龄如何转换为键值。

    对于第二种方法,有人可能疑惑了:还要用if-else转换? 当然。前面已经说过:简单的if-else不会有什么问题的,表驱动只是为了优化复杂的逻辑判断,使其更灵活、易扩展。

    //TypeScript 语法片段
    const Age={
    	0:"unadult",
    	1:"adult"
    }
    const Gender={
    	0:"female",
    	1:"male"
    }
    const Marry={
    	0:"unmarried",
    	1:"married"
    }
    
    const rateMap={
    	[Age[0]+Gender[0]+Marry[0]]:0.1,
    	[Age[0]+Gender[0]+Marry[1]]:0.2,
    	[Age[0]+Gender[1]+Marry[1]]:0.3,
    	[Age[0]+Gender[1]+Marry[0]]:0.4,
    	[Age[1]+Gender[0]+Marry[0]]:0.5,
    	[Age[1]+Gender[0]+Marry[1]]:0.6,
    	[Age[1]+Gender[1]+Marry[1]]:0.7,
    	[Age[1]+Gender[1]+Marry[0]]:0.8
    }
    const isAdult=(age:number)=>age>=18 ? 1: 0
    const getDate=(age,hasMarried,gender)=>{
    	age=isAdult(age)
    	return rateMap[Age[age]+Gender[gender]+Marry[marry]]
    }

    这样才是正确的打开方式嘛!

    哦对,刚刚好像还说了一种方法:
    2、阶梯访问表
    同样是为了解决上面那个年龄范围的问题,阶梯访问没有索引访问直接,但是会更节省空间。
    为了使用阶梯方法,你需要把每个区间的上限写入一张表中,然后通过循环来检查年龄所在的区间,所以在使用阶梯访问的时候一定要注意检查区间的端点。

    //TypeScript 语法片段
    const ageRanges:number[]=[17,65,100],
    	keys:string[]=['<18','18-65','>65'];
    const getKey=(age:number):string=>{
    	for(let i in keys){
    		//console.log(i);
    		//console.log(ageRanges[i]);
    		if(age<=ageRanges[i]){
    			return keys[i];
    		}
    	}
    	return keys[keys.length-1];
    }

    3、索引访问表

    实际中的保险费率问题,在处理年龄范围的时候很头疼,这种范围往往不像上面第一种方法中那么容易得到 ‘key'。
    我们当时提到了复制信息从而能够直接使用键值,但是这种方法浪费了很多空间,因为每个年龄都会保存着一份数据。
    但是如果我们只是保存索引,通过这个索引来查询数据呢?
    假设人刚出生是0岁,最多能活到 100 岁,那么我们需要创建一个长度为 101 的数组,数组的下标对应着人的年龄,这样在 0-17 的每个年龄我们都储存 ‘<18',在18-65储存 ‘18-65', 在65以上储存 ‘>65'。这样我们通过年龄就可以拿到对应的索引,再通过索引来查询对应的数据。
    看起来这种方法要比上面的直接访问表更复杂,但是在一些很难通过转换键值、数据占用空间很大的场景下可以试试通过索引来访问:

    //Typescript 代码片段
    const ages:string[]=['<18','<18','<18',...'18-65','18-65','18-65',...'>65','>65','>65',...'>65'];
    const ageKey:string=ages[age];

    这样虽然在造表的时候稍有些麻烦,但是在处理数据时却是异常简便!


    表驱动的典型应用

    表驱动最大的意义就是将条件判断(数据)和逻辑剥离分开,将条件用可配置的表(对象 or 数组)来管理

    将0-360°划分为8个不同的空间,但不要总是用if-else实现:

    //JavaScript 代码片段
    const keys=['A','B','C','D','E','F','G','H'],
    	range=[45,90,135,180,225,270,315,360];
    const degreeTkey=(rage)=>{
    	for(let i in range){
    		if(rage<=range[i]){
    			return keys[i];
    		}
    	}
    }
    const map={
    	'A':()=>{
    		//...
    	},
    	'B':()=>{
    		//...
    	},
    	//...
    }
    
    //调用如:
    map[degreeTkey(46)]();

    枚举解决if-else对应关系复杂的问题

    啥角色干啥事,这是一个很明显的对应关系,所以学过的“枚举”为啥不用?
    其实枚举和上面提到的【表搜索】很像:我们举一个“系统管理员操作权限”的问题
    首先定义一个公用接口 RoleOperation,表示不同角色所能做的操作:

    public interface RoleOperation {
      String op();//表示某个角色可以做哪些op操作
    }

    接下来我们将不同角色的情况全部交由枚举类来做,定义一个不同角色有不同权限的枚举类 RoleEnum

    public enum RoleEnum implements Role0peration {
      //系统管理员(有A操作权限)
    	ROLE_ ROOT_ _ADMIN {
    		@Override
    		public String op() {
    			return "ROLE_ ROOT_ ADMIN:" + " has AAA permission";
    		}
    	},
    	//订单管理员(有B操作权限)
    	ROLE_ ORDER_ ADMIN {
    		@override
    		public String op() {
    			return "ROLE_ ORDER_ _ADMIN:" + " has BBB permission";
    		}
    	},
    	//普通用户(有C操作权限)
    	ROLE_ NORMAL {
    	@Override
    		public String op() {
    			return "ROLE_ NORMAL:" + "has CCC permission";
    		}
    	};
    }

    而且这样一来,以后假如我想扩充条件,只需要去枚举类中加代码即可,而不是去改以前的代码,这岂不很稳!

    public class JudgeRole {
    	public String judge( String roleName ) {
    		//一行代码搞定!之前的if/else没了!
    		return RoleEnum.va1ue0f(roleName).op();
    	}
    }

    工厂模式解决if-else“分支过多”问题

    不同分支做不同的事情,很明显就提供了使用工厂模式的契机,我们只需要将不同情况单独定义好,然后去工厂类里面聚合即可。

    首先,针对不同的角色,可以单独定义其业务类:

    //系统管理员(有A操作权限)
    public class RootAdminRole implements Role0peration {
    	private String roleName ;
    	public RootAdminRole( String roleName){
    		this.roleName = roleName ;
    	}
    	@Override
    	public String op() {
    		return roleName + "has AAA permission" ;
    	}
    }
    //订单管理员(有B操作权限)
    public class OrderAdminRole implements RoleOperation {
    	private String roleName ;
    	public OrderAdminRole( String roleName ) {
    		this.roleName = roleName ;
    	} 
    	@Override
    	public String op() {
    		return roleName + "has BBB permission";
    	}
    }
    //普通用户(有C操作权限)
    public class NormalRole implements RoleOperation {
    	private String roleName ;
    	public NormalRole( String roleName){
    		this.roleName = roleName;
    	}
    	@Override
    	public String op() {
    		return roleName + "has CCC permission";
    	}
    }

    接下来再写一个工厂类 RoleFactory对上面不同角色进行聚合:

    public class RoleFactory {
    	static Map<String, Role0peration> roleOperationMap = new HashMap<>();
    	//在静态块中先把初始化工作全部做完
    	static {
    		role0perationMap.put( "ROLE_ ROOT_ ADMIN", new RootAdminRole("ROLE_ _ROOT_ ADMIN") ) :
    		roleOperationMap.put( "ROLE_ ORDER_ ADMIN", new OrderAdminRole("ROLE_ ORDER_ ADMIN") );
    		role0perationMap.put( "ROLE_ NORMAL", new NormalRole("ROLE_ NORMAL") );
    	}
    	pub1ic static RoleOperation getOp( String roleName ) {
    		return roleOperationMap.get( roleName ) ;
    	}
    }

    接下来借助上面这个工厂,业务代码调用也只需一行代码, if/else同样被消除了:

    public class JudgeRole {
    	public String judge(String roleName){
    		//一行代码搞定!  之前的if/else也没了!
    		return RoleFactory.get0p(roleName).op();
    	}
    }

    这样的话以后想扩展条件也很容易,只需要增加新代码,而不需要动以前的业务代码,非常符合“开闭原则”。

    jsjbwy