前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何用函数式编程思想优化业务代码,这就给你安排上!

如何用函数式编程思想优化业务代码,这就给你安排上!

作者头像
腾讯云开发者
发布2021-08-19 10:11:21
2900
发布2021-08-19 10:11:21
举报
文章被收录于专栏:【腾讯云开发者】

导语 | 本文将介绍函数式编程中的几个核心概念,以及使用相关的函数式编程来优化业务代码的实践方案。

一、前言

日常开发中经常会遇到流程分支多、流程长的业务逻辑,如果排期较为紧张的话通常会选择if elseswitch case一把梭。然而随着迭代的推进,会有越来越多的新增流程分支或者需求变更,长此以往下去大多就成了 “祖传代码”。

随着EPC的落地,对代码中函数圈复杂度提出了要求,许多同学为了规避代码检查选择拆分函数,一行代码分成三个函数写,或者把原来的逻辑分支改成用映射匹配,这样看来虽然圈复杂度确实降低了,但是对代码的可维护性实际上是产生了损耗的。由于我最近做的需求大多也是这样的场景,于是开始尝试找寻一种模式来解决这个问题。

下图为流程图示例,实际业务中的情况远比下图要复杂:

二、核心概念

(一)compose

compose是函数式编程中使用较多的一种写法,它把逻辑解耦在各个函数中,通过compose的方式组合函数,将外部数据依次通过各个函数的加工,生成结果。在此处我们不对函数式编程进行展开,感兴趣的同学可以学习函数式编程指北

(参考网址:https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/)

下方代码示例是当我们不使用compose希望组合使用多个函数时最简单的调用方式。这里我们只有3个函数,看起来还比较直观,那么如果当我们有20个函数时呢?

代码语言:javascript
复制
const funcA = (message) => message + " A";const funcB = (message) => message + " B";const funcC = (message) => message + " C";const ret = funcC(funcB(funcA("Compose Example")));
console.log(ret); // Compose Example A B C

如下便是compose最基础的实现,尽管大部分对于compose的定义,以及其他一些fp工具库(比如ramda、lodash-fp)对compose的定义和实现都是从右向左,但是我们这里选择右倾实现,如果你希望保持左倾的话,可以将下方函数中的reduce替换为reduceRight。

