JS 条件判断小技巧(二)

我前面讲过一期关于条件判断小技巧的文章,今天接着聊。所谓小技巧,说实在就是特定场景里的特例手段;对于具备一定开发能力的码农,这些特例基本都能避开。但是,某些业务逻辑本身就十分复杂,嵌套的条件语句在逻辑层面就不可能有所优化了;碰到这类场景,我们又该如何作为呢? function greeting(role, access) { if( 'owner' === role ){ if( 'public' === access ){ ... } if( 'private' === access ){ ... } ... } else if ( 'admin' === role ){ if( 'public' === access ){ ... } if( 'private' === access ){ ... } ... } else if( 'hr' === role ) { ... } } 看一下代码,第一层的if-else判定的是各种角色(role)类别,第二层判定的是角色访问权限设置(access)。这类代码其实并没有特别优雅的处理手段,只能回到《clean code》里最本源的解决手段——把函数写小。本质问题还是函数体过大,而所谓的把大函数拆成多个小函数,事实上就是以抽象换取可读性。 OOP 多态 最常规的手段就是 OOP 多态了。上述代码块,第一层的 role 抽象为 User 实例,嵌套层内的各种 access 进一步抽象为 User 的实例方法。 class User { public() { throw new Error('Denied!') } private() { throw new Error('Denied!') } } Javascript 并没有 interface 这类语法,好在有 class 了,我仿造 interface 写了一个基类如上。接着就是将各种角色抽象为新的子类型: class Owner extends User { public() { console.log('Owner in public'); } private() { console.log('Owner inside'); } } class Admin extends User { public() { console.log('Admin in public'); } private() { console.log('Admin inside'); } } ... OOP 推荐使用工厂方法初始化实例,我顺手也写个工厂,这样便可以利用工厂方法消除掉了第一层if-else class UserFactory { static create(role) { if( 'owner' === role ) return new Owner(); else if( 'admin' === role ) return new Admin(); ... } } 调用的时候我们先通过 role 创建抽象实例,再根据 access 调用具体方法: function greeting(role, access) { const user = UserFactory.create(role); user[access](); } 上面一长串的if-else,一下子被压缩到了两行。这就实现了以抽象(很多可描述的类)换取了可读性(较少的判断嵌套) 调用链 OOP 效果确实很明显,不过上述代码还是过于特例,假如access并不是字符串(如1,2,3),像user[1]这种就很难映射到具体方法了;所以我们往往还要写更细碎的 access 抽象,也便意味着更多的抽象子类,以及新的工厂方法。很多时候,我们也不并需要抽象得尽善尽美。这个场景里写个调用链,也是勉强可用的: const rules = [ { match(role, access) { return 'owner' === role; }, action(role, access) { if( 1 === access ) console.log('Owner in public'); else if( 2 === access ) console.log('Owner in private'); } }, { match(role, access) { return 'admin' === role; }, action(role, access) { ... } } ... ]; 上面 rules 数组里,每一个元素(rule)里的match被设计用来判定用户权限:遍历数组,若是match为 true,则运行正下方的action——access 相关业务;反之,继续match下一个 rule: function greeting(role, access){ rules.find(e => e.match(role)).action(role, access) } 最后 greeting 被重构为上述代码。当然,效果没有多态好,只消掉了一层if-else,第二层判定还是留在了 action 里。 AOP AOP,没看错,Javascript 也是有 AOP 的,只是它的实现要修改 Function 的原型链,不是很推荐;但是Function.prototype.before,Function.prototype.after还是挺常见的,开发组里能协商好,还是可以尝试一下的: Function.prototype.after = function(next) { let fn = this; return function $after(...args) { let code = fn.apply(this, args) next.apply(this, args); return code; } } 传统的 aop after 如上所示。不难看出,用到了高阶函数:具体执行时,先运行函数本体,再运行 after 传进来的 next 方法。为了让 after 应用到我们的话题中,我稍微改一下函数实现: const nextSmb = Symbol('next'); Function.prototype.after = function(next) { let fn = this; return function $after(...args) { let code = fn.apply(this, args) if( nextSmb === code ) return cnext.apply(this, args); return code; } } 这个 after 实现变成了先运行函数本体,若返回是nextSmb则继续执行后续的 next 方法,反之则停止。有什么用呢?我们看看如何使用: function owner (role, access) { function public(access) { return 1 === access ? console.log('owner in public') : nextSmb; } function private(access) { return 2 === access ? console.log('owner in private') : nextSmb; } const ownerChain = public.after(private); return 'owner' === role ? ownerChain(access) : nextSmb; } 代码还是有点难度的,先看一部分——owner 的定义。这个函数被设计处理role === 'owner'时的逻辑,内部的public和private方法是处理access为 1 和 2 时的逻辑。我们把public和private方法串联成ownerChain(终于用到after方法了),它的作用就是把之前的if-else逻辑抽象成一个上节讲到的函数调用链,在遍历调用链时检查 access 条件:若符合条件,则执行本节点代码,并结束调用链;反之,继续往调用链的后续节点传送。 我把重构后的 greeting 也列一下——单个role的access可以用after串联;不同role之间也可以进一步利用after串起来。 function admin (role, access) { // familiar with owner } let greeting = owner.after(admin) greeting('owner', 1); 嗯,这样,我们最原始的greeting方法就被彻底重构了。可以预见,如果调用链很长greeting会是这样: let greeting = owner.after(admin).after(hr).after(staff)... 当然这个方法缺点也很明确,比起之前冗长的代码,可读性增强了,但是理解成本有点高,若团队内没有事先约定,这个维护起来还是挺难的。 ramda ramda是我很喜欢用的一个方法库,在 github 上有大约 18K 的 star,它提供了一整套 FP 方法。比起上面调用链和aop这种野路子,ramda 库更适合在团队内推广。我们试着用 ramda 重写一下上面提到的greeting方法: const R = require('ramda') const ownerChain = R.cond([ [(role, access) => 1 === access, () => console.log('owner in public')], [(role, access) => 2 === access, () => console.log('owner in private')], ]) const adminChain = R.cond([ ... ]) const greeting = R.cond([ [R.equals('owner'), ownerChain], [R.equals('admin'), ownerChain], ]) 我想大家即便没用过 ramda,也能大体猜出代码用法吧。R.cond类似于上面用到的调用链实现:二维数组第一列就是 match 函数,做判定;第二列就是 action 函数,用于执行嵌套逻辑。若嵌套较深,可以像ownerChain和adminChain一样再实现一套R.cond调用链。 我们再将 FP 的 ramda 实现与上面 OOP 多态做个比较:OOP 将逻辑抽象为对象,FP 则是抽象为更小的函数。通常来说 FP 的代码更加精简,但是学习成本更高:如果没有专项训练,你根本看不懂 FP 代码,更别说码代码了。我自己部门里也有写半吊子 FP 的团队,最后写出来的代码长得像迎客松一样,并没有比多层嵌套的条件语句美观多少。 小结 本文在之前if-else小技巧的基础上,介绍了一些更通用场景里的优化方式。(当然,有些方式哗众取宠了?)虽然大篇幅介绍了一些野路子,但最终还是推荐大家学习正统 OOP、FP 的解决方案。学生时代,我们很少接触代码,还没事还喷喷书本知识;工作后,见识多了,才发现前人的经验弥足珍贵。 相关 《JS 条件判断小技巧(一)》 文章同步发布于an-Onion 的 Github。码字不易,欢迎点赞。

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):