Chapter 1. Designing Applications

<feature><title>In This Chapter</title> </feature>

One of the most frequent questions ActionScript developers ask is, “How do I know what constitutes a class?” This question strikes at the heart of a larger dilemma, which is: What are the steps for building a successful application from start to finish—from concept to completion? This is a big topic to tackle. Many people dedicate themselves to understanding and improving methodologies to answer this question.

The difficulty with teaching someone how to design and build an application from start to finish is that it requires elements that are difficult to talk about much less teach. It requires being able and willing to look at the big picture as well as looking at things from many perspectives. It requires creative thinking as well as abstraction. It requires practice and experience. But there are steps you can follow to help with the learning process. There are technologies you can use to assist you in developing your ActionScript classes. This chapter outlines some of the steps and technologies that have proven useful for many ActionScript developers.

Some methodologies say there are five steps for building applications; other methodologies say there are eight steps; still others can’t decide how many steps it takes. In general, most developers agree that there are at least three phases to building successful applications:

  1. Analysis

  2. Design

  3. Implementation

In addition, most developers also agree that testing is a vital part of the application development process. Although not always considered a core phase we’ll also look at testing as a fourth important phase.

As we look at each of these phases, remember that they are not necessarily linear. You can go back to an earlier step at any point if necessary. During the design phase, for example, you might realize that you forgot about an important use case for your application. At that point, you can return to the analysis phase. However, you should be as thorough as possible at each step. Don’t jump to the design phase too early just because you can. The more thorough and complete you are with each phase before moving to the next, the more successful your application is likely to be. Additionally, thoroughness at each phase helps minimize the risk that you’ll have to make major architectural changes later on, which could severely impact schedules and project success.

The Analysis Phase

The analysis phase is concerned exclusively with what the application is supposed to do. The question of how the application will accomplish the goal is deferred to the design and implementation phases. In many ways, the analysis phase can be the most challenging because it requires that you take (often vague) ideas and translate them into specific functional requirements. You must create a map of what the application looks like from a distance. Although you can get away with a minimal analysis phase for a small project, the analysis phase becomes increasingly important for a project’s success as the project increases in size and scope. Although you might be able to walk around your neighborhood without a map, if you wanted to cross the country, you’ll undoubtedly agree that you need a map. This is true of application development as well.

All too often, the analysis phase is glanced over or deemphasized. Poor analysis leads to frustration for all parties involved (the developers who have to constantly make guesses and refector, the managers who have the responsibility to see the project through to a successful completion, the client who wants the working application, customers that have to use the application that may suffer from limited feature sets and bugs due to poor analysis, etc.). The goal of analysis is to provide a clear specification that outlines the needs of the user. Unlike later phases, the analysis phase should be as non-technical as possible.

The outcome of the analysis phase is generally a document that outlines the functional requirements. However, it’s important to understand that there are many ways to approach gathering these requirements, and the resultant document has no one required format. What is most important is that you, your team, and/or your company uses an approach and document format that works best for you while still achieving the goal of clearly defining this map for the application you want to build.

Although there’s no one required approach or format, we’ll present one common approach to analysis using use cases. If you are new to the idea of doing formalized analysis then you may find it useful to try using use cases. We also encourage you to research other techniques and document formats to find what works best for you.

Introducing Use Cases

One way to define the functional requirements of an application is simply to list everything that the application should be able to do. Although that approach is not necessarily wrong, it is naïve in that it fails to take into account the real-world use of the application. Applications don’t exist in isolation; they interface with all sorts of users. Therefore, it’s much more realistic and useful to approach the functional requirements from the standpoint of how the application is used. This approach naturally leads to a kind of functional requirement called use cases.

Use cases present the application requirements by showing various ways in which users might interact with the application. The following is an example of a simple use case:

  • Generate Map: The user submits a form with a street address. The system displays a physical map of the street address, with the map zoomed in at the default level.

Use cases can be formatted in many ways. Generally, use case experts talk about three basic formats.

Brief: One paragraph outlining the main success scenario. The preceding example was in the brief format.

Casual: Multiple paragraphs outlining not only the main success scenario, but also alternative scenarios. The following is an example of a casual format use case:

  • Generate Map

    • Main success scenario: The user submits a form with a street address. The system displays a physical map of the street address, with the map zoomed in at the default level.

    • Alternative scenariosIf the address is invalid, the address form is redisplayed with an error message notifying the user why the operation failed.

      If the default zoom level is unavailable for the requested address, display a map at the greatest zoom level available for the location.

Formal: The most elaborate of the formats for a use case document. This format lists all the steps for the use case as well as supporting data such as actors and conditions. The formal use case is discussed in more detail in the next section.

Writing Formal Use Cases

Typically you’ll want to create formal use cases for a functional requirements document. In this section we’ll look at how to create a formal use case. A formal use case can include the following sections:

  • Primary actor: A description of the user who drives the operations outlined by the use case. The description of the primary actor can include things such as the role of the user (e.g. anonymous, basic, administrator, etc.) as well as characteristics of the user that may be relevant to how they interact with the application (e.g. age, disabilities, etc.)

  • Preconditions: Those conditions that must be met for the use case to proceed.

  • Main success scenario: A more granular, step-based description of the way the application works than is given in the basic or casual formats.

  • Alternative scenarios: More granular, step-based descriptions of the ways the application will handle alternative uses than are given in the casual format.

  • Special requirements: A list of requirements for the use case that don’t fit as part of the main or alternative scenarios.

  • Open issues: A list of notes including questions that must be answered to fully implement a solution for the use case.

The following is an example of a formal use case. Note that this example does not have any open issues.

  • Generate Map

    Primary actor: Customer

    Preconditions: Customer is already viewing the form that allows the user to specify an address and click a button to submit the form.

  • Main success scenario:

    1. Customer fills out address form.

    2. Customer submits address data.

    3. System requests map data from mapping service.

    4. System draws map at default zoom level.

  • Alternative Scenarios:

    3a.

    System detects invalid address format and redisplays form with error message.

    3b.

    Mapping service is unavailable and system displays error message.

    4a.

    Data is not available for default zoom level and system displays map at next highest available zoom level.

  • Special Requirements:

    This portion of the application must be accessible (508 compliant).

Now that we’ve had a chance to see the structure of a formal use case, we’ll next look at how to start writing these use cases for an application.

Forming Use Cases

Now that you’ve seen how to write a use case, it follows that you’ll want to know how to start forming these use cases. For example, what level of granularity is appropriate? Should you have ten uses cases or a hundred? The answer to these questions is subjective. There is no one correct set of use cases for an application. However, you will likely find the following guidelines to be helpful:

  1. Determine the types of users. An application can have many types of users. Each user will have different use cases. A simple example is one in which an application has a standard, anonymous user type and an administrative user type. The administrative user typically expects additional features that are not enabled for standard users. Your application might have additional tiers of users as well. For example, in addition to standard and administrative users, your application might have registered users who have access to features not available to standard users.

  2. Determine the basic goals each type of user can achieve. For example, all users might be able to generate maps, but only registered users can save maps. Additionally, only administrative users might be able to view the logs and analytics for the application.

  3. Fill out each use case with the appropriate sections.

  4. Evaluate the use cases. It’s important that you take your time with the use cases to make sure they are correct and appropriate before moving to the design phase. Getting the use cases correct helps ensure the best possible result of the design and implementation phases. It’s much easier to make changes to the use cases before you’ve designed or implemented the application than to revise them afterward and have to redesign and re-implement the application.

Using UML in Analysis

UML (Unified Modeling Language) is a language in common use for modeling applications. Although UML is perhaps most frequently used during the design phase (as we’ll see in the next section) it is not uncommon to use UML during analysis as well. One of the three parts of a system model in UML is what’s called the functional model. The functional model allows you to create use case diagrams, which can be very helpful. UML use case diagrams generally are not detailed enough to be used apart from written use cases. However, they are often a nice addition to written use cases as they provide a visual representation of the uses cases, actors, and systems. Figure 1.1 illustrates actors and uses cases for a common system, a store.

An example of use cases in UML.

Figure 1.1. An example of use cases in UML.

The Design Phase

After you’ve completed the analysis phase of an application, you have a map for what the application is supposed to do. However, that map is at such a high level that you cannot use it to begin writing code. The result of the analysis phase may be a map, but it doesn’t tell you how you’re going to get from point a to point b. For example, are you going to walk, drive, fly, or take the train? For that you need the next step, which we call the design phase.

In the design phase, you take the functional requirements documentation from the analysis phase and start to look at it from an architectural standpoint—looking to identify subsystems and eventually classes. During the design phase you’ll parse out the elements that should be written as classes. Then you determine the responsibilities for those classes as well as the relationships between the classes.

The goal of the design phase is to generate some sort of technical document that provides a blueprint of the application you intend to build, including all the specific subsystems and classes that you will use and the relationships between them. You should expect to use this technical document to help you break up the application development into individual tasks. You should also expect that the technical document clearly identifies dependencies and collaborations between classes.

As with the analysis phase, the design phase has no rule dictating what techniques and tools you must employ. There are many ways that different people approach the design phase, and we encourage you to find the one that works best for you. However, we have found that class responsibility and collaboration (CRC) cards are a technique that proves very helpful in the design phase. In the next section we’ll discuss CRC cards in more detail.

Introducing CRC Cards

CRC cards are a low-tech, yet very effective, way to determine exactly what classes you need to write, what those classes need to be able to do, and how those classes relate.

Typically, you’ll find that 3x5 or 4x6 lined index cards work best as CRC cards. At the top of the index card, write the name of the class. On the left side of the index card, list the responsibilities for the class. On the right side of the card, list the classes with which the class needs to collaborate to accomplish those responsibilities. Figure 1.2 illustrates the format for a CRC card.

The typical format for a CRC card.

Figure 1.2. The typical format for a CRC card.

CRC cards are useful because you can draw them up quickly and make changes just as quickly. Using CRC cards, you can rapidly map out the functionality of an application; when you decide to split a single class into two classes, combine two classes, or change a class name, you can do that with your CRC cards in a few seconds. You can also sit around a table with a team and work together on the cards.

Now that you know the format of CRC cards, you’ll undoubtedly have a few questions regarding how to decide what constitutes a class, what responsibilities are, how to know what classes are collaborators, and so forth. The next few sections address each of these questions.

Determining Classes

Deciding what constitutes a class is as much an art as it is a science. Just as every painter has different ideas about composition, use of color, and so on, so too does every application designer have different ideas about how to build an application. However, you’ll likely find certain guidelines helpful when you try to determine what classes your application needs.

It’s often a good idea to look at your use cases to find classes. Classes are nouns. You can scan use cases for all the significant nouns and use those as classes in your application. For example, consider the Generate Map use case we described earlier in this chapter. From that use case we can easily identify these relevant nouns which are natural candidates for classes: “address form,” “address data,” “mapping service,” “map data,” and “map.”

When you have selected all the candidates for classes, write them down on your CRC index cards. The next step is to determine the responsibilities for each class.

Determining Class Responsibilities

After you’ve decided on the initial candidates for classes, you can assign responsibilities to those classes. Assigning responsibilities is an important step because it helps you determine the viability of the class candidate. If a class candidate doesn’t have any responsibilities, it must be unnecessary, and you can discard it. If the candidate seems to have too many responsibilities, it probably needs to be divided into two or more classes. There are some schools of thought that state that a class should have no more than one responsibility. While we respect that standpoint, we find it to be severe. A general rule of thumb that we use is that a class should have between one and three responsibilities.

It’s important to understand what a responsibility is (and what it is not). A responsibility is essentially what a class (or an instance of the class) should be able to do or facilitate. Although there is a relationship between a class’s methods and its responsibilities, they are not identical. You should not think of a class’s responsibilities in terms of methods or method names. A class may require many methods to accomplish just one responsibility. At this point in the design, it’s too early to map out the actual methods. Responsibilities are higher-level abstractions than methods.

A responsibility is usually something that can be written out in plain language in a few words. The following are examples of possible class responsibilities:

  • Create user input form

  • Validate user input

  • Encapsulate data model for a map

  • Handle requests and responses to and from server-side service

  • Draw vector map from data model

As you work on determining the responsibilities for the classes in your application, you will most likely drop classes, add classes, and change existing classes. These revisions are a desirable part of the process, which result in a well-considered design.

Although you can go through each class candidate and try to think of the responsibilities each class might have, that approach can be problematic. It encourages you to add responsibilities based on what you think the class candidate ought to do rather than based on what the application requires. A better approach is to scan the use cases for verbs—both explicit and implicit verbs. Explicit verbs are obvious because they are written in the use case steps. Implicit verbs are the verbs that are not written in the steps but are necessary for the successful completion of a step.

Determining Collaborators

Many, if not most, classes cannot fulfill all their responsibilities on their own. They must rely on other classes to assist them. The assisting classes are called collaborators. Collaborators generally lend a hand either by providing data or by enabling the class to offload functionality.

After you have defined classes and class responsibilities, the next step in the design phase is to determine what each class’s collaborators are. This is extremely helpful in finding additional classes that you hadn’t previously thought of. For example, consider a Map class whose responsibilities include drawing a vector map based on a data model. It might be immediately obvious that in such a case a MapData class would be a collaborator since Map would want to query MapData for the data needed to draw the map. Locating collaborators is useful for us in terms of determining relationships between existing classes. In this case, because we likely already have a CRC card for the MapData class – derived from the “map data” noun we spotted in the use cases – this collaborator did not help us find a new class. However, when we think about the Map class still more, we’ll probably realize that drawing all the different types of elements on a map would probably be far too much for the Map class itself to handle. Instead we can rely on collaborators that draw the specific map elements, and we realize that these collaborators become new classes we missed before: Street, Highway, River, and CityMarker.

Elaborating on Relationships Between Classes

Classes have relationships with one another. When finding collaborating classes, you are finding the classes that have relationships. However, it’s possible and necessary to determine what type of relationship these collaborating classes have. Although every relationship between classes will be unique, it is possible to generalize those relationships into the following categories:

  • Association

  • Aggregation

  • Inheritance

Association and aggregation are types of relationships that can more generally be called composition. Later in this chapter (in the section titled, “Inheritance and Composition”), we’ll compare and contrast the generalized principals of composition and inheritance as they apply to implementation.

The Association Relationship

Association is the weakest of these relationships. Association relationships are also sometimes called dependency relationships. When two classes are related in this way, one of the classes relies on its collaborator to help with one or more of its responsibilities.

An example of an association relationship is the relationship between a Map and a MapData class. The Map class has a dependency on the MapData class. Without a MapData instance, a Map object wouldn’t be able to draw the map.

Associations are perhaps the most common sort of relationship between classes. You can think of associations as “uses” relationships, meaning that Map “uses” MapData.

The Aggregation Relationship

Aggregation is a stronger form of composition relationship than the association relationship. When classes are related by aggregation, the life cycles of the classes are linked. When classes are related by association, one class instance can be created or destroyed without necessarily affecting the other. However, when classes are related by aggregation, it implies that one class is the owner of the collaborator class. If the owner class is destroyed, so too are the aggregate collaborator classes.

An example of an aggregation relationship is that of the Map and Street classes. You can think of aggregations as “has a” relationships, meaning that Map “has a” Street. That doesn’t mean that all Street objects are owned by Map objects. But this relationship does state that Map objects can have Street objects, and when the Map object is destroyed, so too are the Street objects it owns.

The Inheritance Relationship

Inheritance is the strongest sort of relationship between classes. When a class inherits from an existing class, it initially looks exactly like the class from which it inherits. The entire interface and implementation (more on these topics in the next chapter) of the existing class (what we call the superclass or base class) are passed down to the new class (what we call the subclass.) The relationship is so strong between superclasses and subclasses that subclass instances can even stand in for superclass instances in many cases. Because of the strength of inheritance relationships we say that inheritance defines an “is a” relationship such that the subclass “is a” superclass.

Inheritance relationships allow you to create abstractions that are shared by many similar classes. For example, Street, Highway, River, and CityMarker are all types of map elements. If all the classes share common interfaces and implementations, these classes might have a lot of duplicate and redundant code. You can abstract that code by placing it into a new MapElement class. Street, Highway, River, and CityMarker can then all inherit from the MapElement class. They will automatically inherit the interface and implementation from MapElement, which will remove the need to repeat that code in each of the subclasses. It also means that you can begin to use polymorphism. Although we’ll talk about this topic in more detail in the next chapter, the idea behind polymorphism is that a more specific type can substitute for a more general type. In other words, the Map class can have an aggregation relationship with MapElement rather than having aggregation relationships with Street, Highway, River, and CityMarker. That distinction is very important because if you later wanted to add a Bridge class, you could simply define it such that it inherits from MapElement, and the Map object would automatically work with Bridge objects without your having to rewrite any of the Map code.

Although inheritance relationships are very powerful, they also tend to create very rigid relationships. Inheritance has its place and deserves credit for all that it can do. However, so much emphasis has been placed on inheritance relationships in many programming communities that it is often overused and misused. Inheritance relationships should generally be the least frequent type of relationships in your applications. Inheritance enables polymorphism, which is extremely valuable. However, inheritance is not the only way to enable polymorphism, as you’ll read in the next chapter. We’ll compare and contrast inheritance with composition relationships in the “Inheritance and Composition” section later in this chapter.

Formalizing Public APIs

By this point, you’ve decided on the classes your application requires as well as the responsibilities of each class, the class collaborators, and the relationships each class has with those collaborators. Although you might be anxious to start coding right now, there are still some steps to complete in the design phase.

The next step is to formalize the public APIs (Application Programming Interface, which means the public methods) of the classes.

Formalizing the API for a class is a matter of translating the responsibilities into method signatures. Not all responsibilities necessarily translate into public methods because some of what a class is responsible for might be private. For example, the AddressForm class might have a responsibility to validate user input. That is probably not something that translates into a public method. Rather, it is far more likely that this responsibility is handled internally by the class when the user clicks a button. However, some class responsibilities might translate into several public methods. For example, in the case of our map example, the responsibility “handle request and responses to and from server-side service” might translate into the following methods (depending on the application requirements):

function getMapDataForAddress(address:AddressData):void;
function getSavedMapData(id:uint):void;

Note

In the preceding example, the two methods are purely based on speculation as to what sorts of methods such an application might require for a server-side service proxy (often called a remote proxy). Furthermore, both methods are declared with void return types because the assumption is that the class is a proxy to a server-side service that works asynchronously with Flash Player, and responses will be handled by event listeners.

Using UML for Design

We first mentioned UML in relation to analysis. However, one of the most common uses of UML is during the design phase because you can use UML class diagrams to visually represent all the classes, their APIs, and the relationships between the classes. UML class diagrams are really useful because they allow you to look at all the classes and there relationships all at one time in a relatively succinct format. Usually a UML class diagram doesn’t replace the need for technical documentation. However, UML class diagrams can often supplement technical documentation and serve as a useful tool both during the design phase as well as during the implementation phase when you must actually write all the classes shown in a UML class diagram. Figure 1.3 shows a very simple UML class diagram that shows two classes and an interface.

A simple UML class diagram.

Figure 1.3. A simple UML class diagram.

Note that this figure shows only public class members, yet you can also represent private and protected members.