代码语言:javascript
复制
const compose = (...funcs) => {  if (funcs.length === 0) {    return (args) => args;  }  if (funcs.length === 1) {    return funcs[0];  }  // 如果要使用左倾实现,可以将 reduce 替换为 reduceRight  return funcs.reduce((a, b) => (...args) => b(a(...args)));};

使用compose组合函数后看看如何使用:

代码语言:javascript
复制
const fn = compose(funcA, funcB, funcC);const ret = fn("Compose Example");console.log(ret); // Compose Example A B C

相比于环环相扣的嵌套调用,使用compose将多个函数组合生成为单个函数调用,使我们的代码无论从可读性还是可扩展性上都得到了提升。

(二)异步 compose

实际的应用场景我们不可能一个流程内全部为同步代码,可能会需要调用接口获得数据后再进入下一个流程,也可能会需要调用jsApi和客户端进行通信展示相应的交互。

如果要将compose改造为支持异步调用也非常简单,只需修改一行代码即可。可以选择用Promise进行扩展,这里我们为了保持同步的代码风格,选择使用async/await进行扩展,使用这种方式的话记得使用try catch兜底错误。

代码语言:javascript
复制
const asyncCompose = (...funcs) => {  if (funcs.length === 0) {    return (args) => args;  }  if (funcs.length === 1) {    return funcs[0];  }  // 只需要修改这一行即可  return funcs.reduce((a, b) => async (...args) => b(await a(...args)));};

改造一下我们的测试代码,看看效果:

代码语言:javascript
复制
// 支持异步函数的调用const funcA = (message) => new Promise((resolve, reject) => {  setTimeout(() => resolve(message + " A"), 1000);});const funcB = (message) => Promise.resolve(message + " B");// 依然支持同步函数的调用const funcC = (message) => message + " C";
const fn = compose(funcA, funcB, funcC);
(async() => {  const ret = await fn("Compose Example");  console.log(ret); // Compose Example A B C})();

三、实践方案

(一)koa-compose

在上面我们解决了异步函数的组合调用,在实际应用的场景中会发现,业务流程(funcs)有时候并不需要全部执行完毕,当接口的返回值非0,或者用户没有权限进入下一个流程时,我们需要提前结束流程的执行,只有当用户满足条件时才可以进入下一个流程。

这里首先想到的设计方式即是koa的中间件模型,koa最核心的功能就是它的中间件机制,中间件通过app.use注册,运行的时候从最外层开始执行,遇到next后加入下一个中间件,执行完毕后回到上一个中间件,这就是大家耳熟能详的洋葱模型。

koa大家基本都用过,基于middleware的设计模式也都非常熟悉了,同koa middleware保持相近的模式可以减少理解成本和心智负担。但是我们并不需要app.use的注册机制,因为在代码中不同的场景我们可能会需要组合不同的中间件,相比注册机制,我更倾向于用哪些中间件则传入哪些。

koa中间件引擎源码:

代码语言:javascript
复制
function compose (middleware) {  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')  for (const fn of middleware) {    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')  }  return function (context, next) {    // last called middleware #    let index = -1    return dispatch(0)    function dispatch (i) {      if (i <= index) return Promise.reject(new Error('next() called multiple times'))      index = i      let fn = middleware[i]      if (i === middleware.length) fn = next      if (!fn) return Promise.resolve()      try {        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));      } catch (err) {        return Promise.reject(err)      }    }  }}

koa已经将上方的中间件引擎提取为单独的koa-compose,我们可以直接从npm安装。

代码语言:javascript
复制
$ npm install koa-compose# 或者$ yarn add koa-compose

使用方式:

代码语言:javascript
复制
import compose from 'koa-compose';import middlewares from './middleware';import { Context, ContextStatus } from '@/types/libs/auth.d';
const run = async (...middlewares) => {  const context: Context = {    status: ContextStatus.pending,    data: {},  };  try {    const composition = compose(middlewares);    await composition(context);  } catch (e) {    console.error(e);    context.status = ContextStatus.rejected;  }  return context;};
export * from './middleware';export default run;

(二)middleware(中间件设计)

最简单的例子:

中间件的设计我们也可以参考koa middleware来设计,下方为一个最简单的示范,检查用户是否登录,如果登录则继续执行下一个中间件,如果未登录的话则拉起jsApi的登录框。

代码语言:javascript
复制
export const checkIsLogin = async (ctx, next) => {  console.log('checkIsLogin start');  ctx.data.userInfo = await getUserInfo();  if (!ctx.data.userInfo.uid) {    ctx.data.userInfo = await jsApi.login();  }  if (!ctx.data.userInfo.uid) {    return;  }  await next();  console.log('checkIsLogin end');};
支持传参的中间件:
代码语言:javascript
复制
export const checkIsLogin = (options) => async (ctx, next) => {  // TODO Something  console.log(options);  await next();  // TODO Something};
如何判断中间件是否全部执行成功或者提前结束?

我们需要在ctx.status上记录全部流程执行完毕的状态,以便做最后的处理,这里参考Promise的实现,选择用pending、fulfilled、rejected 来表示。

代码语言:javascript
复制
export enum ContextStatus {  pending = 'pending',  fulfilled = 'fulfilled',  rejected = 'rejected',}

如果在每个中间件内都需要手动设置ctx.status成功或者失败,则会产生很多重复代码,为了我们的代码简洁,需要增加一个机制,可以自动检查所有的中间件是否全部都正确的执行完毕,然后将结束状态设置为成功,可以自动检查是否有中间件提前结束,将结束状态设置为失败。我们需要新增2个通用中间件如下,分别置于全部中间件的开头和结尾处。

1.检查是否所有的中间件都从前到后执行完毕:

代码语言:javascript
复制
import { ContextStatus } from '@/types/libs/auth.d';
const checkIsEveryDone = async (ctx) => {  console.log('checkIsEveryDone start');  if (ctx.status === ContextStatus.pending) {    ctx.status = ContextStatus.fulfilled;  }  console.log('checkIsEveryDone start');};
export default checkIsEveryDone;

2.检查是否有中间件没有执行下去,提前结束:

代码语言:javascript
复制
import { ContextStatus } from '@/types/libs/auth.d';
const checkIsEarlyTurn = async (ctx, next) => {  console.log('checkIsEarlyTurn start');  await next();  if (ctx.status !== ContextStatus.fulfilled) {    ctx.status = ContextStatus.rejected;  }  console.log('checkIsEarlyTurn end');};
export default checkIsEarlyTurn;

 作者简介

王宏宇

腾讯新闻前端工程师

腾讯新闻前端工程师,目前于腾讯新闻从事相关 Web 开发工作。致力于开发体验提升,在代码优化有较为丰富的经验。

推荐阅读

拒绝代码臃肿,这套计算引擎设计方法值得一看!

保姆级教程: c++游戏服务器嵌入v8 js引擎

程序员如何把你关注的内容推送到你眼前?揭秘信息流推荐背后的系统设计


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯云开发者 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • (二)异步 compose
  • (二)middleware(中间件设计)
    • 最简单的例子:
      • 支持传参的中间件:
        • 如何判断中间件是否全部执行成功或者提前结束?
        相关产品与服务
        消息队列 TDMQ
        消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档