从【if...else...】到【责任链】再到【composeAOP】,顺带把【传参】解决了~

简介: 从【if...else...】到【责任链】再到【composeAOP】,顺带把【传参】解决了~

image.png

🐛嵌套毛毛虫



我猜你一定见过这样的代码:


if(condition1 === A1){
    if(condition2 === A2){
        ...
    }else if(condition2 === B2){
        ...
    }else if(condition2 === C2){
        ...
    }else{
        ...
    }
}esle if(condition1 === B1){
    ...
    ...
    ...
}else if(condition1 === C1){
    ...
    ...
    ...
}else if(condition1 === D1){
    ...
    ...
    ...
}else{
    ...
}


讲真,并不是说这个代码有多坏,但是每次看到的时候都会引起本瓜不适。

感觉它就像是一只毛毛虫。。。


为了形象的表达这一点,本瓜诚邀灵魂画师 守护安东尼 作示意图一张,salute!!( ̄︶ ̄)↗ 

image.png


* 图片来源:守护安东尼,未经允许,随意转载。

这样写,总是会伴随着各种各样的逻辑判断、隐式输入、输出,还真不太敢动它,担心它直接“死”给你看!


🌴责任链竹节



镜头转向【责任链】,它是 23 种设计模式之一,属于行为型模式,关注对象之间的交互、通信;


参数输入到一个初始函数中,如果不满足当前函数条件,则传递到下一函数中进行处理,满足停止,不满足再传递,这样 one by one 向后进行,直至满足条件或传递结束。


一个个元函数就像是一节节竹节,独立可拆卸、再任意组装;

闲话少说,实现它的代码大致是这样的:


function A1(condition1){
    chainA2.next(chainB2).next(chainC2);
    return condition1 === A1 ? chainA2.setParam(condition2) : 'doNext'
}
function B1(condition1){
    return condition1 === B1 ? ... : 'doNext'
}
function C1(condition1){
   return condition1 === C1 ? ... : 'doNext'
}
function D1(condition1){
   return condition1 === D1 ? ... : 'doNext'
}
...
function A2(condition2){
    return condition2 === A2 ? ... : 'doNext'
}
function B2(condition2){
   return condition2 === B2 ? ... : 'doNext'
}
function C2(condition2){
   return condition2 === C2 ? ... : 'doNext'
}
chainA1.next(chainB1).next(chainC1).next(chainD1)
chainA1.setParam(condition1)


整体感官上,是不是像竹子一样?每一节(函数输入、输出)特别清晰。关键是,它解耦了,组装起来也超级方便~

image.png

* 图片来源:守护安东尼,未经允许,随意转载。


核心的,生成 Chain 的代码如下:

Chain 函数是高级函数,入参是一个函数。这里通过原型链的方式给它加了 next、setParam 两个属性。next 的入参也是 fn,用于设置下一个处理函数,setParam 用于传递原始入参;


var Chain = function( fn ){
  this.fn = fn;
  this.successor = null;
};
Chain.prototype.next = function( successor ){
  return this.successor = successor;
};
Chain.prototype.setParam = function(){
  var ret = this.fn.apply( this, arguments );
  if ( ret === 'doNext' ){
    return this.successor && this.successor.setParam.apply( this.successor, arguments );
  }
  return ret;
};


🍜函数特性AOP



实际上,利用 JavaScript 的函数式特性,还有一种更加方便的方法来创建责任链 —— 即 AOP。


面向切面编程(AOP:Aspect Oriented Program)思想的简单理解:动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。


代码如下:

/**
 * 函数交织(AOP)
 * @param {*} fn
 * @returns
 */
Function.prototype.before = function(fn) {
  const self = this
  return function(...args) {
    const result = fn.apply(null, args)
    return self.call(null, result)
  }
}
Function.prototype.after = function(fn) {
  const self = this
  return function(...args) {
    const result = self.apply(null, args)
    return fn.call(null, result)
  }
}


调用示例:


fn1 = step2.before(init).after(step3).after(step4)
//fn1 = init -> step2 -> step3 -> step4


我们可以任意指定、搭配函数的执行先后关系;


🥂composeAOP



