扁平化_非常_嵌套密集型函数:如何

Flattening a _very_ nesting-intensive function: how to?

本文关键字:如何 函数 嵌套 非常 扁平化 密集型      更新时间:2023-09-26

我是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
  },

如果我正在编写像您这样的代码,我更喜欢使用异步库及其瀑布函数,这样我就不必用它的承诺版本包装异步 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);
          });
        });
      }
    });
  });
};

承诺允许您直接从同步代码编写异步代码,因为它们会恢复异常冒泡并返回值组合。

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

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
});

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

您可以使用类似 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()

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

这个有点

作弊的不回答,但希望至少有一点启发性。我要做的第一件事是简化代码本身的同步版本。 你从这样的东西开始:

_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);
  });
};