Right now, we've got a number of different fields to calculate into our total. We've sort of tangled them together in an unscalable way. Here's how we could define the behavior in a separated way.
models/order.js
module.exports = models.define({
totalStrategies: []
, attributes: {
total: {
get: function(){
// Run each strategy in waterfall, passing the order and the result
// of the previous strategy to the next
return this.totalStrategies.reduce( function( current, strategy ){
return strategy( this, current );
}.bind( this ), 0 );
}
}
}
});
// This part could be separated into multiple files maybe in a
// directory called `order-total-strategies`?
var strategies = module.exports.prototype.totalStrategies
// Sub-total
strategies.push( function( order, value ){
if ( !Array.isArray( order.attributes.items ) ){
throw new Error('Invalid value for attribute `items`');
}
// Assuming items have the `total` field set -
// If we were really going after the canonical source, we could
// figure it out
return order.attributes.items.reduce( function( current, item ){
return current + item.total;
}, 0 );
});
// Amenities
strategies.push( function( order, value ){
if ( !Array.isArray( order.attributes.amenities ) ){
throw new Error('Invalid value for attribute `amenities`');
}
if ( !Number.isInteger( order.attributes.guests ) ){
throw new Error('Invalid value for attribute `guests`');
}
return order.attributes.amenities.reduce( function( current, amenity ){
if ( amenity.scale === 'multiply' ){
return current + ( amenity.price * order.guests )
}
return current + item.total;
}, 0 );
});
// Adjustment
strategies.push( function( order, value ){
return value + order.attributes.adjustment;
});
// And so on
We could generalize further with a new value type called a Plan
. Plans are just a special type of value that, when requested, will execute an array of functions called Strategies
. A plan sub-type (like Waterfall
) describe how those Strategies
are executed.
Plan and Waterfall
var Plan = function(){
this.strategies = [];
return this;
};
Waterfall.prototype.add = function( strategy ){
this.strategies.push( strategy );
return this;
};
// Initial passed as initial value to the waterfall
// Rest args are passed to strategy
var Waterfall = function( initial ){
this.initial = initial;
this.rest = Array.prototype.slice.call( arguments, 1 );
return this;
};
Waterfall.prototype = new Plan();
Waterfall.prototype.valueOf = function(){
return this.strategies.reduce( function( current, strategy ){
return strategy.apply( null, this.rest.concat( current ) );
}.bind( this ), this.initial );
};
models/order.js
module.exports = models.define({
initialize: function(){
this.setupTotal();
}
, setupTotal: function(){
var total = this.attributes.total = new Plans.Waterfall( 0, this );
total.add( require('./order-total-strategies/sub-total') );
total.add( require('./order-total-strategies/amenities') );
total.add( require('./order-total-strategies/adjustment') );
total.add( require('./order-total-strategies/delivery-fee') );
total.add( require('./order-total-strategies/sales-tax') );
total.add( require('./order-total-strategies/tip') );
}
});
somewhere.js
var order = require('models/order').create({
/* assuming we had all of the necessary data */
});
order.attributes.total // => 1247 ... or something