还记得《感谢 compose 函数,让我的代码屎山💩逐渐美丽了起来~》这篇文章吗?compose 其实有很多种写法!我们可以借助上面的 before 和 after 函数实现这一版的 composeAOP ~


const composeAOP = function(...args) {
  const before = args.pop()
  const start = args.pop()
  if (args.length) {
    return args.reduce(function(f1, f2) {
      return f1.after(f2)
    }, start.before(before))
  }
  return start.before(before)
}


对了,回答之前有不少人问为啥 compose 是从右至左执行??


const compose = function(...args) {
  if (args.length) {
    return args.reverse().reduce(function(f1, f2) {
      return f1.after(f2)
    })
  }
}
compose(step4,step3,step2,step1,init)("start")


这里说一下:原因是它模拟了通常情况下函数逐层调用,层层包裹的顺序,像剥洋葱一样,从外而内,从右至左去解析:


step4(step3(step2(step1(init(...args))))) // 一层层括号像极了洋葱皮


如果你喜欢从左至右,或换 pop()shift() ,或去掉那层 reverse() 即可,或改 afterbefore......顺序问题,无关好坏,全凭喜好~


🎯传参问题!!



如果你有心在控制台试试以上代码,不难发现其中的一个很严重的传参问题!!这个问题在《compose 优化屎山》那篇文章实际上也存在,也有细心的掘友反馈。


function init(...args){
    console.log(args)
    return [...args,"init"]
}
function step1(...args){
    console.log(args)
    return [...args,"step1"]
}
function step2(...args){
    console.log(args)
    return [...args,"step2"]
}
function step3(...args){
    console.log(args)
    return [...args,"step3"]
}
compose(step3,step2,step1,init)("start")


随着参数的传递,args 数组的维度在不断上升。

image.png

如果我们使用 flat(Infinity) 拉平数组,传参就变成了这样:

image.png


这样做有一个很大的问题就是:需要对照数组的传参顺序!这是很头疼的,因为保不定哪天就要增删改流程参数。


所以,期望是能换成对象作传参,消除按顺序传参的桎梏。比如:

{start:"start",init:"init",step1:"step1"......}


直接动手试试:

function init(...args){
    console.log(JSON.stringify(args))
    return {args:args,init:"init"}
}
function step1(...args){
    console.log(JSON.stringify(args))
    return {args:args,step1:"step1"}
}
function step2(...args){
    console.log(JSON.stringify(args))
    return {args:args,step2:"step2"}
}
function step3(...args){
    console.log(JSON.stringify(args))
    return {args:args,step3:"step3"}
}
compose(step3,step2,step1,init)("start")


得到:

image.png


显然这不是我们想要的,我们得再不断打印寻找规律:

image.png


哇噢~

step3 中想获取 step1,就要 2 个 .args[0]

image.png


step2 中想获取 step1,只要 1 个 .args[0]

image.png


我们基本可以推出:想获得前 N 步的参数,只需带 N 个 .args[0]

于是乎,我们可以尝试写一个 getCountStepAttr() 函数,用于在某个函数步骤中,获得前第 N 步的入参,通过调用对象属性的方式!


来吧,展翅~

function getCountStepAttr(args,N){
    // 需要前第几(N)步的参数
    N = N -1
    let resObj = args[0]
    for(let i =0;i<N;i++){
        resObj = resObj.args[0]
    }
    return resObj
}


直接就可以测试使用了:

image.png


🖖完整代码



贴下完整代码,你可以拷贝在控制台玩一玩看看,本瓜相信你一定会有所收获!!

Function.prototype.after = function(fn) {
  const self = this
  return function(...args) {
    let result = self.apply(null, args)
    return fn.call(null,result)
  }
}
const compose = function(...args) {
  if (args.length) {
    return args.reverse().reduce(function(f1, f2) {
      return f1.after(f2)
    })
  }
}
const getCountStepAttr = function(args,N){
    // 获取前 N 步的入参;
    N = N -1
    let resObj = args[0]
    for(let i =0;i<N;i++){
        resObj = resObj.args[0]
    }
    return resObj
}
function init(...args){
    console.log("【在 init 中调用原始传参】:",getCountStepAttr(args,1))
    return {args:args,init1:"init1",init:"init"}
}
function step1(...args){
    return {args:args,step1:"step1"}
}
function step2(...args){
    return {args:args,step2:"param-step2",step2Add:"param-step2-add"}
}
function step3(...args){
    console.log("【在 step3 中调用 step2 的传参】:",getCountStepAttr(args,1).step2 , getCountStepAttr(args,1).step2Add)
    console.log("【在 step3 中调用 init 的传参】:",getCountStepAttr(args,3).init , getCountStepAttr(args,3).init1)
    console.log("【在 step3 中调用原始传参】:",getCountStepAttr(args,4))
    return {args:args,step3:"step3"}
}
compose(step3,step2,step1,init)("start")


🐵小结展望



本篇在讲什么?


其实还是那金光闪闪的五个大字:函数式编程

我们将过程中的命令式代码用一个个简单的纯函数进行封装,最后组合成各种丰富的功能。


你可以在这个过程中,或任意拆卸、或增添补充、或重构设计,真的不用太担心隐藏的逻辑错漏或耦合造成的复杂业务难梳理!


我们用函数的输入、输出表达映射关系,用函数名表达函数内的功能实现,用参数的传递表达业务逻辑,用封闭的作用域环境构造干净的代码~

当然,你或许还有很多好的想法,代码的干净之路 还有很长一段要走!高山仰止,景行行止,虽不能至,心向往之。再说,能不能“至”还真不一定呢!

都看到这里,不如点个赞吧 👍👍👍 撰文不易,多谢鼓励 👏👏👏


欢迎点赞、收藏、评论~


我是掘金安东尼,公众号同名,输出暴露输入,技术洞见生活,再会~


相关文章
|
人工智能 运维 架构师
技能革命3.0时代:云计算就业岗位有哪些?
本文探讨云计算就业市场的深层逻辑,从岗位体系、AI赋能及技术局限性应对策略三方面解析。云计算岗位涵盖基础设施、平台服务、数据智能与应用创新四层,需复合型能力;AI不仅提供技术工具,还推动教育范式变革,助力跨界融合;面对技术局限,分步验证与经验洞察双管齐下。未来就业将向技能多元化、自主性增强和社会价值再定义方向进化,强调个体能力生态的持续成长。
1505 33
|
存储 安全 测试技术
云计算应该掌握的八种用途
云计算通过互联网提供按需付费的云服务,广泛应用于软件开发、社交网络、大数据分析、数据备份、文件存储、灾难恢复、云端通信和业务流程管理等领域。它帮助企业降低硬件成本、提高开发效率、保障数据安全、增强业务连续性,并支持多样化应用场景,成为数字化转型的核心引擎。
2623 2
|
网络协议 网络架构
TCP/IP 协议体系结构四层分别是什么?
TCP/IP协议体系结构四层分别是:1、数据链路层;实现网卡接口的网络驱动程序,以处理数据在物理媒介上的传输。2、网络层;实现数据包的选路和转发。3、传输层;为两台主机上的应用程序提供端到端的通信。4、应用层;负责处理应用程序的逻辑。
|
网络安全 Windows
在Windows电脑上启动并配置SSH服务
在Windows电脑上启动并配置SSH服务
7931 0
|
存储 数据库 索引
B树和B+树的插入、删除图文详解
B树和B+树的插入、删除图文详解
1727 0
|
存储 安全 算法
Java 泛型概述
Java 泛型概述
288 0
|
SQL 负载均衡 前端开发
最佳实践—如何选择应用端链接池
背景 当应用程序连接PolarDB-X实例执行操作时,从PolarDB-X实例的角度看,会有如下两种类型的连接: 前端连接:由应用程序建立的,到PolarDB-X计算节点(CN)中逻辑库的连接。 后端连接:由PolarDB-X计算节点建立的,到后端数据节点(DN)中物理库的连接。
205 0
最佳实践—如何选择应用端链接池
|
NoSQL
Gdb调试多进程程序
Gdb调试多进程程序 程序经常使用fork/exec创建多进程程序。多进程程序有自己独立的地址空间,这是多进程调试首要注意的地方。Gdb功能强大,对调试多线程提供很多支持。 方法1:调试多进程最土的办法:attach pid Attach是调试进程的常用办法,只要有可执行程序以及相应PID,即可工作。
1675 0

热门文章

最新文章