Mixing functional and object-oriented programming in JavaScript

Object-oriented programming has been the dominant programming paradigm for several decades. It is taught in Computer Science 101 classes around the world, while functional programming is not. It is what software architects use to design applications, while functional programming is not. And it makes sense too: OOP makes it easy to conceptualize abstract ideas. It makes it easier to write code.

So, unless you can convince your boss that the application needs to be all functional, we're going to be using functional programming in an object-oriented world. This section will explore ways to do this.

Functional inheritance

Perhaps the most accessible way to apply functional programming to JavaScript applications is to use a mostly functional style within OOP principles, such as inheritance.

To explore how this might work, let's build a simple application that calculates the price of a product. First, we'll need some product classes:

var Shirt = function(size) {
  this.size = size;
};

var TShirt = function(size) {
  this.size = size;
};
TShirt.prototype = Object.create(Shirt.prototype);
TShirt.prototype.constructor = TShirt;
TShirt.prototype.getPrice = function(){
  if (this.size == 'small') {
    return 5;
  }
  else {
    return 10;
  }
}

var ExpensiveShirt = function(size) {
  this.size = size;
}
ExpensiveShirt.prototype = Object.create(Shirt.prototype);
ExpensiveShirt.prototype.constructor = ExpensiveShirt;
ExpensiveShirt.prototype.getPrice = function() {
  if (this.size == 'small') {
    return 20;
  }
  else {
    return 30;
  }
}

We can then organize them within a Store class as follows:

var Store = function(products) {
  this.products = products;
}
Store.prototype.calculateTotal = function(){
  return this.products.reduce(function(sum,product) {
    return sum + product.getPrice();
  }, 10) * TAX; // start with $10 markup, times global TAX var
};

var TAX = 1.08;
var p1 = new TShirt('small');
var p2 = new ExpensiveShirt('large');
var s = new Store([p1,p2]);
console.log(s.calculateTotal()); // Output: 35

The calculateTotal() method uses the array's reduce() function to cleanly sum together the prices of the products.

This works just fine, but what if we need a dynamic way to calculate the markup value? For this, we can turn to a concept called Strategy Pattern.

Strategy Pattern

Strategy Pattern is a method for defining a family of interchangeable algorithms. It is used by OOP programmers to manipulate behavior at runtime, but it is based on a few functional programming principles:

  • Separation of logic and data
  • Composition of functions
  • Functions as first-class objects

And a couple of OOP principles as well:

  • Encapsulation
  • Inheritance

In our example application for calculating product cost, explained previously, let's say we want to give preferential treatment to certain customers, and that the markup will have to be adjusted to reflect this.

So let's create some customer classes:

var Customer = function(){};
Customer.prototype.calculateTotal = function(products) {
  return products.reduce(function(total, product) {
    return total + product.getPrice();
  }, 10) * TAX;
};

var RepeatCustomer = function(){};
RepeatCustomer.prototype = Object.create(Customer.prototype);
RepeatCustomer.prototype.constructor = RepeatCustomer;
RepeatCustomer.prototype.calculateTotal = function(products) {
  return products.reduce(function(total, product) {
    return total + product.getPrice();
  }, 5) * TAX;
};

var TaxExemptCustomer = function(){};
TaxExemptCustomer.prototype = Object.create(Customer.prototype);
TaxExemptCustomer.prototype.constructor = TaxExemptCustomer;
TaxExemptCustomer.prototype.calculateTotal = function(products) {
  return products.reduce(function(total, product) {
    return total + product.getPrice();
  }, 10);
};

Each Customer class encapsulates the algorithm. Now we just need the Store class to call the Customer class's calculateTotal() method.

var Store = function(products) {
  this.products = products;
  this.customer = new Customer();
  // bonus exercise: use Maybes from Chapter 5 instead of a default customer instance
}
Store.prototype.setCustomer = function(customer) {
  this.customer = customer;
}
Store.prototype.getTotal = function(){
  return this.customer.calculateTotal(this.products);
};

var p1 = new TShirt('small');
var p2 = new ExpensiveShirt('large');
var s = new Store([p1,p2]);
var c = new TaxExemptCustomer();
s.setCustomer(c);
s.getTotal(); // Output: 45

The Customer classes do the calculating, the Product classes hold the data (the prices), and the Store class maintains the context. This achieves a very high level of cohesion and a very good mixture of object-oriented programming and functional programming. JavaScript's high level of expressiveness makes this possible and quite easy.

Mixins

In a nutshell, mixins are classes that can allow other classes to use their methods. The methods are intended to be used solely by other classes, and the mixin class itself is never to be instantiated. This helps to avoid inheritance ambiguity. And they're a great means of mixing functional programming with object-oriented programming.

Mixins are implemented differently in each language. Thanks to JavaScript's flexibility and expressiveness, mixins are implemented as objects with only methods. While they can be defined as function objects (that is, var mixin = function(){...};), it would be better for the structural discipline of the code to define them as object literals (that is, var mixin = {...};). This will help us to distinguish between classes and mixins. After all, mixins should be treated as processes, not objects.

Let's start with declaring some mixins. We'll extend our Store application from the previous section, using mixins to expand on the classes.

