5.2 Designing the ImageViewer Class

 <  Day Day Up  >  

Once you get comfortable with the syntax of your first object-oriented language, you'll inevitably realize that the challenge of OOP isn't mastering the syntax, it's designing each application's architecture. As an OOP programmer, you'll face design decisions daily: which classes will make up the application? How will those classes interrelate? What public methods and properties will they expose? What methods and properties must be kept internal? What will all these classes, methods , and properties be named?

Luckily, you won't have to face these questions alone. For years , the OOP community has been cataloging so-called design patterns , which describe common solutions to generalized, recurring design problems. We'll see how to apply design patterns to Flash in Part III of this book. Design patterns focus mainly on the interactions among multiple classes. For now, we face a simpler problem: how to design a single, standalone class.

We've already given our ImageViewer class a name and a general purpose. Believe it or not, that means a good deal of the design work has already been done. By giving the class a general purpose, we've already determined its responsibilities: loading and displaying an image. Likewise, from the inverse perspective, we've decided that those responsibilities don't belong in other classes. That is, in an application that uses the ImageViewer class, other classes won't attempt to load images on their own; instead, they'll instantiate ImageViewer instances and use those instances to load images. Thus, just by formulating the ImageViewer class's responsibilities, we've shaped how it interacts with other classes, which is an important aspect of any OOP application design. Determining a class's responsibilities necessarily determines, in part, how it fits into a larger structure.

With our ImageViewer class's name and purpose settled, we can now establish its detailed functional requirements. Here's a list of functionality that could be required of the ImageViewer class:

  • Load an image

  • Display an image

  • Crop an image to a particular rectangular "view region"

  • Display a border around the image

  • Display image load progress

  • Reposition the view region

  • Resize the view region

  • Pan (reposition) the image within the view region

  • Zoom (resize) the image within the view region

From that list, we'll select only what's absolutely required by the current situation, leaving everything else for later. Our approach follows the Extreme Programming rule that you should "never add functionality early" (see http://www.extremeprogramming.org/rules.html).

The minimum functionality required to simply get the ImageViewer class up and running is:

  • Load an image

  • Display an image

Let's start with that short list.

5.2.1 From Functional Requirements to Code

Our first step in moving from functional requirements to a completed class is to determine how the ImageViewer class will be used by other programmers. For example, what method would a programmer invoke to make an image appear on screen? How about loading an image? Are those two operations implemented as separate methods or as one method? After we've determined how the class should be used by other classes (whether by ourselves or another developer), we can then turn our attention to how it should work (i.e., we can implement the proposed methods). Of course, during implementation we're bound to encounter issues that affect the way the class is used and thus we'll modify our original design.

The set of public methods and properties exposed by a class is sometimes referred to as the class's API (application programming interface). Revision is a natural part of the development cycle, but ideally , the public API doesn't change even when the internal code is revised. The term refactoring means to modify the internal code of a program without modifying its external (apparent) behavior. For example, you might refactor a class to make its operation more efficient or to improve upon poor coding practices.


In our example, we're trying to fashion the ImageViewer class's API. Traditionally, the term "API" refers to the services provided by an entire library of classes, such as the Java API or the Windows API. However, in current common discussion, the term "API" is often used to describe functionality made publicly available by anything from a single class to a whole group of classes. A class's API is sometimes also referred to as its public interface , not to be confused with the graphical user interface (GUI), nor the interfaces we'll study in Chapter 8.

Recall that our ImageViewer class's first functional requirement is to load an image. That operation should be publicly accessible, meaning that code in any class should be able to tell an ImageViewer instance to load an image, perhaps multiple times in succession. The network location (URL) of the image to load must be supplied externally. In short, a programmer using an ImageViewer instance needs a "load" command, and the ImageViewer instance needs a URL from the programmer in order to carry out that command. Sounds like a good candidate for a method! We'll call the "load" command loadImage( ) . Here's the loadImage( ) method's basic signature:

 ImageViewer.loadImage(URL) 

The method name, loadImage , acts as the "verb" to describe the action performed. A method name should be highly comprehensible. Someone reading it in a program's source code should be able to deduce the purpose of the method call without reading copious comments. Well-named methods are effectively self-commenting .

If you're having trouble naming a method, the real cause may be that your method is trying to do too much. Try splitting the method into multiple methods or restructuring your class, particularly if your method name has the word "and" in it.


The loadImage( ) method takes one parameter, URL , which specifies the network location of the image to load. The URL parameter must be a string, and the method doesn't return any value. So the complete signature and return type for the method is:

 ImageViewer.loadImage(URL:String):Void 

But perhaps we've jumped ahead too quickly. Is a separate loadImage( ) method required at all? Maybe the URL of the image to load should simply be passed to the ImageViewer constructor, as in:

 var viewer:ImageViewer = new ImageViewer("someImage.jpg"); 

That's more concise than creating an ImageViewer instance first and then calling loadImage( ) on it. But without a loadImage( ) method, each ImageViewer instance can load only one image. If we want ImageViewer instances to load multiple images in succession, we need a loadImage( ) method. Maybe we should implement both the loadImage( ) method and a constructor parameter URL but make the constructor URL optional. That's an interesting possibility, but it's not strictly required by our current situation. Given that we can easily add the constructor parameter later without affecting our design, we can safely defer the parameter for now. We should record these decisions as part of our class design rationale, either in a formal specification document or simply with comments in the class source code. As you design and implement your classes, be sure to document potential future features under consideration. You should also document features that you've rejected, rather than merely deferred, due to some design limitation. This is particularly true of potential features that have nonobvious drawbacks or limitations. This will help you remember your exact reasons for rejecting the design decision the next time you or someone else revises the code.

Now let's move on to our second functional requirement ”to display an image. Displaying an image on screen in the Flash Player necessarily involves at least one movie clip (the one into which the image is loaded). But which movie clip? Should we provide a way to specify an existing clip as the image-holding clip? We could write a setImageClip( ) method to be invoked before loadImage( ) , as follows:

 var viewer:ImageViewer = new ImageViewer( ); viewer.setImageClip(someClip_mc); viewer.loadImage("someImage.jpg"); 

That would work but might also interfere with the content already in the specified clip. For example, if an ImageViewer instance attempted to load an image into the main timeline of _level0 , the entire contents of the Flash Player would be replaced by the image! Not good. Furthermore, if setImageClip( ) were used to change the image-holding clip after an image had already been loaded, the ImageViewer instance would lose its reference to the original clip. Having lost the clip reference, the ImageViewer instance would no longer be able to position, size , or otherwise control the image, nor would it be able to remove the image before loading a different one.

To keep things simple (always keep it simple in the first version!), we want to guarantee a one-to-one association (one image movie clip for each ImageViewer instance). Hence, each instance will create a clip to hold the image. This ensures that the clip is empty when we load the image into it and that each instance always loads its image(s) into the same clip. In ActionScript, each movie clip can load only one image at a time.

We've decided that each ImageViewer instance will create its own image-holding clip, but we still need to know where to put that clip. That is, we need to be told which existing clip will contain the image-holding clip we create. How about adding a public method, setTargetClip( ) , that specifies the movie clip and depth on which to put our image-holding clip. Code that uses the ImageViewer would look like this:

 var viewer:ImageViewer = new ImageViewer( ); //   someClip_mc   is the clip that will contain the new image-holding // clip, and 1 is the depth on which the image-holding clip is created. viewer.setTargetClip(someClip_mc, 1); viewer.loadImage("someImage.jpg"); 

Hmm. That feels a bit cumbersome. It shouldn't take two method calls just to load an image. Moreover, we want each ImageViewer instance to know its target clip immediately on creation. So let's move the responsibility of setting the target clip to the ImageViewer constructor. Anyone creating a new ImageViewer instance must provide the target clip and depth as arguments to the constructor, so here is the new constructor signature:

 ImageViewer(target:MovieClip, depth:Number) 

Code that uses the ImageViewer would now look like this:

 var viewer:ImageViewer = new ImageViewer(someClip_mc, 1); viewer.loadImage("someImage.jpg"); 

That code would create the ImageViewer instance, which would create a new empty movie clip in someClip_mc at depth 1. It would then load someImage.jpg into the empty clip. Once loaded, someImage.jpg would automatically appear on screen ( assuming someClip_mc were visible at the time).

The signatures and return datatypes of the constructor and loadImage( ) method in our current class design now look like this (recall that constructor declarations never include a return datatype):

 ImageViewer(target:MovieClip, depth:Number) ImageViewer.loadImage(URL:String):Void 

That looks reasonably sensible . Only one way to find out: let's code it up.

 <  Day Day Up  >  


Essential ActionScript 2.0
Essential ActionScript 2.0
ISBN: 0596006527
EAN: 2147483647
Year: 2004
Pages: 177
Authors: Colin Moock

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net