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!