Skip to content

goofballLogic/gbL.jsMop

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

42 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gbL.jsMop

Preamble

http://www.purl.org/stefan_ram/pub/doc_kay_oop_en

Sending messages

When I think about OO, I think of objects like individual biological cells, which can communicate by emitting messages which can be absorbed by nearby cells.

This is a bit different than the normal paradigm in most C-like languages - javascript, C#, Java etc.

Where, in normal javascript, I would write

listView.render(viewData);

I would really rather do something like

send.renderViewList(viewData);

and have this message picked up by the appropriate view. It means I don't have to retain a reference to the receiving object(s), and it fits the biological model more closely.

In the recipient, I would like to specify messages I will receive. Something like

receive.renderView(viewData);

However, this kind of paradigm doesn't really exist at a native level in javascript. You can simulate this kind of thing in many ways, including things like the Observer pattern, Bus architectures and Blackboard patterns/architectures.

I wanted something simple, flexible, but also something that worked with a minimum of effort. So I write the gbL.jsMop script as a workaround.

tl;dr / Show me the money

Here we create a mop object, and create+register a Controller object and two View objects. Then I send a "show names" message through the mop which is received by the Controller. The Controller then processes the data into a view model and sends a "render view list" message. The "List" View object receives this message and renders itself using the view model sent by the Controller.

var mop = new gbL.jsMop()
	.register(new Controller(), "List controller")
	.register(new View("List"), "List view")
	.register(new View("AnOther"), "AnOther view")
	;
	
var data = [
	{ name: "Jessie" },
	{ name: "Imran" }
];

/* 
    When the "show names" message is sent, the controller would 
    receive it, because it says "I.receive.showNames"
*/ 

function Controller() {

	var I = { send: function() {}, receive: {} };
	
	// messages I send
	I.send.renderViewList = function(viewData){};
	
	// messages I receive
	I.receive.showNames = function(items) {
		var viewData = generateNameListViewModel(items);
		I.send.renderViewList(viewData);
	};
	
	return I;
	
	function generateNameListViewModel(items) {
		. . .
	}
}

function View(viewName) {

	var I = { receive: {} };
	
	// messages I receive
	I.receive.renderView = function(viewData) {
		renderSelf(viewData);
	}
	// ... but only messages ending with my name
	I.receive.renderView.filter = function(topics, data) {
		return topics[topics.length] === viewName;
	}
	
	return I;
	
	function renderSelf(viewData) {
		. . .
	}
}

Modular design

I also want to be able to divide my code up into dependency-free units. I don't want my constructor functions for each class of object to reference each other at design time any more than I want my objects to reference each other at run-time….

So I added the bootstrap pattern. For example:

main.js

var mop = new gbL.jsMop().boot({
	"View factory" : require("./viewFactory"),
	"List controller" : require("./listController"),
});

viewFactory.js

module.exports.init = function(mop) {
	mop.register(new ViewFactory(mop), "View factory");
};

function ViewFactory(mop) {
	var I = { receive: {} };
	
	// messages I receive
	I.receive.buildView = function(viewName) {
		var view = new View(viewName);
		mop.register(view, viewName + " view");
	};
	
	return I;
}

function View(viewName) {
	. . .
}

listController.js

module.exports.init = function(mop) {	
	mop.register(new Controller(), "List controller");
}

function Controller() {

	var I = { send: function() {}, receive: {} };
	
	// messages I send
	I.send.buildView = function(){};
	I.send.renderViewList = function(viewData){};
	
	// messages I receive
	I.receive.showNames = function(items) {
		ensureListView();
		var viewData = generateNameListViewModel(items);
		I.send.renderViewList(viewData);
	};
	
	return I;
	
	function ensureListView() {
		I.send.buildView("List");
		ensureListView = function() { }; // naughty
	}
	
	function generateNameListViewModel(items) {
		. . .
	}
}

Environment

Designed to work specifically in the browser, or in Node, but should work in most CommonJS environments.

Running/Building tests

####Without a browser (mocha) You will need to

npm install mocha
npm install expect.js

and then

make test

####In the browser Just browse to

test-browser/browserTests.html

####Building (and opening) the browser tests You will need to

npm install browserify

and then

make browser-test

Up-to-date example (if below==tl;dr)

The constructor is divided into two sections:

  1. Define the messages received and/or sent
  2. Define business logic in functions

Below the sections are divided by the line "return I;"

function Controller() {

	// I will both send and receive messages
	var I = { receive: {}, send: {} };
	
	// the "render view" message - intended for registered Views to receive
	I.send.renderView = function(viewName, data, res){};
	
	// the "model update request" message - intended for the model
	I.send.modelUpdateRequest = function(command){};
	
	// the router creates this message on receiving GET /documentList
	I.receive.GETdocumentList = function(req, res) {
    	listDocuments(function(data) {
    		I.send.renderView("document-list", data, res);
    	});
    };
	
	return I;
	
	function listDocuments(callback) {
		// send a "list-documents" command to the model
		I.send.modelUpdateRequest("list-documents", function(domain) {
			callback(domain.documents);
		});
	}
}

An object of this type can be registered as normal:

var mop = new gbL.jsMop();
mop.register(new ns.Controller(), "Documents controller");
mop.register(new ns.ListDocumentsView(), "List documents view");
mop.reigster(new ns.DomainCommandProcessor(), "Command interface for the domain");

or, using the bootstrap pattern (see below):

new gbL.jsMop().boot({
    "controller" : require("./controllers/Controller.js"),
    "documents view" : require("./views/ListDocuments.js"),
    "domain command processor" : require("./model/CommandProcessor.js")
});

For this to work, Controller.js would have to include the bootstrapping code which allows modules to initialise themselves and register objects. It would end up looking something like:

module.exports.bootstrap = function(mop) {
	mop.register(new Controller(), "Documents controller");
};

function Controller() {
	. . . code (as above) goes here . . .
}

tl;dr

No, really - I mean it. tl;dr

Version specific notes

Unless otherwise indicated, the material in the Pervious Versions section still applies

New in version 0.9.7

Tuesday, 20 November 2012 Version 0.9.7

New pattern of registering receive and send messages

The principle change in this version is a new facility to receive and send which allows a slightly cleaner syntax. It also encourages you to declare the messages you will send ahead-of-time.

"Interface" concept

Objects can now simulate declaration of an interface using the revealing module pattern. Often you will see this:

function Controller() {

	var I = {
		// public members here
	};

	return I;
	
	// private functions here
	
}

Receiving and sending

An object wishing to receive and/or send should now declare a receive and or send attribute:

function Controller() {

	var I = {
		receive: {
			// messages to receive here
		},
		send: {
			//messages to send here
		}
	}
	
	return I;
	
	// private functions here
}

No more underscoring

Messages in these receive and send attributes should be declared using simple camel casing

e.g.

this.receive_add_commit_message = function(stuff) {
	// do something with the stuff received
};

should now be written as

this.receive.addCommitMessage = function(stuff) {
	// do something with the stuff received
};

Collect senders at the top

To make debugging easier, declare your senders at the top as (usually) empty functions:

e.g.

this.send.addCommitMessage = function(stuff){};

after the object is registered, will send a message with subject "add commit message". The inclusion of parameters in the empty function definition is useful as documentation. In addition, you can add your own action which will be called after the message is sent.

e.g.

this.send.addCommitMessage = function(stuff) {
	log("A commit message has been sent");
};

Base Version - 0.9.3

Sunday, 10 June 2012
Version 0.9.3

Examples

Example usage can be found in the /test/scenarios folder.

Basic usage

####First step is to spin up a hub for the messages:

var mop = new gbL.jsMop.Mop();

or

var mop = new require("gbL-jsMop").Mop();

####Sending

Then, to send a message:

mop.send("Hello world").as("test");

which sends a message with subject test and payload of Hello world.

####Receiving If I am an object wanting to receive this sort of message, I would include a method named receive_test:

var receiver = {
    receive_test: function(data) {
        console.log(data);
    }
};

and I would register with the hub to receive messages:

mop.register(receiver, "My first receiver");

#####or

if I don't wish to register as an object, I can regsiter a call back function:

mop.registerHandler("test", function(data) {
    console.log(data);
});

##Debugging There are a few tools to help with debugging message passing. For analysis of registered objects and handlers, just send a census message:

var registered = mop.send().as("census");
console.log("Registered receivers: " + registered.join(", ");

You may also wish to turn on console logging by using:

mop.debug = true;

or

mop.send.debug = true;

##Bootstrapping modules Say you have a set of modules which contain objects wishing to participate in message exchange through a given mop.

For example, in Node, you might have a console-logger.js:

(function(context) {
	
	var mop;
	
	context.bootstrap = function(initMop) {
		mop = initMop;
		initMop.Register(loggingSingleton, "Console logger");
	}
	
	var loggingSingleton = new function() {
		return {
			receive_log: function(data) {
				var label = mop.topics.slice(1).join(" ");
				console.log(label, data);
			}
		};
	}();
	
})(module.exports);

and then as part of bootstrapping, include the console-logger:

var mop = new require("gbL-jsMop").Mop();
mop.boot({
	"logger": require("console-logger"),
	"worker": require("important-worker-module"),
	"another": require("another-important-worker-module")
});

which will mean that e.g. the following will print my friend's name to the console:

mop.send("Lisa Jue Bishop").as("log the name of my dear friend");

#####Or In the browser, you might have a ticker object:

(function(context) {
	var mop;
	
	context.Ticker = {
		bootstrap: function(jsMop) {
			mop = jsMop;
			mop.register(new Ticker(), "Ticker");
		}
	};
	
	function Ticker() {
		// private state and behaviours
		var cancelled = false;
		function tick() {
			mop.send().as("tick");
			if(!cancelled) setTimeout(100, tick);
		}
		// message receivers
		return {
			receive_cancel_ticker: function() {
				cancelled = true;
			}
		}
	}
	
})(gbL.Stocks || { gbL.Stocks = {} });

which you could then boot as so:

var mop = new gbL.jsMop.Mop();
mop.boot({
	"ticker": gbL.Stocks.Ticker,
	"symbol-list": gbL.Stocks.SymbolLister
});

which will cause tick messages to be sent until:

mop.send().as("cancel ticker");

is sent.

##Further patterns

####Partial subject match If you want to accept messages about a more general subject than those specified for the messages, you can receive messages which match the start of the subject:

function HelloListener() {
	this.receive_hello = function() {
		console.log(mop.subject);
	};
}

would receive:

mop.send().as("hello world");

but would also receive:

mop.send().as("hello heaven");
mop.send().as("hello hell");

####Filtering messages If you want to filter the messages received for a given subject, you can attaching a filtering function, like so:

function BeerWatcher() {
	this.receive_important_notification = function(notification) {
		console.log("CRITICAL: " + notification);
	};
	this.receive_important_notification.filter = function(topics, data)
	{
		// only interested in notifications mentioning beer in their subject
		return ~topics.join(" ").indexOf("beer");
	};
}

####Filtering lots of handlers If you have an object which only wants to receive messages which mention a specific ID, you can:

function CalculationNode(nodeId, calculationStrategy) {
	// private state and behaviour
	var parameters, lastResult;
	function reset(preserveResult) { 
		parameters = [];
		preserveResult || lastResult = null; 
	}
	function execute() {
		return (lastResult = calculationStrategy.apply(this, parameters));
	}
	reset();
	
	// message receivers
	this.receive_reset_parameters = function() {
		reset(true);
	};
	this.receive_parameterise = function() {
		for(var i in arguments) parameters.push(arguments[i]);
	};
	this.receive_calculate = function() {
		mop.send(execute()).as("result for node " + nodeId);
	};

	mop.setReceiveFilters(this, function(topics, data) {
		// all the above, only for messages about this node (by Id)
		return ~topics.indexOf("node " + nodeId);
	});
	
	// unfiltered receivers
	this.receive_global_reset = function() {
		reset();
	};
	this.receive_return_results = function() {
		return new function() { this[nodeId] = lastResult; };
	}
}

####Adapter You may wish to use an adapter to send and receive messages, especially when you want to mix the message-passing paradigm with calling methods directly. For example, using the revealing module pattern, you might do something like:

function ServiceAgent() {

    var configuration = null;
	var fetched = [];
	
    function saveConfiguration(config) {
        configuration = $(config).clone();
    }
    
	function dataGet(toGet) {
		return mop
			.send(configuration.baseUrl, toGet)
			.as("ajax GET");
	}
	
	function injestData(data) {
		fetched.push(data);
	}
	
    // inner facet (mop adapter)
    mop.register({
    	receive_configuration: saveConfiguration,
    	receive_data_received: injestData,
    }, "Service Agent");
    
	// outer facet (revealed methods)
    return {
        listOrders: function() {
        	var data = null;
        	if(dataGet("orders")) data = fetched.pop();
        	return data;
       	}
    };
}

An object constructed by this function will expect to collaborate with

  • An object whose responsibility is to broadcast configuration (sending messages with subject "configuration")
  • An object whose responsibility is to make AJAX calls (receiving subjects beginning with "ajax", and sending back the data with subject "data received")

And it exposes a method which can be called directly as so:

var serviceAgent = new ServiceAgent();
var orders = serviceAgent.listOrders();

About

Message passing for javascript

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published