var small = {
  getPrice: function() {
    return this.basePrice + 6;   
  },
  getDimensions: function() {
    return [44,63]
  }
}
var large = {
  getPrice: function() {
    return this.basePrice + 10;   
  },
  getDimensions: function() {
    return [64,83]
  }
};

We're not limited to just this. Many more mixins can be added, like colors or fabric material. We'll have to rewrite our Shirt classes a little bit, as shown in the following code snippet:

var Shirt = function() {
  this.basePrice = 1;
};
Shirt.getPrice = function(){
  return this.basePrice;
}
var TShirt = function() {
  this.basePrice = 5;
};
TShirt.prototype = Object.create(Shirt.prototype);
TShirt..prototype.constructor = TShirt;

Now we're ready to use mixins.

Classical mixins

You're probably wondering just how these mixins get mixed with the classes. The classical way to do this is by copying the mixin's functions into the receiving object. This can be done with the following extension to the Shirt prototype:

Shirt.prototype.addMixin = function (mixin) {
  for (var prop in mixin) {
    if (mixin.hasOwnProperty(prop)) {
      this.prototype[prop] = mixin[prop];
    }
  }
};

And now the mixins can be added as follows:

TShirt.addMixin(small);
var p1 = new TShirt();
console.log( p1.getPrice() ); // Output: 11

TShirt.addMixin(large);
var p2 = new TShirt();
console.log( p2.getPrice() ); // Output: 15

However, there is a major problem. When the price of p1 is calculated again, it comes back as 15, the price of a large item. It should be the value for a small one!

console.log( p1.getPrice() ); // Output: 15

The problem is that the Shirt object's prototype.getPrice() method is getting rewritten every time a mixin is added to it; this is not very functional at all and not what we want.

Functional mixins

There's another way to use mixins, one that is more aligned with functional programming.

Instead of copying the methods of the mixin to the target object, we need to create a new object that is a clone of the target object with the mixin's methods added in. The object must be cloned first, and this is achieved by creating a new object that inherits from it. We'll call this variation plusMixin.

Shirt.prototype.plusMixin = function(mixin) {    
  // create a new object that inherits from the old
  var newObj = this;
  newObj.prototype = Object.create(this.prototype);
  for (var prop in mixin) {
    if (mixin.hasOwnProperty(prop)) {
      newObj.prototype[prop] = mixin[prop];
    }
  }
  return newObj;
};

var SmallTShirt = Tshirt.plusMixin(small); // creates a new class
var smallT = new SmallTShirt();
console.log( smallT.getPrice() );  // Output: 11

var LargeTShirt = Tshirt.plusMixin(large);
var largeT = new LargeTShirt();
console.log( largeT.getPrice() ); // Output: 15
console.log( smallT.getPrice() ); // Output: 11 (not effected by 2nd mixin call)

Here comes the fun part! Now we can get really functional with the mixins. We can create every possible combination of products and mixins.

// in the real world there would be way more products and mixins!
var productClasses = [ExpensiveShirt, Tshirt]; 
var mixins = [small, medium, large];

// mix them all together 
products = productClasses.reduce(function(previous, current) {
  var newProduct = mixins.map(function(mxn) {
    var mixedClass = current.plusMixin(mxn);
    var temp = new mixedClass();
    return temp;
  });
  return previous.concat(newProduct);
},[]);
products.forEach(function(o){console.log(o.getPrice())});

To make it more object-oriented, we can rewrite the Store object with this functionality. We'll also add a display function to the Store object, not the products, to keep the interface logic and the data separated.

// the store
var Store = function() {
  productClasses = [ExpensiveShirt, TShirt];
  productMixins = [small, medium, large];
  this.products = productClasses.reduce(function(previous, current) {
    var newObjs = productMixins.map(function(mxn) {
      var mixedClass = current.plusMixin(mxn);
      var temp = new mixedClass();
      return temp;
    });
    return previous.concat(newObjs);
  },[]);
}
Store.prototype.displayProducts = function(){
  this.products.forEach(function(p) {
    $('ul#products').append('<li>'+p.getTitle()+': $'+p.getPrice()+'</li>');
  });
}

And all we have to do is create a Store object and call its displayProducts() method to generate a list of products and prices!

<ul id="products">
  <li>small premium shirt: $16</li>
  <li>medium premium shirt: $18</li>
  <li>large premium shirt: $20</li>
  <li>small t-shirt: $11</li>
  <li>medium t-shirt: $13</li>
  <li>large t-shirt: $15</li>
</ul>

These lines need to be added to the product classes and mixins to get the preceding output to work:

Shirt.prototype.title = 'shirt';
TShirt.prototype.title = 't-shirt';
ExpensiveShirt.prototype.title = 'premium shirt';

// then the mixins got the extra 'getTitle' function:
var small = {
  ...
  getTitle: function() {
    return 'small ' + this.title; // small or medium or large
  }
}

And, just like that, we have an e-commerce application that is highly modular and extendable. New shirt styles can be added absurdly easily—just define a new Shirt subclass and add to it the Store class's array product classes. Mixins are added in just the same way. So now when our boss says, "Hey, we have a new type of shirt and a coat, each available in the standard colors, and we need them added to the website before you go home today", we can rest assured that we'll not be staying late!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset