Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Ajax In Action (2006).pdf
Скачиваний:
63
Добавлен:
17.08.2013
Размер:
8.36 Mб
Скачать

Some small refactoring case studies

77

 

 

3.2 Some small refactoring case studies

The following sections address some issues in Ajax development and look at some common solutions to them. In each case, we’ll show you how to refactor to ease the pain associated with that issue, and then we’ll identify the elements of the solution that can be reused elsewhere.

In keeping with an honorable tradition in design patterns literature, we will present each issue in terms of a problem, the technical solution, and then a discussion of the larger issues involved.

3.2.1Cross-browser inconsistencies: Façade and Adapter patterns

If you ask any web developers—be they coders, designers, graphics artists, or all- rounders—for their pet peeves in relation to their work, there’s a good chance that getting their work to display correctly on different browsers will be on their list. The Web is full of standards for technology, and most browser vendors implement most of the standards more or less completely most of the time. Sometimes the standards are vague and open to different interpretations, sometimes the browser vendors extended the standards in useful but inconsistent ways, and sometimes the browsers just have good old-fashioned bugs in them.

JavaScript coders have resorted since the early days to checking in their code which browser they’re using or to testing whether or not an object exists. Let’s take a very simple example.

Working with DOM elements

As we discussed in chapter 2, a web page is exposed to JavaScript through the Document Object Model (DOM), a tree-like structure whose elements correspond to the tags of an HTML document. When manipulating a DOM tree programmatically, it is quite common to want to find out an element’s position on the page. Unfortunately, browser vendors have provided various nonstandard methods for doing so over the years, making it difficult to write fail-safe crossbrowser code to accomplish the task. Listing 3.2—a simplified version of a function from Mike Foster’s x library (see section 3.5)—shows a comprehensive way of discovering the pixel position of the left edge of the DOM element e passed in as an argument.

78CHAPTER 3

Introducing order to Ajax

Listing 3.2 getLeft() function

function getLeft(e){ if(!(e=xGetElementById(e))){

return 0;

}

var css=xDef(e.style);

if (css && xStr(e.style.left)) { iX=parseInt(e.style.left); if(isNaN(iX)) iX=0;

}else if(css && xDef(e.style.pixelLeft)) { iX=e.style.pixelLeft;

}

return iX;

}

Different browsers offer many ways of determining the position of the node via the style array that we encountered in chapter 2. The W3C CSS2 standard supports a property called style.left, defined as a string describing value and units, such as 100px. Units other than pixels may be supported. style.pixelLeft, in contrast, is numeric and assumes all values to be measured in pixels. pixelLeft is supported only in Microsoft Internet Explorer. The getLeft() method discussed here first checks that CSS is supported and then tests both values, trying the W3C standard first. If no values are found, then a value of zero is returned by default. Note that we don’t explicitly check for browser names or versions but use the more robust object-detection technique that we discussed in chapter 2.

Writing functions like these to accommodate cross-browser peculiarities is a tedious business, but once it is done, the developer can get on with developing the application without having to worry about these issues. And with well-tested libraries such as x, most of the hard work has already been done for us. Having a reliable adapter function for discovering the on-page position of a DOM element can speed up the development of an Ajax user interface considerably.

Making requests to the server

We’ve already come across another similar cross-browser incompatibility in chapter 2. Browser vendors have provided nonstandard mechanisms for obtaining the XMLHttpRequest object used to make asynchronous requests to the server. When we wanted to load an XML document from the server, we needed to figure out which of the possibilities to use.

Internet Explorer will only deliver the goods if we ask for an ActiveX component, whereas Mozilla and Safari will play nice if we ask for a native built-in object. Only the XML loading code itself knew about those differences. Once the

net.Content-

Some small refactoring case studies

79

 

 

XMLHttpRequest object was returned into the rest of the code, it behaved identically in both cases. Calling code doesn’t need to understand either the ActiveX or the native object subsystem; it only needs to understand the

Loader() constructor.

The Façade pattern

For both getLeft() and new net.ContentLoader(), the code that does the object detection is ugly and tedious. By defining a function to hide it from the rest of our code, we are making the rest of the code easier to read and isolating the objectdetection code in a single place. This is a basic principle in refactoring—don’t repeat yourself, often abbreviated to DRY. If we discover an edge case that our object-detection code doesn’t handle properly, then fixing it once rolls that change out to all calls to discover the left coordinate of a DOM element, create an XML Request object, or whatever else we are trying to do.

In the language of design patterns, we are using a pattern known as Façade. Façade is a pattern used to provide a common access point to different implementations of a service or piece of functionality. The XMLHttpRequest object, for example, offers a useful service, and our application doesn’t really care how it is delivered as long as it works (figure 3.1).

In many cases, we also want to simplify access to a subsystem. In the case of getting the left-edge coordinate of a DOM element, for example, the CSS spec provided us with a plethora of choices, allowing the value to be specified in pixels, points, ems, and other units. This freedom of expression may be more than we need. The getLeft() function in listing 3.2 will work as long as we are using pixels as the unit throughout our layout system. Simplifying the subsystem in this way is another feature of the Façade pattern.

The Adapter pattern

A closely related pattern is Adapter. In Adapter, we also work with two subsystems that perform the same function, such as the Microsoft and Mozilla approaches to getting an XMLHttpRequest object. Rather than constructing a new Façade for each to use, as we did earlier, we provide an extra layer over one of the subsystems that presents the same API as the other subsystem. This layer is known as the Adapter. The Sarissa XML library for Ajax, which we will discuss in section 3.5.1, uses the Adapter pattern to make Internet Explorer’s ActiveX control look like the Mozilla built-in XMLHttpRequest. Both approaches are valid and can help to integrate legacy or third-party code (including the browsers themselves) into your Ajax project.

80CHAPTER 3

Introducing order to Ajax

Calling code

loadXML() function

Implicit XMLHttpRequest interface

Implicit XMLHttpRequest interface

Native XMLHttpRequest

ActiveX XMLHttpRequest

Figure 3.1 Schematic of the Façade pattern, as it relates to the XMLHttpRequest object across browsers. The loadXML() function requires an XMLHttpRequest object, but doesn't care about its actual implementation. Underlying implementations may offer considerably more complex HTTP Request semantics, but both are simplified here to provide the basic functionality required by the calling function.

Let’s move on to the next case study, in which we consider issues with JavaScript’s event-handling model.

3.2.2Managing event handlers: Observer pattern

We can’t write very much Ajax code without coming across event-based programming techniques. JavaScript user interfaces are heavily event-driven, and the introduction of asynchronous requests with Ajax adds a further set of callbacks and events for our application to deal with. In a relatively simple application, an event such as a mouse click or the arrival of data from the server can be handled by a single function. As an application grows in size and complexity, though, we may want to notify several distinct subsystems and even to expose a mechanism whereby interested parties can sign themselves up for such notification. Let’s explore an example to see what the issues are.

Some small refactoring case studies

81

 

 

Using multiple event handlers

It’s common practice when scripting DOM nodes using JavaScript to define the script in the window.onload function, which is executed after the page (and therefore the DOM tree) is fully loaded. Let’s say that we have a DOM element on our page that will display dynamically generated data fetched from the server at regular intervals once the page is loaded. The JavaScript that coordinates the data fetching and the display needs a reference to the DOM node, so it gets it by defining a window.onload event:

window.onload=function(){

displayDiv=document.getElementById('display');

}

All well and good. Let’s say that we now want to add a second visual display that provides alerts from a news feed, for example (see chapter 13 if you’re interested in implementing this functionality). The code that controls the news feed display also needs to grab references to some DOM elements on startup. So it defines a window.onload event handler, too:

window.onload=function(){

feedDiv=document.getElementById('feeds');

}

We test both sets of code on separate pages and find them both to work fine. When we put them together, the second window.onload function overwrites the first, and the data feed fails to display and starts to generate JavaScript errors. The problem lies in the fact that the window object allows only a single onload function to be attached to it.

Limitations of a composite event handler

Our second event handler overrides the first one. We can get around this by writing a single composite function:

window.onload=function(){

displayDiv=document.getElementById('display');

feedDiv=document.getElementById('feeds');

}

This works for our current example, but it tangles together code from the data display and the news feed viewer, which are otherwise unrelated to each other. If we were dealing with 10 or 20 systems rather than 2, and each needed to get references to several DOM elements, then a composite event handler like this would become hard to maintain. Swapping individual components in and out would become difficult and error prone, leading to exactly the sort of situation that we

82CHAPTER 3

Introducing order to Ajax

described in the introduction, where nobody wants to touch the code in case it should break. Let’s try to refactor a little further, by defining a loader function for each subsystem:

window.onload=function(){

getDisplayElements();

getFeedElements();

}

function getDisplayElements(){ displayDiv=document.getElementById('display');

}

function getFeedElements(){ feedDiv=document.getElementById('feeds');

}

This introduces some clarity, reducing our composite window.onload() to a single line for each subsystem, but the composite function is still a weak point in the design and is likely to cause us trouble. In the following section, we’ll examine a slightly more complex but more scalable solution to the problem.

The Observer pattern

It can be helpful sometimes to ask where the responsibility for an action lies. The composite function approach places responsibility for getting the references to DOM elements on the window object, which then has to know which subsystems are present in the current page. Ideally, each subsystem should be responsible for acquiring its own references. That way, if it is present on a page, it will get them, and if it isn’t present, it won’t.

To set the division of responsibility straight, we can allow systems to register for notification of the onload event happening by passing a function to call when the window.onload event is fired. Here’s a simple implementation:

window.onloadListeners=new Array(); window.addOnLoadListener(listener){

window.onloadListeners[window.onloadListeners.length]=listener;

}

When the window is fully loaded, then the window object need only iterate through its array of listeners and call each one in turn:

window.onload=function(){

for(var i=0;i<window.onloadListeners.length;i++){ var func=window.onlloadListeners[i]; func.call();

}

}