The book Design Patterns, co-published by GoF, provides many solutions to common problems in object-oriented software design. These models have been around for quite a long time and have proved very useful in many cases.
Monomer mode
There is only one instance of a particular class. This means that the second time you create a new object with the same class, you should get exactly the same object as the first one.
Creating a simple object using object literals is also an example of singleton, because there are no classes in JavaScript, only objects. When you create a new object, virtually no other object is similar to it, so the new object is already monolithic.
var obj = { myprop: 'my value' };
Use the new operator
JavaScript has no classes, but objects can be created using constructors in the new grammar, sometimes requiring a single implementation using this grammar. The idea is that when multiple objects are created using the same constructor with the new operator, only new pointers to exactly the same object should be obtained.
Examples in static attributes
Cache the instance in the static properties of the constructor. You can use an attribute similar to Universe.instance and cache the instance in that attribute. The disadvantage of this scheme is that instance attribute is publicly accessible and may be modified in external code.
function Universe() { if (typeof Universe.instance === 'object') { return Universe.instance; } this.start_time = 0; Universe.instance = this; } // test var uni = new Universe(); var uni2 = new Universe(); uni === uni2; // true
Examples in closures
You can wrap the instance in a closure. This ensures the privatization of the instance. The cost is additional closure overhead.
function Universe() { var instance = this; this.start_time = 0; Universe = function () { return this; }; }
If you need to make the prototype and constructor pointers work as expected, you can achieve this goal by making some adjustments:
function Universe() { var instance; Universe = function Universe() { return instance; }; Universe.prototype = this; instance = new Universe(); instance.constructor = Universe; instance.start_time = 0; return instance; }
Another solution is to wrap constructors and instances in instant functions.
var Universe; (function () { var instance; Universe = function Universe() { if (instance) { return instance; } instance = this; this.start_time = 0; }; }());
Factory mode
The factory pattern is designed to create objects. It is usually implemented in static methods of classes or classes with the following objectives:
- Perform repetitive operations when creating similar objects
- Provide an interface for factory customers to create objects without knowing the specific type (class) at compile time.
Objects created through factory methods (or classes) are designed to inherit the same idea of parent objects, which are specific subclasses that implement specialized functions. Sometimes the common parent class is the same class that contains factory methods.
Here is an example of the implementation of the factory pattern
function CarMaker() {} CarMaker.prototype.drive = function () { return this.doors; }; CarMaker.factory = function (type) { var constr = type, newcar; if (typeof CarMaker[constr] !== 'function') { throw { name: "Error", message: constr + " not exist" }; } if (typeof CarMaker[constr].prototype.drive !== 'function') { CarMaker[constr].prototype = new CarMaker(); } newcar = new CarMaker[constr](); return newcar; }; CarMarker.Compact = function () { this.doors = 4; }; CarMarker.Convertible = function () { this.doors = 2; }; CarMarker.SUV = function () { this.doors = 24; };
Built-in Object Factory
var o = new Object(), n = new Object(), s = Object('1'), b = Object(true); o.constructor === Object; n.constructor === Number; s.constructor === String; b.constructor === Boolean; // They are all true.
Iterator pattern
In the iterator pattern, there is usually an object that contains a collection of data. The data may be stored within a complex data structure, but a simple way to access each element in the data structure is provided. Consumers of objects do not need to know how to organize data, all they need to do is take out individual data to work.
In the iterator pattern, the object needs to provide a next() method. Calling next() in turn must return the next contiguous element. Of course, in a particular data structure, the meaning of "next" is up to you.
Example
var agg = (function () { var index = 0, data = [1, 2, 3, 4, 5], length = data.length; return { next: function () { var element; if (!this.hasNext()) { return null; } element = data[index]; index = index + 2; return element; }, hasNext: function () { return index < length; } }; }());
Decorator Model
In decorator mode, you can dynamically add additional features to objects at run time. A convenient feature of the decorator pattern is the customizability and configurability of its expected behavior. You can start with ordinary objects that have only some basic functions, then choose from the available decoration resource pool those functions that need to be used to enhance ordinary objects, and decorate them in order, especially when the order of decoration is important.
Implementation through prototype chain inheritance
function Sale(price) { this.price = price || 100; } Sale.prototype.getPrice = function () { return this.price; }; Sale.decorators.fedtax = { getPrice: function () { var price = this.uber.getPrice(); price += price * 5 / 100; return price; } }; Sale.decorators.quebec = { getPrice: function () { var price = this.uber.getPrice(); price += price * 7.5 / 100; return price; } }; Sale.prototype.decorate = function(decorator) { var F = function () {}, overrides = this.constructor.decorators[decorator], i, newobj; F.prototype = this; newobj = new F(); newobj.uber = F.prototype; for (i in overrides) { if (overrides.hasOwnProperty(i)) { newobj[i] = overrides[i]; } } return newobj; }; // test var sale = new Sale(100); sale = sale.decorate('fedtax'); sale = sale.decorate('quebec'); sale.getPrice();
Use list implementation
function Sale(price) { this.price = (price > 0) || 100; this.decorators_list = []; } Sale.decorators = {}; Sale.decorators.fedtax = { getPrice: function (price) { return price + price * 5 / 100; } }; Sale.decorators.quebec = { getPrice: function (price) { return price + price * 7.5 / 100; } }; Sale.prototype.decorate = function (decorator) { this.decorators_list.push(decorator); }; Sale.prototype.getPrice = function () { var price = this.price, i, max = this.decorators_list.length, name; for (i = 0; i < max; i += 1) { name = this.decorators_list[i]; price = Sale.decorators[name].getPrice(price); } return price; } // test var sale = new Sale(100); sale.decorate('fedtax'); sale.decorate('quebec'); sale.getPrice();
Strategy mode
Policy patterns allow you to select algorithms at runtime. The client side of the code can work with the same interface, but it chooses an algorithm to handle a specific task from multiple algorithms according to the context in which the client is trying to execute the task.
Examples of data validation
var validator = { types: {}, messages: [], config: {}, validate: function (data) { var i, msg, type, checker, result_ok; this.messages = []; for (i in data) { if (data.hasOwnProperty(i)) { type = this.config[i]; checker = this.types[type]; if (!type) { continue; } if (!checker) { throw { name: 'ValidationError', message: 'No handler to validate type ' + type; }; } result_ok = checker.validate(data[i]); if (!result_ok) { msg = "Invalid value for *" + i + "*, " + checker.instructions; this.messages.push(msg); } } } return this.hasErrors(); }, hasErrors: function () { return this.message.length !== 0; } }; validator.types.isNonEmpty = { validate: function (value) { return value !== ""; }, instructions: "this value cannot be empty" }; validator.types.isNumber = { validate: function (value) { return !isNaN(value); }, instructions: "this value can only be a valid number, e.g. 1, 3.14 or 2010" }; // test var data = { first_name: "Super", age: "unknown", }; validator.config = { first_name: 'isNonEmpty', age: 'isNumber', }; validator.validate(data); if (validator.hasErrors()) { console.log(validator.message.join("\n")); }
Appearance mode
Appearance pattern is a simple pattern that provides an optional interface for objects. It's a very good design practice that keeps methods simple and doesn't make them deal with too much work. If there are many uber methods that accept multiple parameters, in contrast, according to this implementation method, more methods will eventually be created. Sometimes, two or more methods may be commonly invoked together. In such cases, it makes sense to create another method to wrap duplicate method calls.
Appearance mode is very suitable for browser script processing, which can hide the differences between browsers behind appearance.
var myevent = { stop: function (e) { if (typeof e.preventDefault === 'function') { e.preventDefault(); } if (typeof e.stopPropagation === 'function') { e.stopPropagation(); } if (typeof e.returnValue === 'boolean') { e.returnValue = false; } if (typeof e.cancelBubble === 'boolean') { typeof e.cancelBubble = true; } } };
proxy pattern
In the proxy design pattern, one object acts as an interface to another object. The proxy is between the client of the object and the object itself, and it protects the access of the object.
One example of using this pattern is what we can call delayed initialization, where the agent receives the initialization request, but it never passes the request to the ontology object until the ontology object is explicitly used.
Example (omitted)
- Increase performance by merging multiple http requests through proxies
- Cache proxy
Intermediary model
Applications, regardless of their size, consist of a number of individual objects. All these objects need a way to communicate with each other, which does not reduce maintainability to a certain extent, nor does it damage the ability to change parts of the application without compromising the rest of the application. As applications grow, more and more objects will be added. Then, during code refactoring, objects will be deleted or reorganized. When objects know too much information from each other and communicate directly (calling each other's methods and changing attributes), this leads to poor tight coupling.
The mediator model alleviates this problem and promotes loose coupling. In this model, independent objects do not communicate directly with each other, but through mediator objects. When one of the colleague objects changes state, it will notify the mediator, and the mediator will communicate the change to any other colleague object that should know the change.
Examples of intermediaries
function Player(name) { this.points = 0; this.name = name; } Player.prototype.play = function () { this.points += 1; mediator.played(); } var scoreboard = { element: document.getElementById('results'); update: function (score) { var i, msg = ''; for (i in score) { if (score.hasOwnProperty(i)) { msg += i + ': ' + score[i] + ' | ' } } this.element.innerHTML = msg; } }; var mediator = { players: {}, setup: function () { var players = this.players; players.home = new Player('Home'); players.guest = new Player('Guest'); }, played: function () { var players = this.players, score = { Home: players.home.points, Guest: players.guest.points }; scoreboard.update(score); }, keypress: function (e) { e = e || window.event; if (e.whitch === 49) { mediator.players.home.play(); return; } if (e.whitch === 48) { mediator.players.guest.play(); return; } } }; // test mediator.setup(); window.onkeypress = mediator.keypress; setTimeout(function () { window.onkeypress = null; alert('Game over!'); }, 30000);
Observer model
Observer mode is widely used in client JavaScript programming. All browser events are examples of this pattern. It is also called subscription/publishing mode.
The main motivation behind the design of this model is to promote loose coupling. In this pattern, instead of one object calling another object's method, one object subscribes to a specific activity of another object and is notified when the state changes. Subscribers are also called observers, while the objects of observers are called publishers or topics. When an important event occurs, the publisher notifies (calls) all subscribers and may often pass messages in the form of event objects.
Example: magazine subscription
We want to implement a function: publisher paper, daily newspapers and monthly magazines, and subscriber joe is notified of news.
Here is an implementation example of the generic publisher function
var publisher = { subscribers: { any: [] // Event type: subscribers }, // Adding subscribers to subscribers array subscribe: function (fn, type) { type = type || 'any'; if (typeof this.subscribers[type] === 'undefined') { this.subscribers[type] = []; } this.subscribers[type].push(fn); }, // Delete subscribers from subscribers array unsubscribe: function (fn, type) { this.visitSubscribers('unsubscribe', fn, type); }, // Loop through each element in subscribers and invoke the method they provided for registration publish: function (publication, type) { this.visitSubscribers('publish', publication, type); }, visitSubscribers: function (action, arg, type) { var pubtype = type || 'any', subscribers = this.subscribers[pubtype], i, max = subscribers.length; for (i = 0; i < max; i += 1) { if (action === 'publish') { subscribers[i](arg); } else { if (subscribers[i] === arg) { subscribers.splice(i, 1); } } } } };
Converting Ordinary Objects into Publisher Objects
function makePublisher(o) { var i; for (i in publisher) { if (publisher.hasOwnProperty(i) && typeof publisher[i] === 'function') { o[i] = publisher[i]; } } o.subscribers = { any: [] }; }
Functional realization:
paper object
var paper = { daily: function () { this.publish("big news totay"); }, monthly: function () { this.publish("interesting analysis", "monthly"); } };
Construct a paper as a publisher
makePublisher(paper);
Subscriber object joe
var joe = { drinkCoffee: function (paper) { console.log('Just read ' + paper); }, sundayPreNap: function (monthly) { console.log('About to fall asleep reading this' + monthly); } };
joe Subscribes to paper
paper.subscribe(joe.drinkCoffee); paper.subscribe(joe.sundayPreNap, 'monthly');
Trigger some events
paper.daily(); paper.monthly();
Print results
Just read big news totay About to fall asleep reading this interesting analysis
The good part of the code is that paper objects do not have hard coded joe, and joe objects do not have hard coded paper. In addition, there are no intermediary objects in the code that know everything. Participants are loosely coupled, and we can add more subscribers to the paper without modifying them.
Let's extend the example further and make joe a publisher:
makePublisher(joe); joe.tweet = function (msg) { this.publish(msg); };
The public relations department that chooses paper needs to read the reader's tweet s and subscribe to joe's information, so it needs to provide the method readTweets():
paper.readTweets = function (tweet) { console.log('Call big meeting! Someone ' + tweet); }; joe.subscribe(paper.readTweets);
implement
joe.tweet('hated the paper today');
Printing
Call big meeting! Someone hated the paper today
Reference resources
- JavaScript Mode