/spritzr

Provides an inheritance/traits/talents library for js

Primary LanguageJavaScriptApache License 2.0Apache-2.0

Spritzr

Build Status

About spritzr

Spritzr is an inheritance/traits/talents library for node.js and the browser.

It aims to provide three things:

  • Extension: to do classical single inheritance
  • Traits: mixins at a class level and with state (a bit like multiple inheritance)
  • Talents: like traits, but applied to a single instance (and they can also be removed at will)

Spritzr was inspired by a great traits/talents library CocktailJS https://github.com/CocktailJS/cocktail.

Compatibility

Spritzr is automatically unit tested against the following browsers:

  • Internet Explorer 6, 7, 8, 9, 10 and 11
  • Firefox latest and 3.6 versions
  • Chrome latest version
  • Safari desktop latest version
  • Opera desktop latest version
  • Android 2.3 and 4.2
  • iOS 7

It is also tested on node.js version 0.10 and PhantomJS (headless webkit).


BrowserStack

Special thanks to BrowserStack for providing a free account to run our automated tests on all these different browsers and operating systems!


API

Inheritance

Spritzr has an extend function which implements a basic single inheritance model.

The prototype chain is maintained so that instanceof works as expected.

function Animal(type) {
	this.type = type;
};
Animal.prototype.type = null;

Animal.prototype.greet = function() {
	return "Hi, I'm a " + this.type + " called " + this.name;
};

// We need to explicitly call the super constructor using a handy $super property.
function Person(name) {
	this.$super("person");
	this.name = name;
};
Person.prototype.name = null;

Spritzr.extend(Person, Animal);

var steve = new Person("Steve");

expect(steve.greet()).toBe("Hi, I'm a person called Steve");
expect(steve instanceof Person).toBe(true);
expect(steve instanceof Animal).toBe(true);

// Spritzr also provides an isa() function which acts like instanceof
expect(Spritzr.isa(steve, Animal)).toBe(true);

// We can also override methods and properties of the super class
// but still access them using $super
var Tony = function Tony() {
	this.$super("Tony");
};
Spritzr.extend(Tony, Person);

Tony.prototype.greet = function() {
	var normalGreeting = this.$super.greet();
	
	return normalGreeting + ", y'all!";
};

var tony = new Tony();
expect(tony.greet()).toBe("Hi, I'm a person called Tony, y'all!");

Gotchas

Super constructor not explicitly called

When extending a class, the super class constructor will not be implicitly called when the sub class is instantiated. Therefor you should call the $super() constructor from within the subclass constructor; for example:

var Animal = function() {
	// Set up animaly stuff
};

var Mammal = function() {
	this.$super();	// Call the Animal() constructor
	// Set up mammalian stuff like live babies and stuff.
};

Spritzr.extend(Mammal, Animal);
Scope fudging when calling a super class method

When you call a super class method using this.$super.method() then the scope gets lost somewhere along the way, which means that this within the super method is not pointing to the correct object. There are two ways around this:

  1. Ensure you always call this.$super() from within the constructor. This has access to the real object and will cache it for later within the $super object. You can then just call this.$super.method() to call the super class method.
  2. Pass in othe correct scope when you call the method by using this.$super.method.call(this, arg1, arg2 ... ).

Traits

Classes can be extended with multiple traits, which are a bit like interfaces in Java but can also contain method implementations and properties.

var Animal = function() { };

var Mammal = function() { };
Spritzr.extend(Mammal, Animal); // Mammal is an extension of Animal

var Amphibian = function() { };
Spritzr.extend(Amphibian, Animal); // Amphibian is an extension of Animal

var Bird = function() { };
Spritzr.extend(Bird, Animal); // Bird is an extension of Animal

// We create a LaysEggs trait, which can be a class or a plain old object
var LaysEggs = function() { };
LaysEggs.prototype.layEgg = function() {
	return new Egg();
};

