Observer Design Pattern: A Deep Dive

The Observer Design Pattern is a critical tool in the JavaScript developer’s arsenal, offering a flexible and effective solution to manage interactions between different components of a system.

While it is often associated with the Publish/Subscribe pattern, there are key differences between the two. Both patterns deal with communication between different parts of an application, but while the Publish/Subscribe pattern uses a global message bus to facilitate this communication, the Observer pattern is more localized and direct, requiring observers to subscribe directly to the objects they are interested in observing.

The Observer pattern encourages loose coupling between objects. It achieves this by having a ‘subject’ object that maintains a list of ‘observers’. These observers are then notified when there’s a change in the state of the subject object. This pattern is very popular in JavaScript due to its ability to help manage complex, interdependent operations in a clear and organized manner.

Implementing the Observer Pattern

Implementing the Observer pattern in JavaScript involves creating an ‘Observers’ object, which serves as a container for all the observers subscribed to a specific subject. This object maintains a list of all observers and provides an API for adding, removing, and retrieving observers.

An ‘Observer’ object is also needed, and this object includes a method for notification of changes in the subject.

Creating the Observers (observer.js)

javascriptCopy code// JavaScript Document
define(function() {
	'use strict';
	
	var Observers = function() {
		this.observers = [];
	};
	
	Observers.prototype.add = function (observer) {
		this.observers.push(observer);
	};
	
	Observers.prototype.remove = function (observerToRemove) {
		this.observers = this.observers.filter(function(observer) {
			return observer !== observerToRemove;
		});
	};
	
	Observers.prototype.get = function () {
		return this.observers;
	};
	
	return Observers;
});

Creating the Subject (subject.js)

The ‘Subject’ object is the entity that is being observed. It maintains a list of observers and notifies them when its state changes. In this case, the ‘Subject’ object holds a list of items, and observers are notified whenever an item is added or removed.

javascriptCopy code// JavaScript Document
define(function(require) {
	'use strict';
	
	var Observers = require('observer/observers'); 
	
	var Collection = function (items) { 
		this.observers = new Observers(); 
		this.collection = items || []; 
	};
	
	Collection.prototype.observe = function (observer) { 
		this.observers.add(observer); 
	};
	
	Collection.prototype.unObserve = function (observer) {
		this.observers.remove(observer);	
	};
	
	Collection.prototype.notify = function (event, data) { 
		this.observers.get().forEach(function (observer) { 
			observer.notify(event, data); 
		});
	};
	
	Collection.prototype.add = function (item) { 
		this.collection.push(item); 
		this.notify('added', item);
	};
	
	Collection.prototype.remove = function (itemToRemove) { 
		this.collection = this.collection.filter(function (item) { 
			if (item !== itemToRemove) { 
				return true;
			}
			this.notify('removed', item); 
			return false;
		}, this);
	};
	
	return Collection; 
});

Creating an Observer (observer.js)

An observer is an entity that can be notified of changes in the state of the subject. In this case, the observer just logs a message to the console when it is notified of a change.

javascriptCopy code// JavaScript Document
define(function() {
	'use strict';
	
	var Observer = function (name) { 
		this.name = name;
	};
	
	Observer.prototype.notify = function (event, data) { 
		console.log('The event was ', '"' + event + '",', 'the data was', data, 'and I am ', this.name);
	};
	
	return Observer;
});

Tying it All Together (main.js & init.js)

Finally, the main.js and init.js files are used to tie all of these components together. A subject is created, observers are added, data is added and removed, and observers are notified of each change.

javascriptCopy code// main.js
require (
	['factory/init', 'pubSub/init', 'strategy/init', 'observer/init'],
	function (factory, pubSub, strategy, observer) {
			'use strict';
	
			var examples = {
					factory: factory,
					pubSub: pubSub,
					strategy: strategy,
					observer: observer
				};
			
			window.runExample = function (example) {
				examples[example].init();
			};
	}
);

// init.js
define(function(require) {
	'use strict';

	return {
		init: function() {
			var subject, observer, otherObserver, data, moreData, 
				Subject = require('observer/subject'), 
				Observer = require('observer/observer'); 
				
			subject = new Subject(); 
			observer = new Observer('observer1'); 
			otherObserver = new Observer('observer2'); 
			
			data = { 
				prop: 'something'
			};
			moreData = {
				prop: 'something else'
			};
			
			subject.observe(observer); 
			subject.observe(otherObserver);
			
			subject.add(data); 
			subject.add(moreData);
			
			subject.unObserve(observer);
			
			subject.remove(data);
		}
	};
});

While the observer pattern does result in some degree of coupling between the subject and observers, it is a beneficial and necessary form of coupling that allows for efficient communication and interaction between different components of a system. This pattern allows developers to keep their code organized, flexible, and manageable.