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.
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 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:
And a couple of OOP principles as well:
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.
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.
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.
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!