2

我是JsonRestStores的作者。我已经把这个问题推迟了太久了。这是一个将赢得“年度最愚蠢的缩进函数”奖的函数。

主要的棘手问题是闭包变量发生了变化:

body[ self.idProperty ] = params[ self.idProperty ];

还有一个“如果”让事情变得有趣。

那么......有没有一种优雅的方法可以将此功能变成看起来不像带有两个戳的箭头的东西?如果是这样,您能否提供一个示例实现?

  _makePostAppend: function( params, body, options, next ){

    var self = this;
    var body;

    if( typeof( next ) !== 'function' ) next = function(){};

    // Check that the method is implemented
    if( ! self.handlePostAppend ){
      self._sendError( next, new self.NotImplementedError( ) );
      return;
    }

    // Check the IDs
    self._checkParamIds( params, body, false, function( err ){  
      self._sendErrorOnErr( err, next, function(){

        self.schema.validate(  body, function( err, body, errors ) {
          self._sendErrorOnErr( err, next, function(){

            if( errors.length ){
              self._sendError( next, new self.UnprocessableEntityError( { errors: errors } ) );
            } else {

              // Fetch the doc
              self.execAllDbFetch( params, body, options, function( err, fullDoc ){
                self._sendErrorOnErr( err, next, function(){


                  self.extrapolateDoc( params, body, options, fullDoc, function( err, doc) {
                    self._sendErrorOnErr( err, next, function(){

                      self._castDoc( doc, function( err, doc) {
                        self._sendErrorOnErr( err, next, function(){

                          // Actually check permissions
                          self.checkPermissionsPostAppend( params, body, options, doc, fullDoc, function( err, granted ){
                            self._sendErrorOnErr( err, next, function(){

                              if( ! granted ){
                                self._sendError( next, new self.ForbiddenError() );
                              } else {

                                // Clean up body from things that are not to be submitted
                                //if( self.schema ) self.schema.cleanup( body, 'doNotSave' );
                                self.schema.cleanup( body, 'doNotSave' );

                                // Paranoid check
                                // Make sure that the id property in the body does match
                                // the one passed as last parameter in the list of IDs
                                body[ self.idProperty ] = params[ self.idProperty ];

                                self.execPostDbAppend( params, body, options, doc, fullDoc, function( err, fullDocAfter ){
                                  self._sendErrorOnErr( err, next, function(){

                                    self.extrapolateDoc( params, body, options, fullDocAfter, function( err, doc) {
                                      self._sendErrorOnErr( err, next, function(){

                                        self._castDoc( fullDocAfter, function( err, docAfter) {
                                          self._sendErrorOnErr( err, next, function(){

                                            // Remote request: set headers, and send the doc back (if echo is on)
                                            if( self.remote ){
                                              if( self.echoAfterPostAppend ){

                                                 self.prepareBeforeSend( docAfter, function( err, docAfter ){
                                                   self._sendErrorOnErr( err, next, function(){

                                                      self.afterPostAppend( params, body, options, doc, fullDoc, docAfter, fullDocAfter, function( err ){
                                                        self._sendErrorOnErr( err, next, function(){

                                                          self._res.json( 200, docAfter );

                                                        });
                                                      });
                                                   })
                                                 })
                                              } else { 

                                                self.afterPostAppend( params, body, options, doc, fullDoc, docAfter, fullDocAfter, function( err ){
                                                  self._sendErrorOnErr( err, next, function(){

                                                    self._res.send( 204, '' );

                                                  });
                                                });

                                              }

                                            // Local request: simply return the doc to the asking function
                                            } else {

                                              self.prepareBeforeSend( docAfter, function( err, docAfter ){
                                                self._sendErrorOnErr( err, next, function(){

                                                  self.afterPostAppend( params, body, options, doc, fullDoc, docAfter, fullDocAfter, function( err ){
                                                    self._sendErrorOnErr( err, next, function(){

                                                      next( null, docAfter, self.idProperty );

                                                    })
                                                  })

                                                })
                                              })
                                            }

                                          })
                                        });

                                      });
                                    })


                                  }) // err
                                }) // execPostDbAppend

                              } // granted


                            })
                          }) 

                        }) 
                      }) 

                    })
                  }) 

                }) // err
              }) // checkPermissionsPostAppend

            } // errors.length

          }) // err
        }) // self.validate

      }) // err
    }) // self.validate
  },
4

4 回答 4

2

Promise 允许您几乎直接从同步代码编写异步代码,因为它们恢复异常冒泡和返回值组合。

假设您已经将其他方法重写为 Promise:

var Promise = require("bluebird");

...

_makePostAppend: function (params, body, options) {
  var fullDoc, doc, docAfter, fullDocAfter;
  // Check that the method is implemented
  if (!this.handlePostAppend) {
    return Promise.rejected(new this.NotImplementedError());
  }

  //Note that it is Promise#bind, not Function#bind
  return this._checkParamIds(param, body, false).bind(this).then(function () {
    return this.schema.validate(body);
  }).then(function () {
    return this.execAllDbFetch(params, body, options);
  }).then(function (_fullDoc) {
    fullDoc = _fullDoc;
    return this.extrapolateDoc(params, body, options, fullDoc);
  }).then(function (doc) {
    return this._castDoc(doc);
  }).then(function (_doc) {
    doc = _doc;
    return this.checkPermissionsPostAppend(params, body, options, doc, fullDoc);
  }).then(function (granted) {
    if (!granted) throw new this.ForbiddenError();

    this.schema.cleanup(body, 'doNotSave');
    body[this.idProperty] = params[this.idProperty];
    return this.execPostDbAppend(params, body, options, doc, fullDoc);
  }).then(function (_fullDocAfter) {
    fullDocAfter = _fullDocAfter;
    return this.extrapolateDoc(params, body, options, fullDocAfter);
  }).then(function (doc) {
    return this._castDoc(fullDoc);
  }).then(function (_docAfter) {
    docAfter = _docAfter;
    if (this.remote) {
      if (this.echoAfterPostAppend) {
        return this.prepareBeforeSend(docAfter).bind(this).then(function (_docAfter) {
          docAfter = _docAfter;
          return this.afterPostAppend(params, body, options, doc, fullDoc, docAfter, fullDocAfter);
        }).then(function () {
          return this._res.json(200, docAfter);
        });
      } else {
        return this.afterPostAppend(params, body, options, doc, fullDoc, docAfter, fullDocAfter).bind(this).then(function () {
          return this._res.send(204, '');
        });
      }
    } else {
      return this.prepareBeforeSend(docAfter).then(function (_docAfter) {
        docAfter = _docAfter;
        return this.afterPostAppend(params, body, options, doc, fullDoc, docAfter, fullDocAfter);
      });
    }
  });
}

请注意,您不再需要使用 2 个空格缩进来作弊,上面的内容使用 4 个空格缩进会更具可读性。也许这只是我。

用法是:

this._makePostAppend(params, body, options).bind(this).then(function() {

}).catch(this.UnprocessableEntityError, function(e) {

}).catch(this.NotImplementedError, function(e) {

}).catch(this.ForbiddenError, function(e) {

}).catch(function(e) {
  //Any other error
});
于 2013-11-09T15:06:09.677 回答
2

如果我正在编写像你这样的代码,我更喜欢使用异步库和它的瀑布函数,这样我就不必用它的承诺版本包装异步 API。这非常简单。承诺也很棒,@Esailija 的回答没有任何问题,但我个人认为这更容易实现并且同样可读:

var async = require('async');

var _makePostAppend = function (params, body, options, next) {
  var self = this, body;
  if (typeof(next) !== 'function') next = function () { };

  // Check that the method is implemented
  if (!self.handlePostAppend) {
    self._sendError(next, new self.NotImplementedError());
    return;
  }

  async.waterfall([
    function (cb) {
      // Check the IDs
      self.checkParamIds(params, body, false, cb);
    },
    function (cb) {
      self.schema.validate(body, cb);
    },
    function (body, errors, cb) {
      if (errors.length) cb(new self.UnprocessableEntityError({ errors: errors }));
      // Fetch the doc
      self.execAllDbFetch(params, body, options, cb);
    },
    function (fullDoc, cb) {
      self.extrapolateDoc(params, body, options, fullDoc, function (err, doc) {
        cb(err, fullDoc, doc);
      });
    },
    function (fullDoc, doc, cb) {
      self._castDoc(doc, function (err, doc) {
        cb(err, fullDoc, doc);
      });
    },
    function (fullDoc, doc, cb) {
      // Actually check permissions
      self.checkPermissionsPostAppend(params, body, options, doc, fullDoc, function (err, granted) {
        cb(err, fullDoc, doc, granted);
      });
    },
    function (fullDoc, doc, granted, cb) {
      if (!granted) cb(new self.ForbiddenError());

      // Clean up body from things that are not to be submitted
      //if( self.schema ) self.schema.cleanup( body, 'doNotSave' );
      self.schema.cleanup(body, 'doNotSave');

      // Paranoid check
      // Make sure that the id property in the body does match
      // the one passed as last parameter in the list of IDs
      body[self.idProperty] = params[self.idProperty];

      self.execPostDbAppend(params, body, options, doc, fullDoc, function (err, fullDocAfter) {
        cb(err, fullDoc, fullDocAfter);
      });
    },
    function (fullDoc, fullDocAfter, cb) {
      self.extrapolateDoc(params, body, options, fullDocAfter, function (err, doc) {
        cb(err, fullDoc, doc, fullDocAfter);
      });
    },
    function (fullDoc, doc, fullDocAfter, cb) {
      self._castDoc(fullDocAfter, function (err, docAfter) {
        cb(err, fullDoc, doc, fullDocAfter, docAfter);
      });
    }
  ], function (err, fullDoc, doc, fullDocAfter, docAfter) {
    self._sendErrorOnErr(err, next, function () {
      // Remote request: set headers, and send the doc back (if echo is on)
      if (self.remote) {
        if (self.echoAfterPostAppend) {
          async.waterfall([
            function (cb) {
              self.prepareBeforeSend(docAfter, cb);
            },
            function (docAfter, cb) {
              self.afterPostAppend(params, body, options, doc, fullDoc, docAfter, fullDocAfter, cb)
            }
          ], function (err, docAfter) {
            self._sendErrorOnErr(err, next, function () {
                self._res.json(200, docAfter);
            });
          });
        } else {
          self.afterPostAppend(params, body, options, doc, fullDoc, docAfter, fullDocAfter, function (err) {
            self._sendErrorOnErr(err, next, function () {
              self._res.send(204, '');
            });
          });
        }

        // Local request: simply return the doc to the asking function
      } else {
        async.waterfall([
          function (cb) {
            self.prepareBeforeSend(docAfter, function (err, docAfter) {
              cb(err, doc, fullDoc, fullDocAfter, docAfter);
            })
          },
          function (doc, fullDoc, fullDocAfter, docAfter, cb) {
            self.afterPostAppend(params, body, options, doc, fullDoc, docAfter, fullDocAfter, function (err) {
              cb(err, docAfter);
            });
          }
        ], function (err, docAfter) {
          self._sendErrorOnErr(err, next, function () {
            next(null, docAfter, self.idProperty);
          });
        });
      }
    });
  });
};

或者更好的是,我使用了@Esailija 回答中的作用域技巧:

var async = require('async');

var _makePostAppend = function (params, body, options, next) {
  var _self = this, _body, _fullDoc, _doc, _docAfter, _fullDocAfter;
  if (typeof(next) !== 'function') next = function () { };

  // Check that the method is implemented
  if (!_self.handlePostAppend) {
    _self._sendError(next, new _self.NotImplementedError());
    return;
  }

  async.waterfall([
    function (cb) {
      // Check the IDs
      _self.checkParamIds(params, _body, false, cb);
    },
    function (cb) {
      _self.schema.validate(_body, cb);
    },
    function (body, errors, cb) {
      if (errors.length) cb(new _self.UnprocessableEntityError({ errors: errors }));
      // Fetch the doc
      _self.execAllDbFetch(params, body, options, cb);
    },
    function (fullDoc, cb) {
      _fullDoc = fullDoc;
      _self.extrapolateDoc(params, _body, options, fullDoc, db);
    },
    function (doc, cb) {
      _self._castDoc(doc, cb);
    },
    function (doc, cb) {
      _doc = doc;
      // Actually check permissions
      _self.checkPermissionsPostAppend(params, _body, options, doc, _fullDoc, cb);
    },
    function (granted, cb) {
      if (!granted) cb(new _self.ForbiddenError());

      // Clean up body from things that are not to be submitted
      //if( self.schema ) self.schema.cleanup( body, 'doNotSave' );
      _self.schema.cleanup(_body, 'doNotSave');

      // Paranoid check
      // Make sure that the id property in the body does match
      // the one passed as last parameter in the list of IDs
      _body[_self.idProperty] = params[_self.idProperty];

      _self.execPostDbAppend(params, _body, options, _doc, _fullDoc, cb);
    },
    function (fullDocAfter, cb) {
      _fullDocAfter = fullDocAfter;
      _self.extrapolateDoc(params, _body, options, fullDocAfter, cb);
    },
    function (doc, cb) {
      _doc = doc;
      _self._castDoc(_fullDocAfter, cb);
    }
  ], function (err, docAfter) {
    _self._sendErrorOnErr(err, next, function () {
      // Remote request: set headers, and send the doc back (if echo is on)
      if (_self.remote) {
        if (_self.echoAfterPostAppend) {
          async.waterfall([
            function (cb) {
              _self.prepareBeforeSend(docAfter, cb);
            },
            function (docAfter, cb) {
              _self.afterPostAppend(params, _body, options, _doc, _fullDoc, docAfter, _fullDocAfter, cb)
            },
            function (cb) {
              _self._res.json(200, docAfter);
              cb();
            }
          ], function (err, results) {
            _self._sendErrorOnErr(err, next);
          });
        } else {
          _self.afterPostAppend(params, _body, options, _doc, _fullDoc, docAfter, _fullDocAfter, function (err) {
            _self._sendErrorOnErr(err, next, function () {
              _self._res.send(204, '');
            });
          });
        }

        // Local request: simply return the doc to the asking function
      } else {
        async.waterfall([
          function (cb) {
            _self.prepareBeforeSend(docAfter, cb);
          },
          function (docAfter, cb) {
            _docAfter = docAfter;
            _self.afterPostAppend(params, _body, options, _doc, _fullDoc, docAfter, _fullDocAfter, cb);
          }
        ], function (err) {
          _self._sendErrorOnErr(err, next, function () {
            next(null, _docAfter, _self.idProperty);
          });
        });
      }
    });
  });
};
于 2013-11-09T21:10:06.017 回答
1

回调地狱是一个可怕的地方:)

您可以使用Step.js 之类的东西,它使回调成为更具可读性的步骤序列。还有许多其他异步管理库,但我不确定这是否真的能拯救你……你仍然会有一团糟,只是缩进不会那么重。


我建议你停止程序化思考,开始思考你的数据模型,哪些对象有哪些方法,哪些方法负责哪些原子任务。

所以我会简单地以与任何过度肥胖的方法相同的方式重构:通过将相关代码的位抽象到它们自己的方法中。

self.checkIds(function() {
  self.fetchDoc(function() {
    self.checkPermissions({
      deny: self.denyPermission,
      allow: function() {
        // call method that handles the next thing
      }
    })
  })
});

现在你有了一个宏方法,它简单地调用发生实际工作的组件方法。这些方法中的每一个都可能在内部有一些回调,然后调用你给它的任何“全部完成”回调,将控制权传递回你的宏函数。

这具有额外的好处,即能够根据实际发生的情况阅读和理解大量步骤。它现在读起来像是一步一步的说明,而不是一堆巨大的低级噪音。

所以做十几个小得多的方法,做一小组非常简单的异步事情,完成后回调。并确保描述性地命名它们。然后将它们全部链接到一个更小且更易于维护的回调树中。

如果这个大方法可以在没有注释的情况下有意义,那么您将知道您的版本非常接近一个更好的版本。

祝你好运,你的工作已经完成了。


请注意,您也有一些常见的模式,所以您重复自己!

像这儿:

self.extrapolateDoc( params, body, options, fullDocAfter, function( err, doc) {
  self._sendErrorOnErr( err, next, function(){

和这里:

self.afterPostAppend( params, body, options, doc, fullDoc, docAfter, fullDocAfter, function( err ){
  self._sendErrorOnErr( err, next, function(){

您可以编写一个接受方法名称和参数列表的方法,并self._sendErrorOnErr()在第一个方法回调时自动推送内容。

找到更常见的模式,你可以进一步减少它。

于 2013-11-09T11:11:14.913 回答
-2

这是一个有点欺骗性的非答案,但希望至少有点启发性。我要做的第一件事是简化代码本身的同步版本。你从这样的事情开始:

_makePostAppend: function (params, body, options) {
  // ...and in the darkness, bind them (if needed)
  _.bindAll(this, 'checkParamIds', 'execAllDbFetch'); // ...etc.
  // Shortcuts. These would be unnecessary for private methods in a closure
  var
    checkParamIds = this.checkParamIds,
    schema = this.schema,
    execAllDbFetch = this.execAllDbFetch,
    execPostDbAppend = this.execPostDbAppend,
    extrapolateDoc = this.extrapolateDoc,
    checkPermissionsPostAppend = this.checkPermissionsPostAppend,
    _castDoc = this._castDoc,
    UnprocessableEntityError = this.UnprocessableEntityError,
    ForbiddenError = this.ForbiddenError,
    idProperty = this.idProperty,
    remote = this.remote,
    echoAfterPostAppend = this.echoAfterPostAppend,
    prepareBeforeSend = this.prepareBeforeSend,
    afterPostAppend = this.afterPostAppend,
    _res = this._res;

  checkParamIds(params, body, false);

  // This could throw the UnprocessableEntityError itself
  var errors = schema.validate(body);
  if (errors.length) {
    throw new UnprocessableEntityError({ errors: errors });
  }

  // Every method takes params+body+options. Wrap that in a single object? Alternatively:
  // var processor = this.getDocProcessor(params, body, options);
  // var fullDoc = processor.execAllDbFetch();
  // var doc = extrapolateDoc(fullDoc);
  // ...etc.
  var fullDoc = execAllDbFetch(params, body, options);
  var doc = extrapolateDoc(params, body, options, fullDoc);
  doc = _castDoc(doc);

  // This could throw the ForbiddenException itself
  var granted = checkPermissionsPostAppend(params, body, options, doc, fullDoc);
  if (!granted) { throw new ForbiddenError(); }

  schema.cleanup(body, 'doNotSave');
  body[idProperty] = params[idProperty];

  var fullDocAfter =  execPostDbAppend(params, body, options, doc, fullDoc);
  var docAfter =  extrapolateDoc(params, body, options, fullDocAfter);
  docAfter = _castDoc(docAfter);

  if (!remote || echoAfterPostAppend) {
    docAfter = prepareBeforeSend(docAfter);
  }
  afterPostAppend(params, body, options, doc, fullDoc, docAfter, fullDocAfter);
  return remote ?
    _res.json(200, echoAfterPostAppend ? docAfter : '') :
    docAfter;
}

现在,如果你有生成器,你可以简单地为异步方法加上 yield 前缀,然后将它传递给其他库函数(Q.star,我认为是其中之一)。您需要承诺异步方法,或者至少绑定它们的输入(即 func(in1, in2) 变为 apply(func, in1, in2)),但这样您最终会得到看起来非常相似的东西普通的旧同步代码。

但是,您可能没有生成器,因此也有一个不那么作弊的答案。不那么作弊意味着我从一个稍微简化的变体开始,如上面的注释中所述。也就是说,同步代码是这样的:

// Helper method. Could be private, defined elsewhere, etc.
var extrapolateCast = function (fullDoc) {
  var doc = extrapolateDoc(fullDoc);
  return _castDoc(doc);
};

_makePostAppend: function (params, body, options) {
  checkParamIds(params, body, false);
  schema.validate(body);
  var proc = getDocProcessor(params, body, options);

  var fullDoc = proc.execAllDbFetch();
  var doc = extrapolateCast(fullDoc);

  proc.checkPermissionsPostAppend(doc, fullDoc);

  schema.cleanup(body, 'doNotSave');
  body[idProperty] = params[idProperty];

  var fullDocAfter =  proc.execPostDbAppend(doc, fullDoc);
  var docAfter =  extrapolateCast(fullDocAfter);

  if (!remote || echoAfterPostAppend) {
    docAfter = prepareBeforeSend(docAfter);
  }
  proc.afterPostAppend(doc, fullDoc, docAfter, fullDocAfter);
  return remote ?
    _res.json(200, echoAfterPostAppend ? docAfter : '') :
    docAfter;
}

二十行左右,更易于管理。使用异步,使用上下文对象的一种解决方案可能如下所示。(是的,我知道这在很多方面都很糟糕,但我已经花了很多时间在这上面。)编辑:这实际上不起作用,上下文中的变量将立即解决,而不仅仅是在执行之前. 您可以将它们包装在一个函数中以使其延迟,或者实现类似于“保存”的“加载”函数。

_makePostAppend: function (params, body, options) {
  // Helper method. Could be private, defined elsewhere, etc.
  var extrapolateCast = pipeline(
    extrapolateDoc,
    _castDoc
  );

  var vars = {},
    contextObj = context(vars),
    save = contextObj.save,
    record = contextObj.record,
    proc = getDocProcessor(params, body, options);

  pipeline(
    apply(checkParamIds, params, body, false),
    apply(schema.validate, body),
    record('fullDoc', proc.execAllDbFetch),
    save('doc', extrapolateCast),
    apply(proc.checkPermissionsPostAppend, vars.doc, vars.fullDoc),
    apply(schema.cleanup, body, 'doNotSave'),
    lift(function () { body[idProperty] = params[idProperty]; }),
    record('fullDocAfter', apply(proc.execPostDbAppend, vars.doc, vars.fullDoc)),
    record('docAfter', extrapolateCast),
    conditional(
      function () { return !remote || echoAfterPostAppend; },
      prepareBeforeSend
    ),
    apply(proc.afterPostAppend, vars.doc, vars.fullDoc, vars.docAfter, vars.fullDocAfter)
  );

  // NOTE: Converting this part is left as an exercise for the reader. (read: I'm tired.)
  return remote ?
    _res.json(200, echoAfterPostAppend ? docAfter : '') :
    docAfter;
};

您在下面看到的辅助方法:

var 
context = function (store) {
  return {
    save: function (key, task) {
      return pipeline(
        task,
        tap(lift(function (result) {
          store[key] = result;
        }))
      );
    },
    // Same as save, but discards result
    record: function (key, task) {
      return pipeline(
        task,
        liftM(function (result) {
          store[key] = result;
          return [];
        })
      );
    }
  };
},
conditional = function (cond, task) {
  // NOTE: Too tired to figure this out now. Sorry!
},
createTask = function (impl) {
  return function () {
    var args = _.initial(arguments),
      callback = _.last(arguments);
    async.nextTick(function () {
      impl.call(null, args, callback);
    });
  };
},
liftM = function (fun) {
  return createTask(function (args, callback) {
    try {
      var results = fun.apply(this, args);
      return callback.apply(this, [null].concat(results));
    } catch (err) {
      return callback.call(this, err);
    }
  });
},
lift = function (fun) {
  return createTask(function (args, callback) {
    try {
      var result = fun.apply(this, args);
      return callback(null, result);
    } catch (err) {
      return callback(err);
    }
  });
},
tap = function (interceptor) {
  return createTask(function (args, callback) {
    return async.waterfall([
      _.partialArr(interceptor, args),
      liftM(function () {
        return args;
      })
    ], callback);
  });
};
于 2013-11-16T20:15:08.873 回答