Based on Practical Design Patterns in Javascript.
These patterns deal with the creation of new instances of an object
These patterns deal with the makeup of the actual objects themselves. They are concerned with how objects are made up and simplify relationships between objects.
These patterns deal more with how objects relate to each other and how they operate, as opposed to the structure or getting a new instance of something. They are concerned with the assignment of responsibilities between objects and how they communicate.
When you use the new keyword in front of any function, it creates a constructor function out of that function. That means it
- Creates a brand new object
- Links to an object prototype
- Binds
thisto the new object scope - Implicitly returns
this
// ES5
function Band(name) {
this.name = name;
this.play = function() {
console.log(this.name + ' starts to play');
};
}Now when calling the function by using Band('Metallica'), the this inside of Band refers to the window scope, which we don't want. To create an own this, use it like:
var band1 = new Band('Metallica');
band1.play();To improve this example even more, create the play method by using prototype. Currently, play gets reinitialized at every instance of Band, which is unnecessary. When you use prototype, the method is created once, and is used by every instance of Band:
// ES5
function Band(name) {
this.name = name;
}
Band.prototype.play = function() {
console.log(this.name + ' starts to play');
};In EcmaScript2015 we can use actual constructors like so:
// ES2015
class Band {
constructor(name) {
this.name = name;
}
play() {
console.log(this.name + ' starts to play');
}
}
var band1 = new Band('Metallica');
band1.play();You could think of a module like a toolbox. It's just a simple way to take a bunch of methods that are similar, and put the in a toolbox of functions for everyone to use.
- Simple way to encapsulate methods
- Creates a "toolbox" of functions to use
The difference between the constructor pattern and the module patterns, is that most of the time you only need 1 instance of the module. For example, you have a service which does all the database work. In Javascript, the way you write it is almost the same as the constructor pattern, but you use it in a different way.
// ES5
function BandService() {
return {
all: function() {
return [
{name: 'Metallica', formed: 1981 },
{name: 'Foo Fighters', formed: 1994 }
];
},
find: function(bandName) {
return this.all().find(function(band) { return band.name === bandName; });
}
}
}
// ES2015
class BandService {
all() {
return [
{name: 'Metallica', formed: 1981 },
{name: 'Foo Fighters', formed: 1994 }
];
}
find(bandName) {
return this.all().find((band) => band.name === bandName);
}
}
var bandService = new BandService();
console.log(bandService.all());
console.log(bandService.find('Metallica'));This is a pattern that is used to create objects for us.
- Simplifies object creation
- Creating different objects based on need
- Repository creation
// ES5
function Factory() {
return {
createEmployee: function(name, type) {
var employee;
switch(type) {
case 'fulltime':
employee = new FullTime();
break;
case 'parttime':
employee = new PartTime();
break;
case 'temporary':
employee = new Temporary();
break;
}
employee.name = name;
return employee;
}
}
}
function FullTime() { this.payPerHour = 20; }
function PartTime() { this.payPerHour = 15; }
function Temporary() { this.payPerHour = 10; }
var factory = new Factory();
var employee1 = factory.createEmployee('Peter', 'fulltime');
var employee2 = factory.createEmployee('Harry', 'parttime');
var employee3 = factory.createEmployee('Eric', 'temporary');This way, the Factory creates new instances of objects based on the input given. This way you don't have to worry about the different employee types in your code.
A singleton is used to restrict an object to one instance of that object across the application.
- Remembers the last time you used it
- Hands the same instance back
var GlobalCounter = (function() {
var count = 0;
return {
increase: function() { count++; },
decrease: function() { count--; },
getCount: function() { return count; }
}
})();
var counter1 = GlobalCounter.getInstance();
var counter2 = GlobalCounter.getInstance();The decorator pattern is used to add new functionality to an existing object, without being obtrusive.
- More complete inheritance
- Wraps an object
- Protects exiting objects
- Allows extended functionality
function Task(name) {
this.name = name;
this.isCompleted = false;
};
Task.prototype.complete = function() {
this.isCompleted = true;
};
var firstTask = new Task('Clean up');
firstTask.complete();Now lets say, we need an UrgentTask, but with the functionality of Task.
function UrgentTask(name, priority) {
// This will make sure the UrgentTask will inherit the properties of Task, by calling Task.
// The first parameter is the current scope (this), and de rest are the parameters required by Task.
Task.call(this, name);
this.priority = priority;
}
// This will make sure UrgentTask inherits the prototype from Task.
// Do not set UrgentTask.prototype = Task.prototype because it will link the two prototypes together, which we don't want.
UrgentTask.prototype = Object.create(Task.prototype);
// You can overwrite methods
UrgentTask.prototype.complete = function() {
// You can also call the same method from the super class, but this is optional
Task.prototype.complete.call(this);
console.log('Urgent task completed!');
};
var urgentTask = new UrgentTask('Call wife', 10);
urgentTask.complete();Used to provide a simplified interface to a complicated system.
Think of it as a building. The front wall of a building looks very nice and clean, even though there could be absolute, utter chaos going on inside that building. The good thing is that you don't know about this chaos, because you only see the nice part of the building.
- Facade hides the chaos from us
- Simplifies the interface
Now for example, say that you are given the following Task function from an API. In order to complete the task, you have to run 3 functions. Since we don't want to do that for every task we complete, we will build a facade on top of Task.
// The old stinky API
function Task(data) {
this.name = data.name;
this.priority = data.priority;
this.isComplete = false;
}
var TaskService = (function() {
return {
complete: function(task) {
task.isComplete = true;
console.log('Completed task: ' + task.name);
},
notify: function(task) {
console.log('Notify everyone because task "' + task.name + '" is now complete');
},
save: function(task) {
console.log('Save task "' + task.name + '" to database');
}
};
})();
var firstTask = new Task({ name: 'Clean up', priority: 50 });
TaskService.complete(firstTask);
TaskService.notify(firstTask);
TaskService.save(firstTask);Now the difference between the Facade pattern and the Decorator pattern, is that we're not adding functionality. We're only creating a cleaner interface. We will also use the Revealing Module Pattern, which easily shows what methods are returned from a method.
var TaskServiceWrapper = (function() {
function complete(task) {
TaskService.complete(task);
TaskService.notify(task);
TaskService.save(task);
}
return {
complete: complete
};
})();
var firstTask = new Task({ name: 'Clean up', priority: 50 });
TaskServiceWrapper.complete(firstTask);So now we only have to use TaskServiceWrapper.complete();.
Allows a collection of objects to watch an object and be notified of changes.
- Allows for loosely coupled system
- One object is the focal point
- Group of objects watch for changes
For this example we have a Task (the subject), a ObserverList (handles the subscriptions), a LoggingService and a NotificationService (the observers).
// The Task "class"
function Task(data) {
this.name = data.name;
this.priority = data.priority;
// To make it observable
this.observers = new ObserverList();
}
Task.prototype.complete = function() {
this.isComplete = true;
console.log('Completed task: ' + this.name);
};
Task.prototype.save = function() {
console.log('Save task "' + this.name + '" to database');
// Broadcast this when save is executed
this.notify(this);
};
Task.prototype.addObserver = function(observer) {
this.observers.add(observer);
};
Task.prototype.notify = function(context) {
// This loops over all the observers added to Task
for (var i = 0; i < this.observers.count(); i++) {
// This gets the observer and executes it passing in the context
this.observers.get(i)(context);
}
};
// Create the observerlist, which is used to add and get observers
function ObserverList() {
this.observerList = [];
}
ObserverList.prototype.add = function(obj) {
return this.observerList.push(obj);
};
ObserverList.prototype.get = function(index) {
if (index > -1 && index < this.observerList.length) {
return this.observerList[index];
}
};
ObserverList.prototype.count = function() {
return this.observerList.length;
};
// Create the logging service (observer)
function LoggingService() {
var message = 'Log for ';
this.update = function(task) {
console.log(message + task.name);
};
}
// Create the notification service (observer)
function NotificationService() {
var message = 'Notification for ';
this.update = function(task) {
console.log(message + task.name);
};
}
// Create instance of task
var firstTask = new Task({ name: 'Clean up', priority: 50 });
var loggingService = new LoggingService();
var notificationService = new NotificationService();
// Adding the subscribers to firstTask
firstTask.addObserver(loggingService.update);
firstTask.addObserver(notificationService.update);
firstTask.save();Controls communication between objects so neither object has to be coupled to the others.
- Allows for loosely coupled system
- One object manages all communication
- Many to many relationship
// The Task "class"
function Task(data) {
this.name = data.name;
this.priority = data.priority;
}
Task.prototype.complete = function() {
this.isComplete = true;
console.log('Completed task: ' + this.name);
};
Task.prototype.save = function() {
console.log('Save task "' + this.name + '" to database');
// Broadcast this when save is executed
Mediator.publish('save', this);
};
// Create the logging service (observer)
function LoggingService() {
var message = 'Log for ';
this.update = function(task) {
console.log(message + task.name);
};
}
// Create the notification service (observer)
function NotificationService() {
var message = 'Notification for ';
this.update = function(task) {
console.log(message + task.name);
};
}
var Mediator = (function() {
var channels = {};
// You will be subscribing to a certain channel
var subscribe = function(channel, context, callback) {
// If the channel doesn't exist yet, create it.
if (!Mediator.channels[channel]){
Mediator.channels[channel] = [];
}
// Add the context and callback to the subscription channel so we can use this while publishing.
Mediator.channels[channel].push({
context: context,
callback: callback
});
};
var publish = function(channel) {
if (Mediator.channels[channel]) {
// Get the other given arguments
var restArgs = Array.prototype.slice.call(arguments, 1);
// Loop over all the subscriptions
for (var i = 0; i < Mediator.channels[channel].length; i++) {
var subscription = Mediator.channels[channel][i];
// Execute the subscriber callback function, giving the context and the arguments passed into publish
subscription.callback.apply(subscription.context, restArgs);
}
}
};
return {
channels: {},
subscribe: subscribe,
publish: publish
}
})();
// Create instance of task
var firstTask = new Task({ name: 'Clean up', priority: 50 });
var loggingService = new LoggingService();
var notificationService = new NotificationService();
// Call the mediator
Mediator.subscribe('save', loggingService, loggingService.update)
firstTask.save();Encapsulates the calling of a method as an object.
- Fully decouples the execution from the implementation
- Allows for less fragile implementations
- Support undo operations
- Supports auditing and logging of operations
What it actually means, is calling functions through a string. You see this often in for example jQuery plugins when calling methods after initializing the plugin.
var BandService = {
all: function() {
return [
{name: 'Metallica', formed: 1981 },
{name: 'Foo Fighters', formed: 1994 }
];
},
find: function(bandName) {
return BandService.all().find(function(band) { return band.name === bandName; });
}
};
// The execute function, which is what the command pattern is
BandService.execute = function(cmd) {
var restArgs = Array.prototype.slice.call(arguments, 1);
if (BandService[cmd]) {
return BandService[cmd].apply(BandService, restArgs);
}
}
// Call the command function
BandService.execute('find', 'Metallica');The power of the command pattern hides in the fact that you can store all of these calls, and run them later. For example, when you don't have a connection to the database, you don't want the execute calls to get lost. You could store the calls, then retry to connect to the database and when that succeeds, you can run all of the calls.
var BandService = {
commands: [],
all: function() {
return [
{name: 'Metallica', formed: 1981 },
{name: 'Foo Fighters', formed: 1994 }
];
},
find: function(bandName) {
console.log('Find band: ' + bandName);
return BandService.all().find(function(band) { return band.name === bandName; });
},
// This will replay all the commands
replay: function() {
for (var i = 0; i < BandService.commands.length; i++) {
var command = BandService.commands[i];
BandService.executeNoLog(command.cmd, command.data);
}
}
};
BandService.executeNoLog = function(cmd) {
var restArgs = Array.prototype.slice.call(arguments, 1);
if (BandService[cmd]) {
return BandService[cmd].apply(BandService, restArgs);
}
};
BandService.execute = function(cmd) {
var restArgs = Array.prototype.slice.call(arguments, 1);
// Add the command to the commands list
BandService.commands.push({
cmd: cmd,
data: restArgs[0]
})
if (BandService[cmd]) {
return BandService[cmd].apply(BandService, restArgs);
}
}
// Call the command function
BandService.execute('find', 'Metallica');
BandService.execute('find', 'Foo Fighters');
// Fake a connection timeout. When connected again, replay all commands
setTimeout(function() {
BandService.replay();
}, 1000);