Difference between revisions of "Factory Tutorial"

From Spherical
Jump to: navigation, search
(Created a page on JS Factories. This is some real good stuff.)
 
m (Added authorship)
 
Line 2: Line 2:
  
 
What is a Factory? Why use Factories? How hard are they to make or use? All of these questions will be explained and put to use in this JavaScript programming tutorial. Recommended proficiency: intermediate to advanced.
 
What is a Factory? Why use Factories? How hard are they to make or use? All of these questions will be explained and put to use in this JavaScript programming tutorial. Recommended proficiency: intermediate to advanced.
 +
 +
--[[User:Radnen|Radnen]] ([[User talk:Radnen|talk]]) 01:25, 3 March 2014 (UTC)
  
 
== Factories ==
 
== Factories ==

Latest revision as of 01:25, 3 March 2014


What is a Factory? Why use Factories? How hard are they to make or use? All of these questions will be explained and put to use in this JavaScript programming tutorial. Recommended proficiency: intermediate to advanced.

--Radnen (talk) 01:25, 3 March 2014 (UTC)

Factories

A Factory is a design pattern in JavaScript aimed to aid in the construction of objects. Why is this useful? Sometimes creating your own objects can be a pain; especially when you have hundreds of different objects. In games this is commonly seen in item lists or enemy lists. The trouble is if you create an enemy in the 'old way' you'll get bad behavior.

The Old Way

function Enemy(name, hp, atk) {
    this.name = name;
    this.hp   = hp;
    this.atk  = atk;
}


var enemies = [];
enemies[0] = new Enemy("Bug", 10, 2);
enemies[1] = new Enemy("Rodent", 15, 5);
enemies[2] = new Enemy("Wolf", 25, 8);

// later on:

enemies[0].hp -= 5;

Then if you use enemy[0] in combat you can actually affect the base object rather than a clone of it. So most people, if they want enemies in their game they end up cloning them like so:

function CloneEnemy(enemy) {
    return new Enemy(enemy.name, enemy.hp, enemy.atk);
}

var enemy = CloneEnemy(enemies[0]);
enemy.hp -= 5;

That is a very nice solution, but it requires you to run a clone method each time, and in the case above a new clone method must be made for each object you want to have a copy of. Of course the next step would then to write a general clone method, and I'm sure you have one or heard of it before in JS. This is not how the Factory paradigm works, however. So, we won't go into object cloning.

The New Way

A Factory streamlines the process of constructing new objects. But in order to harness the full power we need to change the way in which we create objects. The enemy class from the old way now looks like this:

function Enemy(params) {
    this.name = params.name || "none";
    this.hp   = params.hp   || 0;
    this.atk  = params.atk  || 0;
}

The params are important since we can use new params objects to define new enemies rather than put then all into an array. So each time you create an enemy you can do this:

var bug = { name: "bug", hp: 10, atk: 2 };

var enemy = new Enemy(bug);
enemy.hp -= 5;

Although this is a slightly better method that reduces the need to use a cloning method, you still have to type up what object to build and use 'new' keyword. It's not bad, in fact if we stop here you'll already benefit from a good JS design practice. But, we can go one step further. We can create the actual Factory. Yes, up until now all we did is create the inventory for the Factory. Let's make the factory.

The Factory Code

A factory is good since it holds info on how to make the object and then makes the object, anew, whenever you ask it to. This is better than cloning entities since it physically calls constructors, and so the performance is not only slightly better it uses native JavaScript techniques to produce the objects without looking like you are doing much at all. It can reduce some boilerplate code.

function Factory(proto) {
    this.params = { };
    this.proto  = proto;
}

Factory.prototype.register = function(name, params) {
    this.params[name] = params;
}

Factory.prototype.create = function(name) {
    var object = params[name];
    return object && new this.proto(object);
}

Tip: The && operator in JS can be used to short-circuit return statements. In the example above if 'object' is undefined it won't return new this.proto(object) but undefined instead. This is a very nifty way of doing it.


So, how do you use it? Let's build off of the new way for Enemy construction. Remember how I made the bug parameters object? Let's do that with a factory instead, and then use the factory to create a new bug enemy.