// And we can apply the trait to specific classes
Spritzr.spritz(Amphibian, LaysEggs); // All Amphibians can now lay eggs
Spritzr.spritz(Bird, LaysEggs); // All Birds can now lay eggs

// Then we can use isa() to work out if an object has a specific trait
var cat = new Mammal();
expect(Spritzr.isa(cat, LaysEggs)).toBe(false);

var frog = new Amphibian();
expect(Spritzr.isa(frog, LaysEggs)).toBe(true);

var parrot = new Bird();
expect(Spritzr.isa(parrot, LaysEggs)).toBe(true);

// Traits are also inherited through class extension
var Emu = function() { };
Spritzr.extend(Emu, Bird); // Emu extends Bird

var emu = new Emu();
expect(Spritzr.isa(emu, LaysEggs)).toBe(true);

// or from other traits
var Monotreme = function() { };
Spritzr.spritz(Monotreme, LaysEggs);

var Platypus = function() { };
Spritzr.extend(Platypus, Mammal); // A platypus is a mammal
Spritzr.spritz(Platypus, Monotreme); // But also a monotreme

var ducky = new Platypus();
expect(Spritzr.isa(ducky, LaysEggs)).toBe(true);

Talents

Talents are a bit like traits, but they are applied to instances rather than classes. They can also be removed at any point.

// We have a class describing people
var Person = function(firstName, lastName) {
	this.firstName = firstName;
	this.lastName = lastName;
};
Person.prototype.firstName = null;
Person.prototype.lastName = null;
Person.prototype.getDisplayName = function() {
	return this.firstName + ' ' + this.lastName;
};

var sharon = new Person("Sharon", "Ackerman");
var tony = new Person("Tony", "Jones");

// We can define a talent to describe friendship
var Friend = function(nickname) {
	this.nickname = nickname;
};
Friend.prototype.nickname = null;
Friend.prototype.getDisplayName = function() {
	return this.firstName + ' "' + this.nickname + '" ' + this.lastName;
};

// And add a new talent to our friends
Spritzr.spritz(sharon, new Friend('The Shazza'));

// We can then check for existence of the talent to find out if a person is our friend
expect(Spritzr.isa(sharon, Friend)).toBe(true);
expect(Spritzr.isa(tony, Friend)).toBe(false);

// And the methods should be overridden appropriately
expect(sharon.getDisplayName()).toBe('Sharon "The Shazza" Ackerman');
expect(tony.getDisplayName()).toBe('Tony Jones');

// We can also remove talents from an instance using the amazing titled unspritz function
Spritzr.unspritz(sharon, Friend);
expect(sharon.getDisplayName()).toBe('Sharon Ackerman');

// We don't need to instantiate the talent first either - the constructor will automatically be called
var HasAccount = function() {
	this.username = (this.firstName.substr(0,1) + this.lastName).toLowerCase();
};
HasAccount.prototype.username = null;

Spritzr.spritz(tony, HasAccount); // This adds the methods/properties and calls the constructor

expect(Spritzr.isa(tony, HasAccount)).toBe(true);
expect(tony.username).toBe("tjones");

Caveats

The library is reasonably well tested, but there are some flows which haven't been thought about much so far:

  • The effect of spritzing the same trait or talent twice into a class of object is untested and undefined.
  • New properties added to a trait after it has been spritzed into a class won't be reflected in the class (so if you add trait A into class B, then add a method to A it won't be reflected in B). You should set up your class hierarchy at the start of the application and avoid mutating it later.
  • Equally, methods added to a talent after it's been spritzed into an instance also won't be reflected in the instance.
  • There is no cycle checking in the isa() method, so if you have circular class dependencies then it's going to go into infinite recursion.
  • Extending a class twice is currently not prevented and will probably cause some wierd behaviour.
  • Probably a load of other stuff I haven't thought of!

The intention is to overcome these caveats in the future - it should be possible.