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.