我是JsonRestStores的作者。我现在推迟这个问题已经有一段时间了。这是一个能赢得“年度最愚蠢缩进功能”奖项的功能。
主要的棘手问题是关键变量有一个改变点:
body[ self.idProperty ] = params[ self.idProperty ];
还有一个“if”让事情变得有趣。
所以...是否有一种优雅的方式将这个功能变成看起来不像带有两个戳的箭头的东西?如果是这样,您能提供示例实现吗?
_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
},
答案 0 :(得分:2)
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
});
答案 1 :(得分:2)
如果我正在编写与您类似的代码,我宁愿使用async库及其waterfall函数,这样我就不必使用它的promise版本来包装异步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);
});
});
}
});
});
};
答案 2 :(得分: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()
推送内容。
找到更常见的模式,你可以进一步削减它。
答案 3 :(得分:-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;
}
现在,如果你有生成器,你可以简单地在async方法前加上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);
});
};