也许你看到过这个新闻:
也许你还看过这个图:
自从某FFF团把情人节电影院的单号位置电影票全买走之后,电影院发现,不能再这么纵容他们了,于是就给了售票系统验证用户选座的要求。
在重构系统过程中,发现原来的验证逻辑非常复杂,搬运逻辑不如直接重写,选座验证逻辑也是下手的好机会
回到出发点
原始代码很复杂,所以需要去找这里的原始需求;经过各种沟通,发觉这里的出发点其实很简单:中间不要剩余一个座位。
如果中间只有一个位置,那么情侣就没办法买,只能卖给单身狗了,这怎么可以呢
正经一点说就是:
避免产生新的单一空余座位
但是电影厅的座位分布并不是理想化的,而且卖出去的座位分布也可能是各种各样,容易产生各种情况,也导致了原始的判断逻辑异常复杂。
原始的逻辑类似上图,由许多if嵌套组成
思考路径
直接从旧代码进行梳理比较麻烦,不如从原始需求着手
电影院选座场景目前已经被简化为类似上图的模型,里面包含了电影院的座位排布信息,以及已经出售的座位信息(红色),以及没有座位的位置(通道等)
如果我选择了上图绿色的座位,那么其左边就会有一个单独的空位,这也是需要避免的部分
那么,是不是在我选择的座位旁边有一个空位,再旁边有一个其他位,就是要避免的情况呢?
再展开一下,其他座位包含了上图所述的四种情况:另一个我选择的座位、已售座位、无座椅位置、场地边缘
但是有一种情况是例外,如上图,我选了中间任意一个位置,都会造成旁边的空位变成单独位置,但是这样是一种可以接受的场景;
否则可能会出现整场(或者某行)剩余两个座位,却买不了一张票的情况。
再进行一下整理,如上图所示,“绿空绿”的情况下,可以肯定是至少三个连续可选座位,所以不用考虑 可能是中间只剩余两个可选座位的情况
“绿空红”,“绿空无”,“绿空边” 的情况下,需要考虑可能是中间只剩余两个可选座位的情况。
本图绿框里面的即为前者,红框里面的情况即为后者。
转化模型
思路已经整理好了,怎么把这个逻辑转为代码呢?
也许很多人第一时间想到的是用if(很多的if),但是会导致代码冗长且难以理解。
仔细想想,这个思路其实就是一种模式匹配,而且是一维(线性)的,说道线性模式匹配,其实js中有一个利器。
没错!就是Regex,正则表达式
那么我们继续整理
因为我们可以选择多个座位,所以上面的思路整理为这样几种情况:
- N绿,空,N绿 (我刚选择的座位中间出现了一个单独的空位)
- 空,N绿,空,红或无或边 (我刚选择的座位,和右边其他非空位情况的位置产生了一个单独空位,而且左边还有一个空位可以选)
- 红或无或边,空,N绿,空 (我刚选择的座位,和左边其他非空位情况的位置产生了一个单独空位,而且右边还有一个空位可以选)
后面两种都是在至少有三个连续空位,所以可以不留单独空位的情况下,产生了一个单独空位
(N表示一个或多个)
是不是感觉有点熟悉?对,N就和正则表达式中的加号是一样的,那么整理一下我们可以写成:
- 绿写成S
- 空写成A
- 边(边缘)写成E,无(无座椅)写成V,红(已售)写成L
就可以写成图上的三个正则表达式
写成代码就是:
1 2 3 | let centerOneSpace= /S+AS+/; let leftOneSpace= /BAS+A/; let rightOneSpace= /AS+AB/; |
再使用正则表达式的 test 来验证每行有没有以上三种情况即可。如果有则当前选座不合法
优化结果
原来的259行多重嵌套的if代码
重写为29行逻辑清晰的代码
以下为第一版文章后半部分
逻辑梳理
首先,每次判断只需要对单行来进行,(毕竟这个是为了情侣考虑的,基本上情侣左右相邻,而不会前后相邻而坐),那么就有这么几种情况是不可以的:
- 刚选的两个座位中间有一个空位
- 刚选的座位和其他位置中间有一个空位
后者展开为:
- 刚选的两个座位中间有一个空位
- 刚选的座位和已售出的座位中间有一个空位
- 刚选的座位和无座位置(没有椅子的位置)中间有一个空位
- 刚选的座位和影厅边缘中间有一个空位
其中后边几种可以归纳起来,就会变成:
- 刚选的两个座位中间有一个空位
- 刚选的座位和(已售出的座位或无座位置或影厅边缘)中间有一个空位
但是这个第二种情况有特殊情况是可以选择的,即:如果只有两个空位(旁边是已售出的座位或无座位置或影厅边缘),则可以只选其中一个位置。(不然单身童鞋可能面临还有俩位置却买不了票的奇葩情况)
明明剩俩座位却不能只买一张,看个电影也要欺负单身狗,这世界没救了
所以讲上述条件的第二个加上限制,就会变成 以下两种情况是不可以的:
- 刚选的两个座位中间有一个空位
- 刚选的座位和(已售出的座位或无座位置或影厅边缘)中间有一个空位,但是另外一边是(已售出的座位或无座位置或影厅边缘)的情况除外
这里可以想到,(已售出的座位或无座位置或影厅边缘)的情况除外,那不就是可选的空座吗?于是就可以转化为:
- 刚选的两个座位中间有一个空位
- 刚选的座位和(已售出的座位或无座位置或影厅边缘)中间有一个空位,但是另外一边也必须是可选的空位
转为代码
逻辑整理的差不多了,那么可以开始转化为代码了,如果用一般的思路,大概需要双重循环加上很多个if判断语句,上述的简洁思想就没办法传承到代码中了
这时候,我想到一个很方便的方式,我们这里要做的事情基本上就是pattern recognizing,即模式识别;想到这里,就可以想到作为擅长处理字符串的js有一个内置大杀器,即regex 正则表达式。正则表达式最擅长的就是字符串模式识别、模式套用。
那么想到这里,我们要做的就是把每行的座位先转换成字符串,然后用相应的pattern去套,看是不是能够匹配了。
这里这样去转换:
说明 | 代码 |
---|---|
用户选择的座位: | V_SELECT='S' |
边缘: | V_EDGE='E' |
没有座: | V_VOID='V' |
已经卖出去: | V_LOCK='L' |
空位: | V_AVAILABLE='A' |
1 2 3 4 | let oneBlock=`(${V_EDGE}|${V_VOID}|${V_LOCK})`; let S=V_SELECT, A=V_AVAILABLE, B=oneBlock; |
然后将我们的规则转化为正则表达式:
- 刚选的两个座位中间有一个空位
S+AS+
- 刚选的座位和(已售出的座位或无座位置或影厅边缘)中间有一个空位,但是另外一边也必须是可选的空位
BAS+A+
A+S+AB
那么这里的验证选座的代码部分就是这个样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | validateSelect(){ let oneBlock = `(${V_EDGE}|${V_VOID}|${V_LOCK})`; let S = V_SELECT, A = V_AVAILABLE, B = oneBlock; let leftOneSpace = `${B}${A}${S}+${A}+`; let rightOneSpace = `${A}+${S}+${A}${B}`; let centerOneSpace = `${S}+${A}${S}+`; let tip = '请不要留下单独空座'; let valis = [ [new RegExp(leftOneSpace), tip], [new RegExp(rightOneSpace), tip], [new RegExp(centerOneSpace), tip], ]; for (let i = 0; i < this.matrix.length; i++) { let oneRow = this.matrix[i]; if (!oneRow) { continue } let s = this.rowToValidatingString(oneRow); for (let j = 0; j < valis.length; j++) { let oneVali = valis[j]; if (oneVali[0].test(s)) { return { validate: false, reason: oneVali[1] } } } } return { validate: true, reason: null } } |
有两个循环,一个是循环所有座位行,一个是循环这几个正则表达式,然后每行都会被每个正则表达式所验证,如果验证通过了,说明选座是不合法的,就会在界面提示,阻挡下一部动作。
优化结果
原选座判定
1 2 3 | function seatPolicy (event, $seat) { ...这里有257行... } |
新版选座判定
1 2 3 | validateSelect(){ ...这里有29行... } |
实现的最终效果是相同的,这里还将判断逻辑巧妙的缩减,将分散的座位判断改换为连续的模式验证。