function Enemy(params) {
    this.name = params.name || "none";
    this.hp   = params.hp   || 0;
    this.atk  = params.atk  || 0;
}

var Enemies = new Factory(Enemy);
Enemies.register("bug", { name: "Bug", hp: 10, atk: 2 }); // like doing bug = { ... }

var enemy = Enemies.create("bug");
enemy.hp -= 5;

See? All we do is tell it what new kind of enemy to make and then it does all of the rest internally. This is great since it reduces the level of knowledge the programmer has going into creating an enemy. All the programmer has to do is call the factories create method and out pops a brand new bug. What's better is the bug is an instanceof Enemy, which is nice to have. It's a perfect clone! We can then go on to define other enemies like the wolf or rodent very easily.

Advanced: Adding Inheritance

When working with items, you might have various inherited classes. Here is an inheritance based Factory.

function Factory(proto) {
    this.bases = { };
    this.proto = proto;
}

Factory.prototype.register = function(name, defaults, superclass) {
    this.bases[name] = { params: defaults, superclass: superclass};
}

Factory.prototype.create = function(type) {
    var data = this.bases[type];
    if (data && data.superclass) {
        var obj = new data.superclass(data.params);
        this.proto.call(obj, data.params);
        obj.prototype = new this.proto({});
        return obj;
    }
    else return data && new this.proto(data.params);
}

The main change is the ability to store a third argument, the superclass. Then in the create method if the super class is not present it'll use the base class to create the item.

Here's what this looks like, elegantly:

function Item(params) {
    this.name   = params.name   || "untitled";
    this.price  = params.price  || 0;
    this.weight = params.weight || 0;
}

function Weapon(params) {
    this.atk = params.atk || 0;
}

var Items = new Factory(Item);

// register a basic item:
Items.register("book", { name: "Book", price: 5, weight: 2 });

// register a weapon item, that inherits from Item:
Items.register("sword", { name: "Sword", price: 50, weight: 10, atk: 5 }, Weapon); // notice new 'atk' parameter

// creating new items:
var book = Items.create("book");
var sword = Items.create("sword");

You'll find that 'book' is an instanceof Item and 'sword' is an instanceof Weapon and Item. This is great for managing items from different classes.

Intermediate: A General Factory

What if you want a factory for any kind of item? Well we can create a general factory that is easier to use than the above factory method.

var Create = (function() {
	var types = { };
	
	function Create(type) {
		var object = types[type];
		return object && new object.base(object.params);
	}
	
	Create.register = function(name, construct, defaults) {
		types[name] = { base: construct, params: defaults };
	}
	
	return Create;
})();

Tip: This is achieved with a closure. Notice 'types' is a global object? Well, only from the perspective of Create. A closure contains everything in it's own little world. This gives it a sandbox to play in and reveals only the Create function by returning just it.

Create is a global object that has some cool properties. Let's make any item we desire!

Create.register("bug", Enemy, { name: "Bug", hp: 10, atk: 2 });
Create.register("book", Item, { name: "Book", price: 5, weight: 2 });

// create some items in a simpler syntax:
var book = Create("book");
var bug = Create("bug");

The sky's the limit here. However you'll run out of names if you don't stick to a naming convention. This method also puts item inheritance into your own hands rather than the hands of the factory. This is by far the most flexible factory method, however.

Bonus: Inheritance

What if you don't want the factory to do the inheritance for you? No sweat, you can do it yourself like this:

function Item(params) {
    this.name   = params.name   || "untitled";
    this.price  = params.price  || 0;
    this.weight = params.weight || 0;
}

function Weapon(params) {
    Item.call(this, params);
    this.atk = params.atk || 0;
}

Weapon.prototype = new Item({});

Create.register("book", Item, { name: "Book", weight: 2, price: 5 });
Create.register("sword", Weapon, { name: "Sword", weight: 10, price: 50, atk: 5 });

// create the items:
var book = Create("book"); // instanceof Item
var sword = Create("sword"); // instance of Weapon and Item

You end up writing a bit more inheritance code, but at least you can safely use the newer global Create Factory.