`

[转]Managing JavaScript Objects

阅读更多

Mark一下,等有空了再好好读一下

 

原文地址:http://msdn.microsoft.com/en-us/scriptjunkie/gg314983.aspx

 

 

You've read all the blog posts and books, and have sat through Douglas Crockford's hours of lectures, so you now know all there is to know about JavaScript objects. You're familiar with the various forms of inheritance, how prototypes work, and scoping is a topic you can discuss easily. The remaining question is one of applicability: how do you organize the objects in your application to take advantage of all you know?

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:

  1. TimelineFilter = {
  2.     changeFilter: function (filter){
  3.         Timeline.applyFilter(filter);
  4.     }
  5. } ;
  6.  
  7. StatusPoster = {
  8.     postStatus: function (status){
  9.         Timeline.post(status);
  10.     }
  11. } ;
  12.  
  13. Timeline = {
  14.     applyFilter: function (filter){
  15.         //implementation
  16.     } ,
  17.     post: function (status){
  18.         //implementation
  19.     }
  20. } ;

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.

In each of these instances, you're required to change code not just related to one object, but also related to others. Now imagine if there were dozens of objects all doing the same thing. The architecture of your JavaScript becomes incredibly fragile and the potential for introducing bugs grows tremendously.

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

In the quest for a maintainable code base, it's very helpful to identify the types of objects you'll be creating and/or using. You can generally break down all of the objects in a properly designed JavaScript application into the following groups.

Base Library

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.

Widgets

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

A module is an independent unit of functionality on the page that is comprised of business logic and most often some UI. For this reason, it helps to think of modules as a combination of HTML, CSS, and JavaScript that represents a single part of the page. In the earlier code example, there were three modules: the StatusPoster, TimelineFilter, and Timeline.

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.

Modules have a lifecycle where they are created and initialized (attaching JavaScript events) and later are destroyed (cleaning up JavaScript). In order to stay loosely coupled, modules don't communicate with other modules directly. Modules also don't touch the DOM outside of their particular area and don't access non-native global objects (hopefully, there are not a lot of these). They may use one or more widgets to represent their UI.

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).

Sandbox

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.

Application 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
  • Extensions

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.

Example

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:

  1. interface Module {
  2.     void  init();
  3.     void  destroy();
  4. }

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:

  1. interface MessageInfo {
  2.     string type;
  3.     variant data;
  4. }
  5.  
  6. interface Sandbox {
  7.     void  notify(MessageInfo message);
  8.     void  listen(String  messageType, Function  handler, Object  scope);
  9.     void  listen(String [] messageType, Function  handler, Object  scope);
  10. }

This Sandbox interface primarily deals with intermodule communication (in a full implementation, you'd want more functionality). The notify() method is the one modules call to broadcast information throughout the system to other modules. The listen() method is used to listen for specific types of broadcasted information (similar in design to event handlers in JavaScript).

Each module is created by a creator function in the following format:

  1. function  (sandbox){
  2.     //return object
  3. }

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:

  1. var  Core = function (){
  2.     var  moduleData = { } ;
  3.  
  4.     return  {
  5.         register: function (moduleId, creator){
  6.             moduleData[moduleId] = {
  7.                 creator: creator,
  8.                 instance: null
  9.             } ;
  10.         } ,
  11.  
  12.         start: function (moduleId){
  13.             moduleData[moduleId].instance =
  14.                 moduleData[moduleId].creator(new  Sandbox(this ));
  15.             moduleData[moduleId].instance.init();
  16.         } ,
  17.         
  18.         stop: function (moduleId){
  19.             var  data = moduleData[moduleId];
  20.             if  (data.instance){
  21.                 data.instance.destroy();
  22.                 data.instance = null;
  23.             }
  24.         } ,
  25.         
  26.         startAll: function (){
  27.             for  (var  moduleId in  moduleData){
  28.                 if  (moduleData.hasOwnProperty(moduleId)){
  29.                     this .start(moduleId);
  30.                 }
  31.             }
  32.         } ,
  33.         
  34.         stopAll: function (){
  35.             for  (var  moduleId in  moduleData){
  36.                 if  (moduleData.hasOwnProperty(moduleId)){
  37.                     this .stop(moduleId);
  38.                 }
  39.             }
  40.         }
  41.  
  42.         //other methods not shown
  43.     } ;
  44.  
  45. } ();

To start the application, you first register the modules needed and then start them, such as:

  1. Core.register("module_id"function (sandbox){
  2.  
  3.     return  {
  4.         init: function (){
  5.             //initialization
  6.         } ,
  7.  
  8.         destroy: function (){
  9.             //destruction
  10.         }
  11.  
  12.     } ;
  13.  
  14. } );

Going back to the previous example of a Twitter-like interface, you may have code that looks like this:

  1. //register all modules
  2. Core.register("timeline-filter" , TimelineFilterCreator);
  3. Core.register("status-poster" , StatusPosterCreator);
  4. Core.register("timeline" , TimelineCreator);
  5.  
  6. //start all modules
  7. Core.startAll();

The code for the various module creators is as follows:

  1. function  TimelineFilterCreator(sandbox){
  2.  
  3.     return  {
  4.         //assume init() and destroy()
  5.  
  6.        changeFilter: function (filter){
  7.            sandbox.notify({  type: "timeline-filter-change" , data: filter } );
  8.        }
  9.     } ;
  10. }
  11.  
  12. function  StatusPosterCreator(sandbox){
  13.  
  14.     return  {
  15.         //assume init() and destroy()
  16.  
  17.        postStatus: function (statusText){
  18.            sandbox.notify({  type: "new-status" , data: statusText } );
  19.        }
  20.     } ;
  21. }
  22.  
  23. function  TimelineCreator(sandbox){
  24.  
  25.     return  {
  26.  
  27.          //assume destroy()
  28.  
  29.          init: function (){
  30.             sandbox.listen(["timeline-filter-change""new-status" ],
  31.                 this .handleNotification, this );
  32.         } ,
  33.  
  34.         handleNotification: function (note){
  35.             switch (note.type){
  36.                 case  "timeline-filter-change" :
  37.                     this .applyFilter(note.data);
  38.                     break ;
  39.                 case  "new-status" :
  40.                     this .post(note.data);
  41.                     break ;
  42.             }
  43.         } ,
  44.  
  45.         applyFilter: function (filter){
  46.             //update filter
  47.         } ,
  48.  
  49.         post: function (status){
  50.             //post status
  51.         }
  52.  
  53. }

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.

Conclusion

Building a loosely coupled JavaScript architecture does require a little extra planning before beginning, but the advantages you receive by doing so are vast. Modules can be added, removed, and reused in multiple contexts, and changes to one module won't cause another to break. This approach really pays off on a team with multiple developers working on various parts of the page at the same time.

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.

If you'd like to learn more about this topic, please see my presentation, Scalable JavaScript Application Architecture .

分享到:
评论

相关推荐

    JavaScript: The Good Parts

    But, whether you're managing object libraries or just trying to get Ajax to run fast, Crockford's guidance in JavaScript: The Good Parts will help you create truly effective JavaScript code.

    Getting Started with Grunt: The JavaScript Task Runner

    The only requirement for this book is a basic understanding of objects and functions in JavaScript. Product Details Paperback: 132 pages Publisher: Packt Publishing (February 19, 2014) Language: ...

    WebAssembly: Accessing C and C++ in Web Applications

    20 Managing matrices in memory 21 Matrix operations example Analyzing WebAssembly Applications 22 Analyzing debug information 23 The WebAssembly text format 24 Advanced WebAssembly text format ...

    python3.6.5参考手册 chm

    The json module: JavaScript Object Notation The plistlib module: A Property-List Parser ctypes Enhancements Improved SSL Support Deprecations and Removals Build and C API Changes Port-Specific ...

    Beginning HTML5 Games with CreateJS

    Extending EaselJS DisplayObjects using object-oriented JavaScript JavaScript debugging Wrapping HTML5 games and publishing them to app store Who this book is for Beginning ...

    KnockoutJS Web Development(PACKT,2015)

    You will also learn how to use this feature for mapping multiple objects and managing them. This book provides an in-depth explanation of native templates, enhanced collection handling, and render ...

    精通jquery 2.0(2013年10月版)

    Chapter 4: JavaScript Primer 61 Chapter 5: jQuery Basics 93 Chapter 6: Managing the Element Selection 117 Chapter 7: Manipulating the DOM 145 Chapter 8: Manipulating Elements 177 Chapter 9: ...

    Professional Python Frameworks - Web 2.0 Programming with Django and TurboGears

    Managing User Identity 140 Contents ftoc.indd xvi 9/10/07 11:33:03 AM xvii User Identity Model Objects 140 Identity Configuration Options 143 Other Identity Framework Components 144 Using the ...

    Python Cookbook英文版

    5.16 Managing Options 5.17 Implementing a Set Class 5.18 Implementing a Ring Buffer 5.19 Implementing a Collection 5.20 Delegating Messages to Multiple Objects 5.21 Implementing the ...

    CSharp 3.0 With the .NET Framework 3.5 Unleashed(english)

    Accessing Controls via JavaScript 657 Calling Web Services with ASP.NET AJAX 664 Summary 669 29 Crafting Rich Web Applications with Silverlight 670 What Makes Silverlight Tick? 670 Starting...

    Joomla! 1.5 Development Cookbook.pdf

    Adding JavaScript to a page 155 Creating a modal window 157 Generating modal content 160 Updating an element using Ajax and MooTools 161 Updating an element based on a form using Ajax and MooTools...

    jdk-9.0.1_doc-all 最新版

    Defines the API for the JavaScript Object. jdk.jstatd Defines the jstatd tool for starting a daemon for the jstat tool to monitor JVM statistics remotely. jdk.localedata Provides the locale data for...

    [Mastering.Node.js(2013.11) 精通Node.js

    Extending JavaScript 9 Events 10 Modularity 12 The Network 13 V8 15 Memory and other limits 16 Harmony 18 The process object 19 The Read-Eval-Print Loop and executing a Node program 21 Summary 23 ...

    Unity.in.Action.Multiplatform.Game.Development.in.Csharp

    file and a test web page 282 ■ Communicating with JavaScript in the browser 283 Adjusting Player Settings: 280 ■ Platform-dependent 282 ■ Building the Unity CONTENTS xiii 12.3 Building for mobile ...

    精通jQuery(2012年版)

    Chapter 4: JavaScript Primer ...............................................................................67 Part 2: Working with jQuery ...............................................................

    php.ini-development

    Turning on this setting and managing its maximum buffer size can yield some ; interesting side-effects depending on your application and web server. ; You may be able to send headers and cookies ...

Global site tag (gtag.js) - Google Analytics