Encapsulation, Voting functions in Javascript

Javascript has a quite powerful mechanism to produce private data. Any function created within the scope of another function will have access to the outer function’s variables – even once the outer function has returned.

 The Problem

The user has pressed “Quit” in a large multi-part application. Different components must ensure that they are happy to close. They may answer this question immediately, or may need to go off and do some asynchronous work such as a server request or ask the user. How do we allow this?

A Solution

After a couple of design iterations this is one solution that uses the idea of a Voting Function. In this case we are only interested if we have the required number of Yes votes, so the voting function takes no arguments and registers a Yes vote when called. It can only be called once.

Unit Tests

Starting with the tests, consider some voters. Voters are functions in this example. A more complex example may use Objects, or a Function/Scope tuple.

/**
 * This voter will say yes immediately.
 */
function yesVoter(args, votingFunction){
	votingFunction();
}

/**
 * This voter will not say yes.
 */
function noVoter(args, votingFunction){
}

/**
 * This voter will try to vote twice.
 */
function doubleVoter(args, votingFunction){
	votingFunction();
	votingFunction();
}

/**
 * This voter will vote yes, after a delay
 */
function lateYesVoter(args, votingFunction){
	window.setTimeout(votingFunction, 10);
}

We can now write some tests:

When a vote is run with multiple participants, and they all say Yes, the vote will pass and the Accept Function will be called. I run the test in an Immediate Function so that its variables are contained to its scope.

(function(){
	var voters = [yesVoter, yesVoter, yesVoter],
		result = false;
	
	function onAccept() {
		result = true;
	}
	
	startVote(voters, onAccept);
	if(!result) throw "Test failed";
	console.log("Test with three YES votes OK");
}());

If a participant votes no then the Accept Function will not be called. Because “No” in this case is an absence of “Yes” then even if a function fails it will result in the Accept Function not being called. This matches the requirement.

(function(){
	var voters = [yesVoter, noVoter, yesVoter],
		result = false;
	
	function onAccept() {
		result = true;
	}
	
	startVote(voters, onAccept);
	if(result) throw "Test failed";
	console.log("Test with two YES and one NO was OK");
}());

This process is about giving components the chance to veto a process, so if there are no components to veto the vote should pass immediately.


(function(){
	var voters = [],
		result = false;
	
	function onAccept() {
		result = true;
	}
	
	startVote(voters, onAccept);
	if(!result) throw "Test failed";
	console.log("Test with no voters was OK");
}());

If a voter votes twice then it will mess things up, so ensure that they can’t.

(function(){
	var voters = [doubleVoter, noVoter];
	
	function onAccept() {
		throw "Test failed. Should not have accepted the vote.";
	}
	
	try {
		startVote(voters, onAccept);
		throw "Test failed. Should have thrown an acception.";		
	} catch (e) {
		console.log("Test looks OK: Exception was '" + e + "'");
	}
}());

Finally, voters must be able to work asynchronously.

(function(){
	var voters = [yesVoter, lateYesVoter],
		result = false;

	function onAccept() {
		result = true;
	}
	
	startVote(voters, onAccept);
	
	window.setTimeout(function(){
		if(!result) throw "Test failed";
		console.log("Test with delayed voter was OK");
	}, 40);
}());

First implementation

This implementation passes the tests, but is not useful for the original problem. This implementation will call each of the voters without waiting for any previous voter to finish, so if they all fire up dialogues then the user will be bombarded – unless they block the process while they work. Some web frameworks provide non-blocking dialogues.

/**
 * Call a vote. The voters may respond immediately or after a delay if they do asynchronous
 * work. Voters respond by calling a voting function to accept.
 * 
 * @param participants Array of Functions to call. They will be sent (args, votingFunction).
 * @param onAccept Function to call on vote accepted. Scope will the "this".
 * @param args Additional information to pass to the functions.
 */
function startVote(participants, onAccept, args) {
	var callbackScope = this,
		votesOutstanding = participants.length,
	    i,
	    votingFunction;
	
	// Handle the case of no participants. Automatically accept.
	if(participants.length == 0){
		onAccept.call(callbackScope, args);
		return;
	}
	
	// Ask the participants
	for(i = participants.length - 1; i >= 0; i--){
		// This is an inner scope to create private variables for
		// each voting function.
		(function(){
			var alreadyVoted = false;
			
			// This is the voting function.
			votingFunction = function(){
				if(alreadyVoted){
					throw "You cannot vote twice";
				}
				alreadyVoted = true;
				
				votesOutstanding--;
				if(votesOutstanding == 0){
					onAccept.call(callbackScope, args);
				}
			};
			
			participants[i](args, votingFunction);
		}());
	}
}

A great feature of this solution is that the state of the voting code is completely hidden from the voters. Rather than passing around an Object that can be mutated, all the voters have is a Function. A real implementation may pass that function as a property of an object, so the voter calls

    args.voteYes();

The advantage is that there are no confusing or possibly dangerous other properties on that object. The public interface is clean.

The client of the startVote function is called back with “this” being a pointer to whatever value it has in the startVote() function. This is achieved by storing the value in callbackScope in the closure. For example:

// Example code where the startVote() method and the callback are
// on the same class.
this.startVote(participants, this.onAccept);

// Example code where the callback is part of a different class.
// A good alternative to this would be to pass callbackScope as a
// parameter to the method, but as written:
this.startVote.call(otherClass, participants, otherClass.onAccept);

The voting function starting at line 29 is defined within an Immediate Function starting at line 25. This causes the alreadyVoted variable to be private to that instance of the voting function. It is completely encapsulated.

All of the voting functions share access to the function parameters and variables of the startVote function. If a second vote is started before the first has finished then each set of voting functions will have its own copy of these variables. It may be useful to know if a vote is guaranteed to fail, in which case the voting function would need a way of saying “No”.

The results of running the tests are:

Test with three YES votes OK
Test with two YES and one NO was OK
Test with no voters was OK
Test looks OK: Exception was 'You cannot vote twice'
Test with delayed voter was OK

Second implementation. Only one participant at a time

This version ensures that only one voter is asked at a time.

/**
 * Call a vote. The voters may respond immediately or after a delay if they do asynchronous
 * work. Voters respond by calling a voting function to accept.
 * 
 * This version ensures that only one participant is asked at a time.
 * 
 * @param participants Array of Functions to call. They will be sent (args, votingFunction).
 * @param onAccept Function to call on vote accepted. Scope will the "this".
 * @param args Additional information to pass to the functions.
 */
function startVote(participants, onAccept, args) {
	var callbackScope = this,
		remainingParticipants = participants.slice(0),
	    votingFunction;
		
	// Ask the participant.
	function askParticipant(participant, onSuccess){
		var alreadyVoted = false;
			
		votingFunction = function(){
			if(alreadyVoted){
				throw "You cannot vote twice";
			}
			alreadyVoted = true;
			onSuccess();
		};

		participant(args, votingFunction);
	}
	
	// Ask the next participant, until we run out of participants.
	// If we have reached the end of the participant list then
	// call the onAccept function.
	function askNextParticipant(){
		var participant;
		
		if(remainingParticipants.length > 0){
			participant = remainingParticipants.shift();
			askParticipant(participant, askNextParticipant);
		} else {
			onAccept.call(callbackScope, args);
		}
	}
	
	askNextParticipant();
}

Improvements

The caller cannot tell the difference between a vote being abandoned and a vote being cancelled. Potentially a participant could hold on to the last vote for a long time.

This entry was posted in javascript. Bookmark the permalink.

1 Response to Encapsulation, Voting functions in Javascript

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.