Nicholas Zakas | October 20th, 2010
Loose Coupling vs. Tight Coupling
Before diving into the objects you'll need to create an application, it's important to take a step back and understand the importance of loose coupling between objects. Loosely-coupled objects are objects that have little or no direction knowledge of each other. Tightly-coupled objects have direction knowledge of each other. The reason why loose coupling is preferred over tight coupling is for maintainability.
When two objects have direct knowledge of each other, an explicit relationship is created wherein changes to either object affect the other one. The following is an example of tightly-coupled code:
The objects in this example are one possible implementation of a Twitter-like interface. There are two objects, StatusPoster and TimelineFilter, which interact with the main timeline. The problem is that each of those two objects has a direct reference to the Timeline object, creating a tightly-coupled relationship between the objects. This is a major problem when working in a moderately large code base as changes in one part of the code may have unintended consequences in other parts of the code. Here are a few scenarios where you've created maintenance challenges:
- You're refactoring Timeline and want to change the name of post() to postStatus().
- You want to reuse StatusPoster on another page that doesn't have Timeline.
- You want to change the name of Timeline to StatusMessages.
Compare this to a loosely coupled approach where the knowledge of other objects is limited. The goal is to allow changes to one object without requiring changes to another; this is the foundation of a truly maintainable code base.
Types of Objects
This is the foundation upon which your application is built. It may be a well-known library such as YUI or jQuery, or it may be your own custom-built library. Either way, the base library is comprised of objects that provide browser normalization and frequently, syntactic sugar, to make common or complex operations easier to execute. One important characteristic of the base library objects is that they allow for easy extension. Generally speaking, only these objects need to know which browser is being used, as their primary goal is to abstract away browser differences. The base library is application-neutral (has no knowledge of the web application as a whole).
Although it's possible to use more than one base library in an application, it's recommended to choose one for simplicity sake. The more base libraries present on the page, the more abstractions you'll need to build into the application architecture to ensure compatibility.
These are UI controls that are sometimes built on top of the base library. Widgets are also application-neutral and are responsible for managing visual interfaces to data such as tab controls and menus. Although widgets interact with the DOM to produce certain user experiences, they are more or less a data representation layer that doesn't need to know about the application as a whole. They are just passed the data they need to render and go about their business.
Widgets are often considered to be extensions of the base library in an overall architecture diagram. This is because widgets typically extend or inherit from base functionality in a library in order to function. Widgets frequently use the classical inheritance pattern to centralize around a common widget infrastructure.
Modules are also application-neutral, in that they only know how to do their job and don't really care about what goes on in other parts of the page. The key focus of a module is to ensure that the user can complete a task or series of tasks within the context of the application.
You may choose to create a base module type from which other modules can inherit, or simply implement each module as a separate object literal with a given interface. Either way, you should make sure that your inheritance chain is relatively shallow. These objects should be as light as possible, and burdening them with a deep prototype chain in which to search for functionality is both unnecessary and detrimental to the loose coupling of each module (you want as few dependencies as possible to ensure maximum portability and maintainability).
When a module needs to communicate or interact outside of its particular area, it must request permission to do so. The sandbox object is a module's view into the outside world, and serves to keep the module loosely-coupled. By limiting the module's direct knowledge of other objects to just one, it's easy to remove the module or move it to another page. You can think of the sandbox object as a security guard to prevent the module from doing things it shouldn't. There may be one sandbox per module, or one sandbox that all modules share. It's less important how many of these objects exist and more important that the abstraction it provides is solid.
For a given application (or group of applications), the sandbox should provide an API for the most common activities that a module is likely to do. These include, but are not limited to:
- Communication with other modules
- Making Ajax requests
- Retrieving the module's base DOM node
- Attaching/detaching event handlers
- Requests for extended capabilities via extensions
The sandbox object design is correct when modules don't seek to circumvent it to accomplish a task (whether or not the task is allowable, of course, is up to you).
Perhaps the most important concept to understand about the sandbox is that it doesn't implement any functionality at all. It is simply the module's interface to the application core. All methods on the sandbox should just hand off to the appropriate methods on the core.
At the center of the application is a single object called the core. The core's job is to manage the modules on the page, which includes:
- Module lifecycle (starting/stopping)
- Communication between modules
- Error handling
When a module requests something through a sandbox object, the request is marshaled to the application core for completion. The core should be the only part of the architecture that has direct communication with the base library, and likewise, the sandbox is the only part of the architecture that has direct communication of the core. Keeping these layers separate enables you to easily swap out just one layer with minimal impact on the others.
One stylistic note: the application core is usually a global object by necessity (you need someplace to register the modules and extensions). Try to ensure that this is the only global object that is introduced in the application.
Applying the basic architecture design discussed in this article to the earlier code example, you would first choose a base library upon which to build your web application. This example doesn't actually require a library, so it is omitted for simplicity purposes. The second step is to design the interface for the module. Again, for simplicity, this example uses a minimal interface as follows:
There are only two methods in this Module interface definition: init(), which is called when the module's lifecycle begins, and destroy(), which is called when the lifecycle is over. Only the application core will ever call these methods. The Sandbox interface is also kept simple for this example:
Each module is created by a creator function in the following format:
The creator function receives a sandbox object as its only argument and is expected to return an object representing the module. As defined in the interface, this object must have at least an init() method and a destroy() method. Using a function to create the object allows there to be multiple unique instances of any given module on the same page.
With these interfaces defined, you can build out a Core object to wire things up:
To start the application, you first register the modules needed and then start them, such as:
Going back to the previous example of a Twitter-like interface, you may have code that looks like this:
The code for the various module creators is as follows:
Note that amongst the module creators, there are no direct references to other modules on the page. Instead, each instance of a module simply notifies the system (via the sandbox.notify() method) that an action has occurred that might affect other modules on the page. Both the TimelineFilter and the StatusPoster take actions that affect the Timeline, so they send out notifications. Since the Timeline knows that it would like to react to those notifications, it listens (via sandbox.listen()) for those notifications and specifies a function to call when one is received. Keep in mind that notifications go back through the Core to be marshaled, meaning that the Core is implementing a mediator pattern.
If the TimelineFilter or StatusPoster are removed from the page, the code for the Timeline doesn't have to change at all. The notifications will never occur, but this won't cause an error in the Timeline. By depending only on the notifications for intermodule communication, you have effectively separated the modules from each other. The other, arguably more powerful part, of this solution is that no module owns any particular notification. You may decide to completely change the page such that TimelineFilter is removed and another control takes it place while performing the same action (filtering the timeline information). That new control can also send a notification of "timeline-filter-change" and the original Timeline object doesn't need to change in order to continue functioning as normal.
The code presented in this article is purposely incomplete, just containing enough information to illustrate the concepts. The reason is that there is likely not one implementation of this architecture that will satisfy everyone's requirements. What's presented here is a jumping off point to get you started on your design rather than a completely formed solution.
About the Author
Find Nicholas on:
- Blog: www.nczonline.net
- Twitter: @slicknet