Not only does UML provide a nice way to visualize the classes used by an application, but it also provides the possibility to export stub code for all the necessary classes and interfaces. At the time of this writing there is no known ActionScript 3.0 stub code generator for UML. However, since this is a common feature for many other languages (Java, C#, etc.) it is reasonable to think that there will be an ActionScript 3.0 generator for UML in the near future.

The Implementation Phase

Following the design phase is the implementation phase. In the implementation phase, you actually write the code you have planned out. If you’ve had successful analysis and design phases, the implementation of your application should be relatively straightforward—simply a matter of coloring in the lines, so to speak. By the time you get to the implementation phase, you should already have decided on the classes, their relationships, their responsibilities, and their APIs.

Much of the implementation phase simply involves writing ActionScript code, and as the one step you can’t skip, it is the phase with which everyone is familiar. As such, we’re not going to focus on the details of how to write classes. However, there are several topics that bear further discussion, namely:

  • Coding conventions

  • Encapsulation

  • Composition and inheritance

  • Coupling

Coding Conventions

There are few rules for naming classes, packages, variables, functions, and interfaces in ActionScript. In each case, you can use only letters, numbers, dollar signs ($), and underscores (_) and the first character must not be a number. Although the rules are few, there are still conventions for naming that you might find useful. At the very least, you will find it useful to know what conventions we use in this book. You should know that the conventions we use aren’t the only conventions, and you aren’t obligated to use them. We introduce this topic here because consistent and conscious coding conventions are a boon to application development. By applying conventions consistently you can expect to write code that is easily read by you and anyone else during team development. Remember that classes can involve hundreds of lines of code, and using consistent conventions helps you to more quickly identify parts of the code and their purposes.

Variables and Functions

For variables, it is a convention to use initial lowercase letters. Consider this example:

var city:Map;

Generally, it is advisable to use as the name words and phrases that describe the variable. For example, city is probably a much better name for a Map variable than m would be. Often times, it’s possible to more accurately describe a variable using several words. In such cases, the convention is to use a style called camel case (sometimes called inter caps) in which the first letter of each word (except the first) is capitalized, as in this example:

var cityMap:Map;

Class properties are special sorts of variables, and as such they use the same naming convention as variables. However, to better distinguish between local variables and class properties, it is a convention to name all private properties with an initial underscore, as in this example:

private var _cityMap:Map;

Note

The issue of underscores for private properties is a contentious one among developers. It is our preference to use underscores as we feel they help clearly differentiate between private properties and local variables. However, some developers will argue vehemently against the use of underscores as they feel there is no significant benefit in their use.

Functions (and methods) also follow the same naming conventions as variables. Function names should start with lowercase letters and use camel case formatting when the function name consists of more than one word. Consider this example:

public function getMapDataForAddress(address:AddressData):void;

Parameters are also special variables, and as such they use the same naming conventions as variables, as you can see in the preceding example.

Unlike private properties it is not common to use underscores for private methods. The logic behind this is that a method is not generally defined within another method as a local variable might be defined within a method. Therefore, it’s always clear that a method is a method without having to use underscores.

Note

The variable and function/method naming conventions presented here are not intended to be comprehensive of all possible naming conventions. Many developers like to use additional conventions such as using variable prefixes to denote type. We are presenting the conventions that we find useful and that we use in this book. You are always welcome to use whatever conventions you find helpful.

Constants

Constants are special types of fields; you can define them with a value, but you cannot change the value subsequently. You’ve likely seen many constants in the Flash Player events API such as EVENT.COMPLETE and MOUSEEVENT.CLICK. As you can see, constants use all uppercase characters by convention. If a constant name uses more than one word, the words are delimited by an underscore, as in MouseEvent.MOUSE_MOVE.

Note

Constants are a new feature in ActionScript 3.0.

Classes and Interfaces

By convention, class names always start with an uppercase character. Class names also use camel case when necessary. In addition, class names should always be nouns.

Interfaces use the same naming conventions as classes except that they have one additional convention: Interface names always start with the letter I (meaning interface.) Additionally, interfaces do not always have to use nouns as names. Although it’s not uncommon to name an interface with a noun (e.g. ICollection) it’s equally common to use an adjective ending in -able. For example, the Flash Player API includes the following ActionScript 3.0 interfaces: IExternalizable and IBitmapDrawable.

Packages

For the most part, package names follow the same conventions as variables: They start with lowercase letters. There are two schools of thought regarding the use of camel case in package names. One group uses camel case while the other group uses exclusively lowercase characters in package names. In this book we do not employ camel case in package names.

There’s yet another important convention when it comes to package names. One of the functions of packages is to ensure that classes exist within unique namespaces. For example, two classes called Example cannot be created in the same package, but may exist in two separate packages. When you decide on package names, try to ensure that the package name guarantees uniqueness. That way, if you happen to use your Example class in a project with an Example class from an existing library, the two classes can coexist.

By convention, package names can guarantee uniqueness by using subpackages in order of descending order of specificity. When a class is part of a library belonging to a company or organization, the convention is to name the packages starting with the organization’s domain name in reverse order. The first part of most package names is the top-level domain such as com or org. The second part of most package names is the domain such as google or amazon. If the classes are specific to a project, the project name follows the company’s domain name. The classes themselves are generally placed in subpackages that group them by classification. For example, utility classes might go in a utils subpackage and service proxy classes might go in a services package. As an example, imagine that you’re writing a class called LoggingService that is specific to a project with a code name of JediKnight for your company called ExampleCompany (with a domain name of examplecompany.com.) You might place that class in the following package:

com.examplecompany.jediknight.services

Encapsulation

One of the rules of good object-oriented design is that all classes should be black boxes: you can put things in and take things out, but you can’t determine how it operates. In other words, the only way to interact with a class instance is to use its public methods. You should never be able to look into an object or change the object’s state except by asking the object to tell you about itself or to change its own state. The object must always maintain sovereignty. The minute an object is no longer in charge of its own internal world, the entire object-oriented universe starts to crumble and fall apart into an unmanageable train wreck.

This idea of classes being black boxes is a fundamental principle of object-oriented design called encapsulation. Encapsulation is absolutely necessary for an object-oriented design to succeed because it enables objects to interact with one another in known and well-defined ways. This approach models the world in which we live in many ways. Every object in the physical world has boundaries that define it and its interface with the world around it. Your body interacts with the air by way of respiration, for example. Without these well-defined interfaces there would be chaos, and it would be impossible to interact with anything in a useful or meaningful way.

Implementing classes so that they adhere to the principle of encapsulation is quite simple. To achieve this goal, there are just two basic rules:

  1. Don’t use any public properties.

  2. Don’t reference objects outside the class unless the reference was passed to the class as a parameter.

Public Properties

Properties store an object’s state. As we’ve already said, an object must be in control of its own state. Public properties allow other objects to directly change an object’s state without the object being in control. The implications of this can be far-reaching, but we can see the problem with a simple example. Consider a Student class that models a student at a school. One of the fields that comprise a Student object’s state is the GPA (grade point average). It might seem like a good idea to simply define the class with a public gpa property. However, consider that GPAs are generally constrained to a specific range of values (0 to 4, for example). With a public property, there’s no way for the application to guarantee that a student’s GPA will always be in the valid range. If the property is public, you can simply set the value to any numeric value regardless of whether or not it is within the valid range, as this example does:

student.gpa = 400;

As if that wasn’t bad enough, there are further ramifications. What if there are other collaborating objects that must be updated with a student’s GPA changes? For example, a SchoolRecord object might need to know when a GPA changes in general, and a Parent object might need to know when the GPA drops below or raises above a certain level. If the Student object doesn’t even know when its own state changes, it can not very well notify other objects when its state changes.

The solution to public properties is to use private properties with accessor methods. In ActionScript, we call the accessor methods getter and setter methods, and ActionScript enables two types of getters and setters: explicit and implicit. An explicit getter or setter is a normal method, typically using the word get or set in the name of the method. For example, rather than declaring a public gpa property, you can declare a private _gpa property and then use methods called getGPA() and setGPA(). Consider this example:

public function getGPA():Number {
   return _gpa;
}
public function setGPA(value:Number):void {
   if(value > 4) {
      _gpa = 4;
   }
   else if(value < 0) {
      _gpa = 0;
   }
   else {
      _gpa = value;
   }
   dispatchEvent(new Event(Event.CHANGE));
}

Notice that the setter method uses boundary testing to verify that the value is always in the valid range between 0 and 4. This example simply corrects values outside the valid range, but another implementation might throw an error. The method also dispatches an event that can notify listeners (such as a SchoolRecord or Parent object). When you want to set the GPA for a student, you can simply call the setGPA() method and pass it the value, as shown here:

student.setGPA(4);

When you want to retrieve the value you can call getGPA(), as in this example:

textfield.text = "GPA: " + student.getGPA();

Implicit getters and setters are similar to explicit getters and setters. In fact, the implementation of implicit methods can look almost identical to that for explicit getters and setters. The difference is that implicit getters and setters are defined as methods, but they look like properties when used. The syntax for implicit getters and setters uses the keywords get and set after the function keyword. The following example rewrites the preceding explicit methods as implicit methods:

public function get gpa():Number {
   return _gpa;
}
public function set gpa(value:Number):void {
   if(value > 4) {
      _gpa = 4;
   }
   else if(value < 0) {
      _gpa = 0;
   }
   else {
      _gpa = value;
   }
   dispatchEvent(new Event(Event.CHANGE));
}

When you want to call the implicit setter method, you use it as part of an assignment statement. The value you assign to the “property” is passed to the setter method, like this:

student.gpa = 4;

You can call the getter method when you reference the “property” in a context that attempts to read the value, as shown here:

textfield.text = "GPA: " + student.gpa;

External References

A class should never directly reference any object that is outside of itself unless it obtains that reference through its public interface. A class can declare private properties and local variables and can reference those objects internally because they exist within the class. A class can also reference an outside object if the reference was passed into it via a public method. For example, a Student class might define a method called attendClass() that accepts an AcademicClass parameter. The Student object can then reference that object because it was passed in as part of a method call.

public class Student {

   public function _classes:Array;
                                                                                    
   public function Student() {
      _classes = new Array();
   }

   public function attendClass(class:AcademicClass):void {
      _classes.push(class);
      // Now that the class was passed in as a parameter the 
      // Student instance can store that reference in the array 
      // and use it later. This doesn't break encapsulation 
      // because the reference was passed in via the public API.
   }

   // Remainder of implmentation.

}

Designing for Encapsulation

Encapsulation is an extremely important principle, and it can have far-reaching consequences. Consider a School class that has a private property called _students, an array of all the students who attend the school. If you need to make the students available to collaborators with the School object (for example, a SchoolDistrict class might need to know about all the students at all the schools in the district), you can make the array accessible using a getter method, as shown here:

public function get students():Array {
   return _students;
}

Even though you aren’t using a public property, the design in this example breaks the principle of encapsulation. Consider what happens when you retrieve the _students array and make changes to it directly:

school.students.splice(10, 5);

The preceding code removes five students from a school, but the school never receives notification about the removal of the students. That is obviously not the behavior you would want (a school should always know when students have been removed). You can address this issue in several ways. One way is to simply return a copy rather than a reference, as shown here:

public function get students():Array {
    return _students.concat();
}

Another solution is to employ the Iterator pattern (described in Chapter 7, “Iterator Pattern”). Regardless of which solution you use, you are solving the design flaw that broke the principle of encapsulation.

Most design patterns are solutions to problems relating to encapsulation. In many cases, encapsulation might appear to be in direct opposition to other important design principles. For example, many applications need to have globally accessible objects of specific types. An application might need a globally accessible User object that represents the current user of the application. As we’ve already discussed, it would break encapsulation if all the other classes in the application had hard-coded references to that one specific User object. However, using the Singleton pattern (described in Chapter 4), you can achieve the goal of a globally accessible object without having to directly reference a specific object.

Inheritance and Composition

One class can leverage the functionality of another class in one of two basic ways: inheritance or composition. Both are powerful techniques. Inheritance allows you to define a new class so that it automatically gets the interface and implementation of an existing class. The following code declares a class called Employee:

public class Employee {
   public function Employee() {}
   public function work():void {
      trace("working");
   }
}

The new class, which we call the subclass, can build on the foundation of the existing class, which we call the superclass or base class, without needing to rewrite the original code or write any new code to use the superclass code. There are different types of employees, and we can define different subtypes by inheriting from the Employee superclass. For example, the following Executive class inherits from Employee by using the extends keyword:

public class Executive extends Employee {
   public function Executive() {}
   public function attendMeeting():void {
      trace("attending meeting");
   }
}

Furthermore, inheritance automatically enables polymorphism because the subclass inherits the interface of the superclass. That means that an Executive object is also an Employee…just a more specific type. An Executive object can be used any time an Employee object is expected although the reverse is not true:—an Employee object cannot stand in for an Executive object. Note that the Executive class defines another method called attendMeeting(). Because Executive objects inherit from the Employee superclass, you can call the work() method for an Executive and you can also call the attendMeeting() method which is specific to Executive.

In contrast with inheritance, composition allows you to write a new class (a front-end class) that has an instance of an existing class (the back-end class). Every time you define a class with a property whose type is another class, you are using composition in some sense. The following example is a rewrite of the Executive class example just shown so that it uses composition rather than inheritance:

public class Executive {
   private var _employee:Employee;
   public function Executive() {}
   public function attendMeeting():void {
      trace("attend meeting");
   }
   public function work():void {
      _employee.work();
   }
}

When you use composition, the new (front-end) class does not automatically inherit the interface of the existing (back-end) class. The front-end class can use the back-end class instance only by way of its public interface. If the front-end class needs to have part or all of the same interface as the back-end class, you must write code that defines the interface as well as its implementation. That is the reason that this rewrite of the Executive class has to define a work() method. Unlike the example that used inheritance, the composition version of the Executive class does not inherit the work() method. If you want the work() method to be part of the Executive interface, you must define it. The preceding example uses a technique called delegation to pass along the method call to the composed object.

Because a class that composes an instance of another class does not automatically inherit the object’s interface, composition does not automatically enable polymorphism. In other words, using composition, an Executive object is not an Employee, and it cannot stand in for an Employee. (The solution to this issue is to use interface constructs as discussed earlier in this chapter.)

In reading the preceding paragraphs, you might think that inheritance sounds like a much better technique for reusing existing functionality. It sounds like composition requires much more work with little or no advantage. Yet both inheritance and composition have their advantages and disadvantages.

Advantages and Disadvantages of Inheritance

As you’ve seen already, inheritance has the following advantages:

  • Simplicity of use: Inheritance is a concept built into the language. All you have to do is use the extends keyword in order to define one class so that it inherits both the interface and the implementation of an existing class.

  • Ability to change inherited implementation: By using the overrides keyword, you can change the implementation inherited for a particular method.

Yet inheritance also has its disadvantages:

  • Implementations are fixed at compile-time: For example, if a Chart3D class inherits from the BarChart class, then it’s impossible at runtime to apply the 3D functionality to a LineGraph object.

  • Supports weak encapsulation and fragile structures: Subclasses have privileged access to a superclass’s implementation. Anything that is marked as public, internal, or protected is accessible to a subclass. This means that encapsulation is weak in inheritance relationships. Because of this, it’s possible that a change to a superclass implementation could break subclasses even if the public interface does not change.

  • Superclass interface changes necessarily change subclasses: If you change the signature of a superclass method the change will ripple to all subclasses.

  • ActionScript allows a class to inherit directly from just one class (as opposed to multiple inheritance, a concept utilized by very few languages): Suppose that all Executive objects share the functionality of both Employee and DecisionMaker classes. ActionScript allows Executive to inherit from just one of those classes, not both.

Advantages and Disadvantages of Composition

Although we haven’t yet mentioned the advantages of composition, they are numerous. Some of the most prominent advantages are as follows:

  • Implementations are configurable at runtime: For example, if a Chart3D class operates on an object typed as Chart (of which there are many subtypes such as BarChart and LineGraph), the Chart3D class can operate on any of those subtypes. The specific subtype can be set at runtime.

  • Supports good encapsulation and adaptable structures: Classes that use composition are forced to go through the back-end class public interfaces. That means that they enforce good encapsulation. That also means that changes in implementation of the back-end classes are less likely to break classes that use them. As long as the interface remains the same, the front-end classes won’t break.

  • Interface changes have limited ripple effect: When the interface of a back-end class changes, it will break front-end classes that rely on the old version of the interface. However, the damage is contained and generally fairly trivial to correct. Because interfaces are not inherited when using composition, the changes affect only the front-end class, but not classes that in turn compose instances of the front-end class. In other words, if Executive is a front-end class for Employee and the interface for Employee changes, you will most likely have to make changes to Executive. However, the interface for Executive does not change. That means that if a Company class composes an Executive object, the Company class does not have to change.

  • Composition allows a front-end class to have relationships with many back-end classes: Using composition, an Executive class can have both an Employee and a DecisionMaker property.

Yet composition is not without its disadvantages:

  • Frequently requires more code than inheritance: If a front-end class needs to use some or all of a back-end class’s interface, it must re-create it.

  • Often more difficult to read than inheritance: Inheritance establishes a very straightforward relationship. Composition is often less direct and presents a trail that’s more difficult to follow if you’re not familiar with the code.

Which to Use: Inheritance or Composition

Generally, the rule of thumb is to favor object composition over inheritance. The advantages of object composition outnumber the disadvantages. Furthermore, the disadvantages of composition are not obstacles as much as they are simply inconveniences. Because inheritance is so much more straightforward, it’s a lot easier to teach and learn in many cases, and it tends to be overemphasized and overused by many people in the ActionScript development community. For this reason, it’s often beneficial for ActionScript developers to determine whether composition is the best option for establishing a relationship between classes.

With that said, it’s also worth noting that with the surge of interest in object-oriented design and design patterns in the ActionScript community, inheritance has been maligned in many circles. It’s important to understand several things about this conflict:

  • Inheritance is not wrong: Just because you should favor composition does not mean that inheritance is never appropriate. Inheritance is a better solution in some cases. It’s difficult to make rules that tell you when to use inheritance and when to use composition. However, as a general guideline, it’s advisable to use inheritance in the following situations: When a new class really does define a subtype of an existing class, when the new class is not likely to have subclasses itself (limiting inheritance chains keeps some of the disadvantages of inheritance at bay), when the new class would benefit greatly by inheriting part of the existing class’s implementation that is hidden from the public, and when the new class does not have special requirements (for example, it needs to be adaptable to significant changes at runtime).

  • Inheritance and composition are not competitors: Although it is true that in almost all cases two classes will be related by either inheritance or composition (and not both), that does not mean that these two types of relationships can not work together. In fact, most classes that use inheritance also use composition.

Conventional teaching says that to determine whether two classes should be related by inheritance or composition, you should use the “is a/has a” test. The “is a/has a” test says that you should answer the following question: Is (new class) a (existing class) or does (new class) have a (existing class)? If the new class is a more specific version of the existing class, the relationship is inheritance. If the new class simply has an instance of the existing class as a property, the relationship is composition. Although that guideline can be useful, it is not definitive. Consider an example using an existing class called Student and a new class called School. If we ask whether School is a Student, the answer is obvious: a School is not a Student. Therefore, the relationship must be composition, not inheritance. Yet just because we can answer that a new class is a more specific version of an existing class doesn’t mean that the relationship should necessarily be inheritance. For example, consider the relationship between a HighSchool class and a School class. If you use only the “is a/has a” test, you might determine that a HighSchool is a School and therefore the relationship is inheritance. Yet consider what happens if you need to have a HighSchool object that uses experimental administration structure and teaching techniques. We can assume that the implementation for School deals with traditional school systems and infrastructure and would not meet the needs of an experimental school. An inheritance relationship between School and HighSchool is rigid. If you use composition to define the relationship, it’s possible to create an experimental high school type at compile type by substituting an ExperimentalSchool instance for the School property of a HighSchool object.

Coupling

Coupling refers to the degree to which two objects must know about one another. When the objects have to know a great deal about one another to work, we call that tight coupling; when they have to know little to nothing about one another, we call that loose coupling. In object-oriented design, we generally strive to have loose coupling among the objects in the system. Loose coupling creates flexible and adaptable systems. If objects are tightly coupled, the system is rigid—one change in one object can cascade and break the entire system. If objects are loosely coupled, changes are much less likely to break things, and even when changes do cause malfunctions, the malfunctions are generally contained.

Many design patterns aim to create loosely coupled systems. For example, if an object needs to ask another object to run a behavior, the traditional way to accomplish this goal is for the object to have a reference to the collaborator and to call a method of that collaborator. That way of structuring an application uses tight coupling because the calling object has to have a reference to the collaborator and it has to know the signature of the method it wants to call. It’s difficult to make changes to that structure. The Command pattern described in Chapter 10 addresses this issue by completely decoupling the objects. The Command pattern adds an intermediary layer that parameterizes the behavior and allows the calling object to simply have a reference to the intermediary object and know about a standard interface. This is just one example of how design patterns can promote loose coupling or decoupling, and you’ll see many more examples throughout the book as you read about each of the patterns.

Testing

Once you’ve completed the implementation phase the next important phase you need to consider is the testing phase. Generally testing involves a quality assurance (QA) group that runs test cases to determine that the application behaves as expected and to try to catch any bugs. This testing phase is iterative. When QA returns a list of bugs the development team must work to fix any issues. However, when fixings bugs it’s possible to introduce new bugs. If you have architected the application well, favoring composition over inheritance for building flexible structures, then the risk of introducing new bugs during this phase is minimized. However, it’s is almost inevitable that some new bugs will be introduced during bug fixing and old fixed bugs will re-emerge. Because of the possibility of this introduction and re-introduction of bugs testing generally involves something called regression testing—which basically means all tests that previously passed must be run again to ensure that changes didn’t cause any of those tests to suddenly fail.

As you might imagine the introduction and re-introduction of bugs can be quite expensive during the testing phase if they go uncaught until the build is regression tested by a QA team. If a bug isn’t caught until QA runs a regression test then it means that the development team must fix the bugs again and send yet another build to QA for regression testing.

If possible it’s always best for developers to try to find new bugs and regressions before sending the build to QA. The difficulty with that strategy is that it requires the development team to be responsible for testing the application. If developers could handle testing in addition to development and bug fixes then there wouldn’t be a need for a QA team in the first place, so it might almost seem ridiculous to suggest that developers should have to test an application. However, if developers can run automated tests that verify that an application continues to work correctly from a programmatic standpoint then that doesn’t require a great deal more work on the part of the developer, and it enables developers to quickly identify errors before sending a build to QA. These programmatic tests are can be formalized into what is called a unit test.

Unit testing allows the developer to create programmatic tests that ensure that parts of the application behave in an expected way. For example, if you have a method that’s supposed to convert a parameter value from radians to degrees and return that value then you want to make sure that if you pass it a value of Math.PI it returns 180 every time. Using this basic concept you can create a series of tests where you ensure that results of operations are as expected (i.e. Math.PI radians is always converted correctly to 180 degrees).

You can create unit tests without a formal unit test framework. However, using a formal framework for unit testing has several advantages. Specifically:

  • When you use an existing framework you don’t have to reinvent the wheel, saving you time

  • An existing framework is likely to be tested so that bugs in the unit testing framework won’t cause your tests to fail to work (which would negate the value of running unit tests in the first place.)

Although there may be additional unit testing frameworks for ActionScript 3.0 subsequent to the writing of this book the one existing unit testing framework we know of at this point is called FlexUnit. As the name implies, you can use FlexUnit for unit testing Flex applications. However, that doesn’t mean that FlexUint is limited to unit testing applications that use the Flex framework. Even if you are working on a purely ActionScript 3.0 project you can use FlexUnit.

At the time of this writing FlexUnit is available for download at http://labs.adobe.com/wiki/index.php/ActionScript_3:resources:apis:libraries. If that URL changes you may not be able to find the downloads there. In such a case you can look to www.rightactionscript.com/aas3wdp for an updated URL.

Once you’ve located the correct URL you should download the archive containing the .swc file which contains the necessary FlexUnit framework libraries. You will want to extract the .swc file from the archive and then make sure that the .swc is included in the library path for your project for which you want to use unit tests.

If you want to write custom unit tests that don’t rely on FlexUnit then you are welcome to do so. However, for the remainder of this section on unit testing we will be giving specific instructions for running unit tests using FlexUnit.

Creating Basic Unit Tests

In FlexUnit basic unit tests require the following elements:

  • Classes you want to test. These are the classes that comprise your application.

  • Test cases. Test cases are special classes that you write just for the purposes of unit testing.

  • Test runner. A test runner is a class (or MXML file) that actually runs all the test cases and reports the results.

The first category of elements isn’t specific to unit tests. That category is simply comprised of the classes you’ve already written. They are part of unit testing because you are testing that they actually work the way you expect. For the basic test cases we’ll test the following class.

package example {
  public class SimpleConverter {
    public function SimpleConverter() {}
    public function convertToRadians(degrees:Number):Number {
      return (degrees / 180) * Math.PI;
    }
    public function convertToDegrees(radians:Number):Number {
      return (radians / Math.PI) * 180;
    }
   }
}

Test cases and test runners, on the other hand, are unique to unit testing. Since test cases and test runners are likely new to you we’ll look at how to create them in the next sections.

Writing Test Cases

A FlexUnit test case is an instance of a class that extends flexunit.framework.TestCase. The test case class constructor should always accept a string parameter and then call the super constructor, passing it the parameter value.

package tests {
  import flexunit.framework.TestCase;
  public class SimpleTest extends TestCase {
    public function SimpleTest(method:String) {
      super(method);
    }
  }
}

The class should then define one or more methods that run a test. Each test should result in an assertion. An assertion is what actually determines the success of the test. You can run an assertion using any of the assert methods inherited by the Assert class which is the superclass of TestCase:

  • assertEquals(): Tests if all the parameters are equal (equivalent to an == operation)

  • assertStrictlyEquals(): Tests if all the parameters are strictly equal (equivalent to an === operation)

  • assertTrue(): Test if the parameter is true

  • assertFalse(): Test if the parameter is false (passes test if the parameter is false)

  • assertUndefined(): Test if the parameter is undefined (passes test if the parameter is undefined)

  • assertNull(): Test if the parameter is null (passes test if the parameter is null)

  • assertNotNull(): Test if the parameter is not null

  • fail(): Though technically not an assertion, the fail() method explicitly causes the test to fail, which can be useful when you need to test for a failure.

The following update to SimpleTest defines two test methods to test the conversions to and from degrees and radians.

package tests {
  import flexunit.framework.TestCase;
  import example.Simple;
  public class SimpleTest extends TestCase {
    public function SimpleTest(method:String) {
      super(method);
    }
    public function testConvert0ToDegrees():void {
      var simple:SimpleConverter = new SimpleConverter();
      var degrees:Number = simple.convertToDegrees(0);
      assertEquals(degrees, 0);
    }
    public function testConvertPIToDegrees():void {
      var simple:SimpleConverter = new SimpleConverter();
      var degrees:Number = simple.convertToDegrees(0);
      assertEquals(degrees, 180);
    }
    public function testConvert0ToRadians():void {
      var simple:SimpleConverter = new SimpleConverter();
      var radians:Number = simple.convertToRadians(0);
      assertEquals(radians, 0);
    }
    public function testConvert180ToRadians():void {
      var simple:SimpleConverter = new SimpleConverter();
      var radians:Number = simple.convertToRadians(180);
      assertEquals(radians, Math.PI);
    }
  }
}

Once you’ve created one or more test cases you next to create a test runner to run the tests and view the results.

Writing a Test Runner

Assuming you’re using Flex you can use the FlexUnit test runner to run a suite of unit tests. First, you must create a runnable MXML document that does the following:

  • Add the flexunit.flexui.* namespace

  • Add an instance of TestRunnerBase, an MXML component

  • Create a flexunit.framework.TestSuite instance, and add all the test cases to it.

  • Assign the TestSuite instance to the test property of the TestRunnerBase instance.

    Call the startTest() method of the TestRunnerBase instance.

The following example MXML document runs all the tests from SimpleTest.

<?xml version="1.0" encoding="utf-8"?>
<!-- Notice that the Application tag adds the flexui namespace prefix and maps it
to flexunit.flexui.*. Also notice that it registers initializeHandler() as an event
handler for the initialize event.-->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:flexui="flexunit.flexui.*"
Writing a Test Runner initialize="initializeHandler(event)">
    
   <mx:Script>
      <![CDATA[
         import flexunit.framework.TestSuite;
         import tests.SimpleTest;

         private function initializeHandler(event:Event):void {
            // Create a new TestSuite object.
            var suite:TestSuite = new TestSuite();

            // Use the addTest() method to add each of 
            // the four test cases to the suite.
            suite.addTest(new SimpleTest("testConvert0ToDegrees"));
            suite.addTest(new SimpleTest("testConvertPIToDegrees"));
            suite.addTest(new SimpleTest("testConvert0ToRadians"));
            suite.addTest(new SimpleTest("testConvert180ToRadians"));
               
            testRunner.test = suite;
            testRunner.startTest();
         }
       ]]>
    </mx:Script>
    <flexui:TestRunnerBase id="testRunner" width="100%" height="100%" />
</mx:Application>

Notice that each test case is an instance of SimpleTest with one of the test method names passed to the constructor. When you run the preceding test runner it should show all the tests as passing. If you make the following change to SimpleConverter you’ll see that one of the tests fails.

package example {
  public class SimpleConverter {
    public function SimpleConverter() {}
    public function convertToRadians(degrees:Number):Number {
      return (degrees / 180) * Math.PI;
      }
      public function convertToDegrees(radians:Number):Number {
        return 0;
      }
   }
}

Note that since convertToDegrees() always returns 0 the testConvertPIToDegrees test will fail. Since the specific test fails you immediately know where the error is occurring, and you can fix the bug.

Another thing that can be useful when creating test cases is to add a static method to each TestCase subclass that returns a TestSuite of all the tests for that class. This allows you to simplify the test runner. The following is an example of such a method you could add to SimpleConverter.

public static function suite():TestSuite {
   var suite:TestSuite = new TestSuite();
   suite.addTest(new SimpleTest("testConvert0ToDegrees"));
   suite.addTest(new SimpleTest("testConvertPIToDegrees"));
   suite.addTest(new SimpleTest("testConvert0ToRadians"));
   suite.addTest(new SimpleTest("testConvert180ToRadians"));
   return suite;
}

The test runner initializeHandler() method would then simplify to the following:

private function initializeHandler(event:Event):void {
   testRunner.test = SimpleTest.suite();
   testRunner.startTest();
}

Creating Asynchronous Unit Tests

Many unit tests are synchronous—meaning that you can immediately determine if a test has passed or failed. For example, the SimpleConverter test in the preceding section passed or failed a test immediately. However, it’s possible that some tests may depend on asynchronous operations. For example, a class may need to make a request and wait for a response from a service method before a test can be verified properly. In such cases it’s important to be able to run tests asynchronously. For an example consider the following class which loads data from a text file when calling the getData() method.

package example {
   import flash.events.EventDispatcher;
   import flash.net.URLLoader;
   import flash.events.Event;
   import flash.net.URLRequest;

   public class AsynchronousExample extends EventDispatcher {

      private var _loader:URLLoader;

      public function get data():String {
         return _loader.data;
      }

      public function AsynchronousExample() {
         _loader = new URLLoader();
         _loader.addEventListener(Event.COMPLETE, onData);
      }

      public function getData():void {
         _loader.load(new URLRequest("data.txt"));
      }

      private function onData(event:Event):void {
         dispatchEvent(new Event(Event.COMPLETE));
      }

   }
}

With a few simple changes it’s possible to run FlexUnit tests asynchronously so you can test operations like getData(). Asynchronous operations should use events to notify listeners when the operation has completed. Typically when you register a listener for a particular event you use the addEventListener() method, and you pass it a reference to the listener method. When writing test cases for asynchronous operations you should register a listener method to handle the event that signals a completed operation. However, rather than registering the listener directly, you should use an inherited TestCase method called addAsync(). The addAsync() method allows you to specify a listener method along with a time out in milliseconds. This allows you to specify what method should handle the event, but if the event doesn’t occur within the timeout window then the test will fail. The event listener method should run the assertion. The following example uses these techniques. You’ll see that the class extends TestCase just like a basic unit test. Furthermore, this test case class also accepts a method name as a parameter for the constructor, and it passes the parameter to the super constructor. What differs is that the test method registers a listener using addAsync() and defers the assertion to onData(). This example times out after 2000 milliseconds. That means that if the data loads in 2000 milliseconds or less then the assertion will run. However, if the data doesn’t load in time then the test case assumes that it was due to a failure and the test fails.

package tests {
   import flexunit.framework.TestCase;
   import example.AsynchronousExample;
   import flash.events.Event;
   import flexunit.framework.TestSuite;

   public class AsynchronousTest extends TestCase {

      public function AsynchronousTest(method:String):void {
         super(method);
      }

      public function testGetData():void {
         var asynchronous:AsynchronousExample = new AsynchronousExample();
         asynchronous.addEventListener(Event.COMPLETE, addAsync(onData, 2000));
         asynchronous.getData();
      }
       
      private function onData(event:Event):void {
         assertNotNull(event.target.data);
      }

      public static function suite():TestSuite {
         var suite:TestSuite = new TestSuite();
         suite.addTest(new AsynchronousTest("testGetData"));
         return suite;
      }

   }
}

The following test runner will run both the simple tests and the asynchronous test.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:flexui="flexunit.flexui.*"
Creating Asynchronous Unit Tests initialize="initializeHandler(event)">

   <mx:Script>
      <![CDATA[
         import flexunit.framework.TestSuite;
         import tests.SimpleTest;
         import tests.AsynchronousTest;

         private function initializeHandler(event:Event):void {
            var suite:TestSuite = new TestSuite();
            suite.addTest(SimpleTest.suite());
            suite.addTest(AsynchronousTest.suite());
            testRunner.test = suite;
            testRunner.startTest();
         }
       ]]>
    </mx:Script>
    <flexui:TestRunnerBase id="testRunner" width="100%" height="100%"/>
</mx:Application>

Summary

Although many people think of building applications as exclusively writing the code, in this chapter we have seen that writing code is just one of the phases of building successful applications. We’ve seen that one of the biggest challenges is knowing what to write, and the analysis and design phases of a project are the time to determine the answer to that question. The third phase, implementation, is the time to actually write the code. Following implementation is the testing phase which allows developers to use unit testing to ensure fewer regressions

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.197.212