嘁,都2020了,你咋还在单纯的使用if-else
行舟客 人气:0在高级语言中,基本上都提供了像if-else
和switch-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(); } }
这样的话以后想扩展条件也很容易,只需要增加新代码,而不需要动以前的业务代码,非常符合“开闭原则”。
加载全部内容