Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

策略模式和表驱动优化你的条件语句 #14

Open
yinguangyao opened this issue Jan 20, 2019 · 0 comments
Open

策略模式和表驱动优化你的条件语句 #14

yinguangyao opened this issue Jan 20, 2019 · 0 comments

Comments

@yinguangyao
Copy link
Owner

在我们平时的开发中,if else是最常用的条件判断语句。在一些简单的场景下,if else用起来很爽,但是在稍微复杂一点儿的逻辑中,大量的if else就会让别人看的一脸蒙逼。
如果别人要修改或者新增一个条件,那就要在这个上面继续增加条件。这样恶性循环下去,原本只有几个if else最后就有可能变成十几个,甚至几十个。
别说不可能,我就见过有人在React组件里面用了大量的if else,可读性和可维护性非常差。(当然,这个不算if else的锅,主要是组件设计的问题)

这篇文章主要参与自《代码大全2》,原书中使用vb和java实现,这里我是基于TypeScript的实现,对书中内容加入了一些自己的理解。

从一个例子说起

日历

假如我们要做一个日历组件,那我们肯定要知道一年12个月中每个月都多少天,这个我们要怎么判断呢?
最笨的方法当然是用if else啊。

if (month === 1) {
    return 31;
}
if (month === 2) {
    return 28;
}
...
if (month === 12) {
    return 31;
}

这样一下子就要写12次if,白白浪费了那么多时间,效率也很低。
这个时候就会有人想到用switch/case来做这个了,但是switch/case也不会比if简化很多,依然要写12个case啊!!!甚至如果还要考虑闰年呢?岂不是更麻烦?
我们不妨转换一下思维,每个月份对应一个数字,月份都是按顺序的,我们是否可以用一个数组来储存天数?到时候用下标来访问?

const month: number = new Date().getMonth(),
    year: number = new Date().getFullYear(),
    isLeapYear: boolean = year % 4 == 0 && year % 100 != 0 || year % 400 == 0;

const monthDays: number[] = [31, isLeapYear ? 29 : 28, 31, ... , 31];
const days: number = monthDays[month];

概念

看完上面的例子,相信你对表驱动法有了一定地认识。这里引用一下《代码大全》中的总结。

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

使用表驱动法前需要思考两个问题,一个是如何从表中查询,毕竟不是所有场景都像上面那么简单的,如果if判断的是不同的范围,这该怎么查?
另一个则是你需要在表里面查询什么,是数据?还是动作?亦或是索引?
基于这两个问题,这里将查询分为以下三种:

  1. 直接访问
  2. 索引访问
  3. 阶梯访问

直接访问表

我们上面介绍的那个日历就是一个很好的直接访问表的例子,但是很多情况并没有这么简单。

统计保险费率

假设你在写一个保险费率的程序,这个费率会根据年龄、性别、婚姻状态等不同情况变化,如果你用逻辑控制结构(if、switch)来表示不同费率,那么会非常麻烦。

if (gender === 'female') {
    if (hasMarried) {
        if (age < 18) {
            //
        } else if (age < 65) {
            //
        } else {
            // 
        }
    } else if (age < 18) {
        //
    } else if (age < 65) {
        //
    } else if {
        //
    }
} else {
    ...
}

但是从上面的日历例子来看,这个年龄却是个范围,不是个固定的值,没法用数组或者对象来做映射,那么该怎么办呢?这里涉及到了上面说的问题,如何从表中查询?
这个问题可以用阶梯访问表和直接访问表两种方法来解决,阶梯访问这个后续会介绍,这里只说直接访问表。
有两种解决方法:
1、复制信息从而能够直接使用键值
我们可以给1-17年龄范围的每个年龄都复制一份信息,然后直接用age来访问,同理对其他年龄段的也都一样。这种方法在于操作很简单,表的结构也很简单。但有个缺点就是会浪费空间,毕竟生成了很多冗余信息。
2、转换键值
我们不妨再换种思路,如果我们把年龄范围转换成键呢?这样就可以直接来访问了,唯一需要考虑的问题就是年龄如何转换为键值。
我们当然可以继续用if else完成这种转换。前面已经说过,简单的if else是没什么问题的,表驱动只是为了优化复杂的逻辑判断,使其变得更灵活、易扩展。

enum genders {
    lessThan18 = '<18',
    between18And56 = '18-65',
    moreThan56 = '>65'
}
enum genders {
    female = 0,
    male = 1
}
enum marry = {
    unmarried = 0,
    married = 1
}
const age2key = (age: number): string => {
    if (age < 18) {
        return genders.lessThan18
    }
    if (age < 65) {
        return genders.between18And56
    }
    return genders.moreThan56
}
const premiumRate: {
    [genders: string]: {
        [marry: string]: {
            rate: number
        }
    }
} = {
    [genders.lessThan18]: {
        [genders.female]: {
            [marry.unmarried]: {
                rate: 0.1
            },
            [marry.married]: {
                rate: 0.2
            }
        },
        [genders.male]: {
            [marry.unmarried]: {
                rate: 0.3
            },
            [marry.married]: {
                rate: 0.4
            }
        }
    },
    [genders.between18And56]: {
        [genders.female]: {
            [marry.unmarried]: {
                rate: 0.5
            },
            [marry.married]: {
                rate: 0.6
            }
        },
        [genders.male]: {
            [marry.unmarried]: {
                rate: 0.7
            },
            [marry.married]: {
                rate: 0.8
            }
        }
    },
    [genders.moreThan56]: {
        [genders.female]: {
            [marry.unmarried]: {
                rate: 0.5
            },
            [marry.married]: {
                rate: 0.6
            }
        },
        [genders.male]: {
            [marry.unmarried]: {
                rate: 0.7
            },
            [marry.married]: {
                rate: 0.8
            }
    }
}
const getRate = (age: number, hasMarried: 0 | 1, gender: 0 | 1) => {
     const ageKey: string = age2key(age);
     return premiumRate[ageKey]
        && premiumRate[ageKey][gender]
        && premiumRate[ageKey][gender][hasMarried]
}

索引访问表

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

const ages: string[] = ['<18', '<18', '<18', '<18', ... , '18-65', '18-65', '18-65', '18-65', ... , '>65', '>65', '>65', '>65']
const ageKey: string = ages[age];

阶梯访问表

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

const ageRanges: number[] = [17, 65, 100],
  keys: string[] = ['<18', '18-65', '>65'],
  len: number = keys.length;
const getKey = (age: number): string => {
  for (let i = 0; i < len; i++) {
    console.log('i', i)
    console.log('ageRanges', ageRanges[i])
    if (age <= ageRanges[i]) {
      return keys[i]
    }
  }
  return keys[len-1];
}

阶梯访问适合在索引访问无法适用的场景,比如如果是浮点数,就无法用索引访问创建一个数组来拿到索引。
在数据量比较大的情况下,考虑用二分查找来代替顺序查找,。
在大多数情况下,优先使用直接访问和索引访问,除非两者实在无法处理,才考虑使用阶梯访问。

从这三种访问表来看,主要是为了解决如何从表中查询,在不同的场景应该使用合适的访问表。

参考资料:

  1. 代码大全(第2版)
@yinguangyao yinguangyao changed the title 表驱动法 策略模式和表驱动优化你的条件语句 Apr 15, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant