Skip to content

Instantly share code, notes, and snippets.

@jrf0110
Created November 12, 2014 17:13
Show Gist options
  • Save jrf0110/8dce646fc0ec856ca359 to your computer and use it in GitHub Desktop.
Save jrf0110/8dce646fc0ec856ca359 to your computer and use it in GitHub Desktop.

What would it look like if orders used strategies?

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment