阅读 480

事实上,回调函数还不错!!

原文点击这里
在js世界里,我们众所周知的恶魔,或许没有那么可怕,我们是不是多了一些误解?

走进回调地狱

我不会对术语回调地狱挖的太深,仅仅只是通过这篇文章解释一些问题和典型的解决方案。如果你对这个术语还不太熟悉,可以先去看看其他的文章。我会一直在这等你回来!
Ok,我先复制粘贴一下问题代码,然后,让我们一起用回调函数来解决,而不是采用promise/async/await

const verifyUser = function(username, password, callback) {
  dataBase.verifyUser(username, password, (error, userInfo) => {
    if (error) {
      callback(error);
    } else {
      dataBase.getRoles(username, (error, roles) => {
        if (error) {
          callback(error);
        } else {
          dataBase.logAccess(username, error => {
            if (error) {
              callback(error);
            } else {
              callback(null, userInfo, roles);
            }
          });
        }
      });
    }
  });
};
复制代码

压垮金字塔

观察代码,你会发现,每次需要执行异步操作时,必须传递一个回调函数来接收异步的结果。 由于我们线性且匿名定义了所有的回调函数,致使它成为一个自下而上,层层危险叠加的回调函数金字塔(实际过程中,这种嵌套可能会更多,更深,更复杂)。
第一步,我们先简单重构一下代码:将每个匿名函数赋值给独立的变量。引入柯里化参数(curried aruguments)来绕过环境作用域中的变量。

const verifyUser = (username, password, callback) =>
  dataBase.verifyUser(username, password, f(username, callback));

const f = (username, callback) => (error, userInfo) => {
  if (error) {
    callback(error);
  } else {
    dataBase.getRoles(username, g(username, userInfo, callback));
  }
};

const g = (username, userInfo, callback) => (error, roles) => {
  if (error) {
    callback(error);
  } else {
    dataBase.logAccess(username, h(userInfo, roles, callback));
  }
};

const h = (userInfo, roles, callback) => (error, _) => {
  if (error) {
    callback(error);
  } else {
    callback(null, userInfo, roles);
  }
};
复制代码

如果没点其他东西的话,肯定有点吹捧的意思。但是这些代码仍然有以下的问题:

  1. if (error) { ... } else { ... }模式重复使用;
  2. 变量名字对逻辑毫无意义;
  3. verifyUserfgh相互高度耦合,因为他们互相引用。

看看这种模式

在我们处理任何这些问题之前,让我们注意这些表达式之间的一些相似之处:
所有这些函数都接受一些数据和callback参数。f,g并且h另外接受一对参数(error, something),其中只有一个将是一个非null/ undefined值。如果error不为null,该函数立即抛给callback并终止。否则,something会被执行来做更多的工作,最终导致callback接收到不同的错误,或者null和一些结果值。
脑海中记住这些共性,我们将开始重构中间表达式,使它们看起来越来越相似。

魔术化妆!!

我发现if语句很累赘,所以我们花点时间用三元表达式来代替。由于返回值被丢弃,以下代码不会有任何的行为。

const f = (username, callback) => (error, userInfo) =>
  error
    ? callback(error)
    : dataBase.getRoles(username, g(username, userInfo, callback));

const g = (username, userInfo, callback) => (error, roles) =>
  error
    ? callback(error)
    : dataBase.logAccess(username, h(userInfo, roles, callback));

const h = (userInfo, roles, callback) => (error, _) =>
  error ? callback(error) : callback(null, userInfo, roles);
复制代码

柯里化

因为我们即将开始用函数参数进行一些严肃的操作,所以我将借此机会尽可能的柯里化函数。
我们不能柯里化(error,xyz)参数,因为databeseAPI期望回调函数携带两个参数,但是我们可以柯里化其他参数。我们后面将围绕dataBaseAPI 使用以下柯里化包装器:

const dbVerifyUser = username => password => callback =>
  dataBase.verifyUser(username, password, callback);

const dbGetRoles = username => callback =>
  dataBase.getRoles(username, callback);

const dbLogAccess = username => callback =>
  dataBase.logAccess(username, callback);
复制代码

另外,我们替换callback(null, userInfo, roles)callback(null, { userInfo, roles }),以便于除了不可避免的error参数之外我们只处理一个参数即可。

