Software Secret Weapons™
|
Javascript Refactoring For Safer Faster Better AJAX by Pavel Simakov on 2007-05-07 16:11:11 under AJAX, view comments |
|||||
|
IntroductionJavaScript is a very powerful and widely used programming language. It can be found today on almost any web site, even in the most plain-looking text-only pages. JavaScript code validates form fields, handles events, constructs urls, controls browser and manipulates page content and styles. It even communicates with dedicated servers in real-time to receive fresh XML data.
The problemQuite often I run into development teams with a lot of JavaScript that suffers from poor code design and desperately needs refactoring. Refactoring is the gradual modification of a software system to improve its internal logical structure, but mostly preserve its functionality. The definitive guide to refactoring by Martin Fowler {3} and handbook to improving general structure of code by Steve McConnell {4} have in existence there for several years, focusing on compiled languages. But JavaScript is a dynamically-typed functional language that is typically used without compilation. Nevertheless, an interpreter and the dynamic nature of JavaScript does not excuse JavaScript developers from not following the best practices of software engineering. Used properly, JavaScript can provide a competitive advantage over statically typed languages like C++, Java or C#.
What are the common problems, or anti-patterns found in JavaScript code? One of the biggest problems is left-and-right use of global variables and functions. The biggest evil of global variables are unexpected side-effects when you make code changes. Global variables frequently lead to further design problems and can host a huge number of bugs. They make development, testing, support and troubleshooting a nightmare and must be eliminated. Good code design that avoids global variables and makes code modular is the only path to reliable interactive web-applications.
The code is quite simple and works fine on both Firefox 1.0+ and IE 6.0+. If you are generally familiar with JavaScript and have used AJAX, a quick look at the source is quite sufficient for our discussion. Otherwise, review Listing 1 or other sources to learn more about JavaScript prototype-based objects and public and private access to object variables and functions.
// declaration function myClass(){ // variables var privateVar = "I am private"; this.publicVar = "I am public"; // functions var privateFunction = function(){ alert("I am private"); } this.publicFunction = function(){ alert("I am public"); } } // usage var myInstance = new myClass(); myInstance.publicVar = "new value"; alert(myInstance.privateVar); // undefined! myInstance.publicFunction(); myInstance.privateFunction(); // error!
What is slightly harder to see is that several functions in this example, make heavy use of these global variables. These functions are:
Such dependency of functions on global variables makes these functions themselves global and not reusable in other contexts or for other purposes. For example, as your web application matures, you might want to handle several XMLHttpRequests concurrently. But it won't work. All concurrent requests will use the same req and ready variables resulting in the race conditions and errors. Or you might want to reuse the above JavaScript code in another similar project. It won't work either. Someone with a lot of natural creativity and perseverance will just duplicate all the code to overcome the design problems. Thus illustrating a classic example of bad initial design forcing further compromises and poor overall product quality.
var req; var ready; function loadXMLDoc(url) { function processStateChange() { processXML() {
The solutionThe good news is in many cases refactoring is quite easy to do. Let's start with the list of general design rules and goals for the new version.
There are several large goals here. The attempt to achieve all of them might strike you as a major application rewrite, rather than just refactoring. Simply consider the rest of the source code involved in your large application and you will realize that we are only focusing on a small piece of a big puzzle.
function oyXMLRPCProvider(){ var req; var ready; var loadXMLDoc = function (url) { var processStateChange = function () { var processXML = function () {
function oyXMLRPCProvider(){ var req; var ready; this.loadXMLDoc = function (url) { var processStateChange = function () { this.onComplete = function () {
function oyXMLRPCProvider(){ // place here all code from previous step var oThis = this; // checks to see if we have too many messages in log
function oyXMLRPCProvider(){ // place here all code from previous step var internalIsBusy = function(){ return inProgress && !isComplete; } this.isBusy = function(){ return internalIsBusy(); } this.getVersion = function(){ return "1.0.0"; } this.isSupported = function(){ var nonEI = window.XMLHttpRequest; var onIE = window.ActiveXObject; if (onIE) { onIE = new ActiveXObject("Microsoft.XMLHTTP") != null; } return window.XMLHttpRequest || onIE; } this.getUrl = function(){ return url; } this.getStatus = function(){ return status; } }
The source for a new version of web page and JavaScript can be found in new/test.htm and new/oyXMLRPC.js respectively. All design goals are met, and the new code is easily integrated back into the complete system. Now we are ready for production!
Prototypes instead of closuresWhile very convenient for the refactoring, using closures might be inefficient from the performance and memory usage perspective. When instance methods are added as closures in the constructor a new closure is instantiated for each object instance. This overhead should not be a problem unless you plan to create thousands of object instances. This is not likely for oyXMLRPCProvider, but maybe true for some other classes. In such cases it is better to extend objects by using JavaScript object prototypes.
By adding a prototype to an object, we can extend all instances of an object at the same time. It takes only minutes for me to rewrite an implementation of oyXMLRPCProvider to work off prototypes. Simply convert every closure this.methodFoo = function() {} into a prototype method oyXMLRPCProvider.prototype.methodFoo = function (){} and you are done. Also change all references to local variables and functions to starts with "this".
The only thin place here is in assigning object method to this.req.onreadystatechange. You have to really understand JavaScript scoping rules as they apply to the closures and prototypes. The trick here is to declare a new local variable to hold "this" value, and to wrap an object callback method into a closure. The source for a new version of web page and JavaScript can be found in new/test.htm and new/oyXMLRPC.js respectively.
Final wordNone of the code snippets, templates, prototypes, tools, libraries, or frameworks available out there can eliminate a need for good code design. Even if all of those libraries were written perfectly without bugs or design flaws, there would still be code that you would have to write on top of these libraries. To have solid control of your own code, stop treating it as a "toy" and continuously invest time in following basic software design principles. As you can see from examples in this article, significant improvements to code can be made with relatively small effort. After all, there is no need to find the best design that follows all the patterns out there. Just find an adequate design that provides basic modularity and code organization. Do not forget other parts of your software system that do not use JavaScript, they will benefit from good design too!
References
Version Control
Finding the right web host can be hard, which providers dedicated hosting as well as wireless broadband connection. Comments (6) Leave a comment |
|
|||||
|
Copyright © 2004-2010 by Pavel Simakov any conclusions, recommendations, ideas, thoughts or the source code presented on this site are my own and do not reflect a official opinion of my current or past employers, partners or clients |
|
Comment by Gabriel Sroka — September 13, 2007 @ 12:32 am
Pavel,
In internalRequestComplete(), you define
var STATE_COMPLETED = 4;
var STATUS_200 = 200;
which is great. I’ve seen dozens of AJAX frameworks that use magic numbers without any definitions.
I’ve gone one or two steps further. I’ve defined “enum” type objects, such as
var State = {loading: 3, done: 4};
var Status = {ok: 200, notFound: 404};
so you can use
if (req.readyState == State.done) {
and
if (status == Status.ok) {
Also, I always add comments with references to URLs. For example, the State values come from
http://www.w3.org/TR/XMLHttpRequest/#xmlhttprequest
and Status.ok=200 comes from
http://tools.ietf.org/html/rfc2616
Comment by Ramar — February 4, 2008 @ 6:35 am
i want to software for working javascript
Comment by frank kootte — February 17, 2008 @ 3:34 pm
Your example describing private and public variables and functions is incorrect.
The following is a correct example:
var func = function ()
{
var private = ”privateString;
var private2 = function(){alert(‘privateFunction’);}
this.priviliged = ‘priviligedString’
var priviliged2 = function(){alert(‘priviligedFunction’);}
}
func.prototype.public = ‘publicString’;
func.prototype.public2 = function(){alert(‘publicFunction’);}
Comment by frank kootte — February 17, 2008 @ 3:35 pm
var priviliged2 = function(){alert(’priviligedFunction’);}
should be
this.priviliged2 = function(){alert(’priviligedFunction’);}
sorry.
Comment by jim groen — March 31, 2008 @ 10:47 am
Found memory leak in IE7
Add this.req = null;
Change:
if (this.status == STATUS_200) {
this.internalOnLog(“internalRequestComplete: calling callback on content with length ” + this.req.responseText.length + ” chars”);
if(this.onComplete) {
this.onComplete(this.req.responseText, this.req.responseXML);
}
this.internalOnLog(“internalRequestComplete: complete on ” + new Date());
this.req = null; // Fix IE memory leak / garbage collection
}
Comment by kailash thakur — November 8, 2008 @ 5:44 am
i want a software of wepon desgining