Who Controls The Data When Modules Don't Know The Whole Story
Lately, I've been doing a lot of thinking about modules in JavaScript. As my client-side code has become much more complex than it used to be, I'm seeing that proper JavaScript application architecture is critically important. As I've tried to decouple my modules using encapsulated code and pub/sub (publish and subscribe) communication, however, I'm finding that I don't really know what I'm doing; this is especially true, when modules are displaying data that doesn't necessarily represent the complete, underlying story.
As I've moved into a more modular JavaScript world, I've tried to learn from really bright people like Addy Osmani (Large-Scale Application Architecture), Rebecca Murphey (Pubsub Screencast), and Nicholas Zakas (Managing JavaScript Objects); but, I'm not yet able to tie all of the concepts together in a nice coherent way. One place where I keep tripping up is the unquestioning faith in "clean data" that allows the modules to remain so decoupled from the application.
In so much of what I've been reading, it seems that the Application - the glue that brings all of the independent modules together - is a rather unintelligent layer. Its job appears to be little more than registering modules and facilitating communication. But what happens when the data that powers the modules needs to be moderated or filtered? Where does the intermediary logic live, and how does this affect inter-module communication?
To explore this kind of a scenario, I've put together a tiny demo that consists of three modules:
Stats - Message stats.
Form - UI for user to enter message.
List - List of recently entered messages.
The Form allows the user to enter a message. This message then gets cached locally in the application and the Stats and List modules are updated in turn. The twist here, however, is that only the most recent N messages will be displayed in the List. Furthermore, the Stats module displays two values: the number of messages posted and the number of messages displayed. This means that two of the modules - Stats and List - depend on data that is outside their point of view.
To accomodate this meta-data about the collection of messages, I've chosen to use some direct, explicit invocation instead of event-based, implicit invocation. Rather than having the List controller simply "listen" for new messages, it has to be told by the application which messages to display. Likewise, the Stats module can't simply listen for new messages as the values that it needs to show are dependent upon rules governed by an external entity; it, therefore, also needs to be told explicitly what values to use.
To allow for this moderated data, I'm using a combination of event-based communication and direct module-API method invocation:
<!DOCTYPE html>
<html>
<head>
<title>
Who Controls The Data When Modules Don't Know The Whole Story
</title>
</head>
<body>
<h1>
Who Controls The Data When Modules Don't Know The Whole Story
</h1>
<!-- This displays stats about the page. -->
<div class="stats">
<p class="displaying">
Displaying <span class="value">0</span> messages.
</p>
<p class="posted">
Posted <span class="value">0</span> messages.
</p>
</div>
<!-- This allows user interaction. -->
<form class="message">
<p>
<input type="text" name="message" value="" />
<input type="submit" value="Submit" />
</p>
</form>
<p>
Previous messages:
</p>
<!-- This displays messages submitted by the user. -->
<ul class="messages">
<!-- To be populated later. -->
</ul>
<!-- --------------------------------------------------- -->
<!-- --------------------------------------------------- -->
<script type="text/javascript" src="../jquery-1.6.3.js"></script>
<script id="app" type="text/javascript">
// NOTE: For this demo, we'll consider the "GLOBAL" scope to
// represent the "APP". I'm not creating it as a self-
// contained module so as to not take up more space.
// I am the messages that the user has submitted via the
// form interface.
var messages = [];
// I am the maximum number of messages to display on the
// screen (most recent messages).
var maxMessages = 3;
// I am the beacon to which events can be bound. This allows
// bi-directional communication between modules as well as
// between the "app" and the modules.
var sandbox = $( "script#app" );
// Bind the form submission event coming from the controller
// so that we can manage our messages.
sandbox.bind(
"message:sent",
function( event, message ){
// Add the message the top of our message queue.
messages.unshift( message );
// Update the message list using the top X of the
// message queue.
listController.setMessages(
messages.slice( 0, maxMessages )
);
// Update the stats.
statsController.setStats(
Math.min( messages.length, maxMessages ),
messages.length
);
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// -------------------------------------------------- //
// -------------------------------------------------- //
// Define the stats controller.
var statsController = (function( sandbox, container ){
// Cache some DOM elements.
var dom = {};
dom.container = container;
dom.displaying = dom.container.find( "p.displaying span.value" );
dom.posted = dom.container.find( "p.posted span.value" );
// Return an API for updating the values.
return({
// I update the stats values.
setStats: function( displaying, posted ){
// Update the DOM values.
dom.displaying.text( displaying );
dom.posted.text( posted );
}
});
})( sandbox, $( "div.stats" ) );
// -------------------------------------------------- //
// -------------------------------------------------- //
// Define the form controller.
var formController = (function( sandbox, container ){
// Cache some DOM elements.
var dom = {};
dom.container = container;
dom.message = dom.container.find( "input[ name = 'message' ]" );
// Bind to the submit action to make sure the form
// doesn't truly submit.
dom.container.submit(
function( event ){
// Cancel the submit event - we'll handle this
// on the client-side.
event.preventDefault();
// Check to see if there is a value entered (if
// not, then this is not a valid submission).
if (!dom.message.val().length){
// There is no message to submit.
return;
}
// Announce the message event.
sandbox.trigger(
"message:sent",
dom.message.val()
);
// Clear and refocus the form.
dom.message
.val( "" )
.focus()
;
}
);
// Return the controller API.
return({
// ...
})
})( sandbox, $( "form.message" ) );
// -------------------------------------------------- //
// -------------------------------------------------- //
// Define the message list controller.
var listController = (function( sandbox, container ){
// Cache some DOM elements.
var dom = {};
dom.container = container;
// I update the message list, re-drawing the list based
// on the given messages.
function updateList( messages ){
// Clear the message list.
dom.container.empty();
// Loop over each message to add it to the list.
for (var i = 0 ; i < messages.length ; i++){
// Create a new list item.
dom.container.append(
"<li>" + messages[ i ] + "</li>"
);
}
}
// Return a controller API that allows the messages to
// be repopulated.
return({
// I set the messages to be displayed.
setMessages: function( messages ){
// Update the message list.
updateList( messages );
}
})
})( sandbox, $( "ul.messages" ) );
</script>
</body>
</html>
As you can see, I'm using a combination of Pub/Sub and direct invocation. When a module needs to interact with the application, it can either trigger an event (as with the Form submission) or use other Sandbox methods (not shown in this demo). When the application needs to interact with a specific module, however, it relies on the module's API rather than using event-based invocation. This allows the application to filter / moderate / augment the data that is being piped into the target modules.
If I wanted to go back to a more strictly event-based architecture, I suppose that I could move some of the application logic into the individual modules. So, for example, instead of thinking of the messages list as a "List" module, perhaps I could think of it as a "Limited List" module. Then, when it gets initialized, it could be given a sense of the max number of items to show. In this way, it could simply subscribe to the "message" event and filter its own list internally.
I'm sure there are other ways to go about this; like I said before, this is all relatively new to me and I'm definitely struggling to tie the concepts together in a clean mental model. All I know right now is that as I've attempted to decouple my modules, I've found myself with an application that doesn't have a good sense of itself. Hopefully, moving some logic into the "Application" and relying on direct invocation over event-based communication (when sensible) will help me over some hurdles.
Want to use code from this post? Check out the license.
Reader Comments
good article.
backbone.js solves all of this for me in a much more structured way - all the issues you describe are non issues for me with backbone.
have you looked into it?
@Luke,
I've read up on Backbone and Spine a bit in the JavaScript Web Applications book by Alex McCaw. They definitely seem like compelling frameworks, which I will hopefully start digging into very soon. I've been trying to knock around these ideas on my own a bit so that I can try to think deeply about them.
I'm feeling good that it sounds like this kind of stuff has been solved very well already.