const verifyUser = username => password => callback =>
  dbVerifyUser(username)(password)(f(username)(callback));

const f = username => callback => (error, userInfo) =>
  error
    ? callback(error)
    : dbGetRoles(username)(g(username)(userInfo)(callback));

const g = username => userInfo => callback => (error, roles) =>
  error ? callback(error) : dbLogAccess(username)(h(userInfo)(roles)(callback));

const h = userInfo => roles => callback => (error, _) =>
  error ? callback(error) : callback(null, { userInfo, roles });
复制代码

把它翻出来

让我们多做一些重构。我们将把所有错误检查代码“向外”拉出一个级别,代码就会暂时变得清晰。我们将使用一个接收当前步骤的错误或结果的匿名函数,而不是每个步骤都执行自己的错误检查,如果没有问题,则将结果和回调转发到下一步:

const verifyUser = username => password => callback =>
  dbVerifyUser(username)(password)((error, userInfo) =>
    error ? callback(error) : f(username)(callback)(userInfo)
  );

const f = username => callback => userInfo =>
  dbGetRoles(username)((error, roles) =>
    error ? callback(error) : g(username)(userInfo)(callback)(roles)
  );

const g = username => userInfo => callback => roles =>
  dbLogAccess(username)((error, _) =>
    error ? callback(error) : h(userInfo)(roles)(callback)
  );

const h = userInfo => roles => callback => callback(null, { userInfo, roles });
复制代码

注意错误处理如何完全从我们的最终函数中消失:h。它只接受几个参数然后立即将它们输入到它接收的回调中。
callback参数现在在各个位置传递,因此为了保持一致性,我们将移动参数,以便所有数据首先出现并且callback最后出现:

const verifyUser = username => password => callback =>
  dbVerifyUser(username)(password)((error, userInfo) =>
    error ? callback(error) : f(username)(userInfo)(callback)
  );

const f = username => userInfo => callback =>
  dbGetRoles(username)((error, roles) =>
    error ? callback(error) : g(username)(userInfo)(roles)(callback)
  );

const g = username => userInfo => roles => callback =>
  dbLogAccess(username)((error, _) =>
    error ? callback(error) : h(userInfo)(roles)(callback)
  );

const h = userInfo => roles => callback => callback(null, { userInfo, roles });
复制代码

逐渐形成的模式

到目前为止,您可能已经开始在混乱中看到一些模式。特别是callback通过计算进行错误检查和线程处理的代码非常重复,可以使用以下两个函数进行分解:

const after = task => next => callback =>
  task((error, v) => (error ? callback(error) : next(v)(callback)));

const succeed = v => callback => callback(null, v);
复制代码

我们的步骤变成:

const verifyUser = username => password =>
  after(dbVerifyUser(username)(password))(f(username));

const f = username => userInfo =>
  after(dbGetRoles(username))(g(username)(userInfo));

const g = username => userInfo => roles =>
  after(dbLogAccess(username))(_ => h(userInfo)(roles));

const h = userInfo => roles => succeed({ userInfo, roles });
复制代码

是时候停一下了,尝试将aftersuceed内联入这些新的表达式中。这些新表达确实等同于我们考虑的因素。
OK,看一下,fgh看起来已经没什么用了呢!

减负

······所以,让我们甩了它们!我们所要做的就是从h向后,将每个函数内联到引用它的定义中:

// 内联 h 到 g 中
const g = username => userInfo => roles =>
  after(dbLogAccess(username))(_ => succeed({ userInfo, roles }));
复制代码
// 内联 g 到 f
const f = username => userInfo =>
  after(dbGetRoles(username))(roles =>
    after(dbLogAccess(username))(_ => succeed({ userInfo, roles }))
  );
复制代码
// 内联 f 到 verifyUser
const verifyUser = username => password =>
  after(dbVerifyUser(username)(password))(userInfo =>
    after(dbGetRoles(username))(roles =>
      after(dbLogAccess(username))(_ => succeed({ userInfo, roles }))
    )
  );
复制代码

我们可以使用引用透明度来引入一些临时变量并使其更具可读性:

const verifyUser = username => password => {
  const userVerification = dbVerifyUser(username)(password);
  const rolesRetrieval = dbGetRoles(username);
  const logEntry = dbLogAccess(username);

  return after(userVerification)(userInfo =>
    after(rolesRetrieval)(roles =>
      after(logEntry)(_ => succeed({ userInfo, roles }))
    )
  );
};
复制代码

现在你已经得到了!它相当简洁,没有任何重复的错误检查,甚至和promise模式有点相似。你会像这样调用verifyUser:

const main = verifyUser("someusername")("somepassword");
main((e, o) => (e ? console.error(e) : console.log(o)));
复制代码

最终代码

// callback测序工具APIs
const after = task => next => callback =>
  task((error, v) => (error ? callback(error) : next(v)(callback)));

const succeed = v => callback => callback(null, v);

// 柯里化后的database Api
const dbVerifyUser = username => password => callback =>
  dataBase.verifyUser(username, password, callback);

const dbGetRoles = username => callback =>
  dataBase.getRoles(username, callback);

const dbLogAccess = username => callback =>
  dataBase.logAccess(username, callback);

// 成果
const verifyUser = username => password => {
  const userVerification = dbVerifyUser(username)(password);
  const rolesRetrieval = dbGetRoles(username);
  const logEntry = dbLogAccess(username);

  return after(userVerification)(userInfo =>
    after(rolesRetrieval)(roles =>
      after(logEntry)(_ => succeed({ userInfo, roles }))
    )
  );
};
复制代码

终极魔法

我们完成了吗?有些人可能仍然觉得verifyUser的定义有点过于三角化。有办法解决,但是首先我们做点其他的事。
我没有独立发现重构此代码时定义aftersucceed过程。我实际上预先定义了这些定义,因为我从Haskell库中复制了它们,它们的名称为>>=pure。这两个函数共同构成了"continuation monad"(译者注:可以理解为把嵌套式的金字塔结构打平变成链式结构能力的一种模式)的定义。
让我们以不同的方式格式化定义verifyUser

const verifyUser = username => password => {
  const userVerification = dbVerifyUser(username)(password);
  const rolesRetrieval = dbGetRoles(username);
  const logEntry = dbLogAccess(username);

  // prettier-ignore
  return after   (userVerification)    (userInfo =>
         after   (rolesRetrieval)      (roles    =>
         after   (logEntry)            (_        =>
         succeed ({ userInfo, roles }) )));
};
复制代码

更换succeedafter与那些奇怪的别名:

const M = { ">>=": after, pure: succeed };

const verifyUser = username => password => {
  const userVerification = dbVerifyUser(username)(password);
  const rolesRetrieval = dbGetRoles(username);
  const logEntry = dbLogAccess(username);

  return M[">>="] (userVerification)    (userInfo =>
         M[">>="] (rolesRetrieval)      (roles    =>
         M[">>="] (logEntry)            (_        =>
         M.pure   ({ userInfo, roles }) )));
};
复制代码

M是我们对"continuation monad"的定义,具有错误处理和不纯的副作用。这里省略了细节以防止文章变长两倍,但是这种相关性是有许多方便的方法来排序不受金字塔末日效应影响的单子计算("continuation monad")。没有进一步的解释,这里有几种表达方式verifyUser

const { mdo } = require("@masaeedu/do");

const verifyUser = username => password =>
  mdo(M)(({ userInfo, roles }) => [
    [userInfo, () => dbVerifyUser(username)(password)],
    [roles, () => dbGetRoles(username)],
    () => dbLogAccess(username),
    () => M.pure({ userInfo, roles })
  ]);
复制代码
//适用提升
const verifyUser = username => password =>
  M.lift(userInfo => roles => _ => ({ userInfo, roles }))([
    dbVerifyUser(username)(password),
    dbGetRoles(username),
    dbLogAccess(username)
  ]);
复制代码

我故意避免在这篇文章的大部分内容中引入类型签名或monad这样的概念,以使事情变得平易近人。也许在未来的帖子中,我们可以用我们头脑中最重要的monadmonad-transformer概念重新推导出这种抽象,并特别注意类型和规律。

致谢

非常感谢@jlavelle,@mvaldesdeleon和@gabejohnson提供有关此帖子的反馈和建议。

关注下面的标签,发现更多相似文章
评论