Designing the System

 < Day Day Up > 

The iterative approach described earlier recommends designing and implementing individual pieces of the application. The domain model in Figure 16.2 shows four clear domains to be implemented: Product , Offer , Subscription , and Customer . First, we'll explore the Product domain.

Product Domain

Following this best practice, we will start by detailing the use case surrounding the product domain identified earlier.

Product Domain Use Cases

Use Case: Enter product

Actors: DER

Preconditions: Actor has been authenticated to the system, and identified as a member of the Data Entry group . They will begin this process on the Data entry screen.

Primary Flow:

  1. Actor chooses to enter a product.

  2. A screen is presented to the actor with the following fields:

    • Name

    • Description

    • Publisher

    • Single issue price

  3. Actor fills out screen, and clicks button to insert product.

  4. Validation routines are run over inputs, assuring that name, publisher, description, and single issue price are not blank, and that single issue price is a number.

  5. Once validated , data is sent to server to be inserted.

  6. After successful insertion by server, server will return a message to the client indicating successful insertion.

  7. User is returned to the Data entry screen, where they have the option to enter another product. If he or she answers yes, the user starts again with Step 2. Otherwise , he or she is returned to the main Data entry screen.

N OTE

The step numbers in the following list (and in similar subsequent lists) map to specific step numbers in the main step sequence for each use case.


Alternative Flow:

4a. If any of the fields fail validation routines, a message box will alert the actor of the problems.

4b. When the user closes the message box, focus will be given to the first field that failed the validation routines.

6a. If server returns a message other than that the data was successfully inserted, the user will be notified that there was a problem inserting the data and will be prompted with a link to mail the issue to the applications administrator.

Use Case: Search for a product

Actors: DER, CCR, and SM

Preconditions: Actor has been authenticated to the system, and identified as a member of one of the groups listed as actors.

Primary Flow:

  1. Actor chooses to find a product.

  2. A screen is presented to the actor with the following fields:

    • Name

    • Description

    • Publisher

    • Single issue price

  3. Actor fills out screen, and clicks button to search for a product.

  4. Matching products are presented to user.

  5. User chooses a product from the list.

  6. Based on role, user is either presented with a screen to edit the product or presented with a screen to view the product.

Alternative Flow:

4a. If there are no matching products, users are redirected to Step 2, with a message indicating that no products have matched. If there is only one matching product, user skips Step 5 and proceeds directly to Step 6.

Use Case: View/edit a product

Actors: DER, CCR, SM

Preconditions: Actor has been authenticated to the system and identified as a member of one of the groups listed above as actors. Actor has chosen a product following the "Search for Product" use case.

Primary Flow:

  1. A search screen is presented to the actor with the following fields:

    • Name

    • Description

    • Publisher

    • Single issue price

  2. Based on user role, the fields are either dynamic text (CCR) or input text (DER and SM) fields. For a CCR, this is the end of the use case.

  3. Actor makes appropriate edits to the text fields.

  4. Actor chooses to submit updates to the server.

  5. Data is validated, as per "Enter Product" use case, Step 4.

  6. Once validated, data is sent to server to be inserted.

  7. After successful update by server, server will return a message to the client indicating successful insertion.

  8. User is asked if he or she wishes to edit another product. If he or she answers yes, the process starts again with "Search for a Product" use case. Otherwise, he or she is returned to the main screen for users of their role.

Alternative Flows:

5a. If any of the fields fail validation routines, a message box will alert the actor of the problems.

5b. When user closes the message box, focus will be given to the first field that failed the validation routines.

7a. If server returns a message other than that the data was successfully inserted, the user will be notified that there was a problem inserting the data and prompted with a link to mail the issue to the applications administrator.

Product Sequence Diagram

After the use cases surrounding the products are understood , a sequence diagram can begin to be built for the product object, as seen in Figure 16.3.

Figure 16.3. The sequence diagram of the product helps determine what messages it will need to exchange with other objects.

graphics/16fig03.gif

With a sequence diagram, it can begin to be understood how the Product object will interact with other objects. As can be seen from Figure 16.3, the only entity the product needs to interact with is a DataAccess object. It is responsible for facilitating communication between our client (the Flash Player) and the server-side object, which ultimately will interact with the database.

Product Class Diagram

With the use cases and sequence diagrams in place, a first iteration of the Product class diagram can be derived, capturing the properties identified through the use cases and the methods identified through the sequence diagram. Figure 16.4 shows an initial view of the Product class diagram.

Figure 16.4. The class diagram for a product indicates the properties and methods necessary for a Product .

graphics/16fig04.gif

As can be seen in the class diagram, the Product class needs methods to create a product (this will be handled with the product constructor), a method to validate a product, one to update, one to find a product, and lastly, one to retrieve a product. The Product class will also have a static property, dataAccessObject , which will be an instance of the DataAccess class, which was created in Chapter 12, "XML and Flash."

N OTE

The code relating to this class is created in the next section, "First Build of Code."


First Build of Code

After the class diagram is created, we can begin to build our first class file. Listing 16.1 shows the first cut at the Product class.

Listing 16.1. First Draft of a Product Class
 class Product{       // instance properties       private var name:String;       private var publisher:String;       private var singleIssuePrice:Number;       private var description:String;       private var prodID:Number;       //static properties       private static var dataAccessObject:DataAccess = new DataAccess("http://localhost:8500/oopas2/components/product.cfc?wsdl");       //consturctor       function Product(name:String,publisher:String,price:Number,description:String){              setValues(name,publisher,price,description);       }       //data access functions       public function insert(resultsMethod:Function, statusMethod:Function):Boolean{{              if(this.validateData()){              trace("calling DAO - Create"); Product.dataAccessObject.accessRemote("create",this.serializeValues());                     return true;              } else {                     trace("Data invalid");                     return false;              }       }       //validation       public function validateData():Boolean{             var isValid:Boolean = true;             if(!validateString(this.getName())){                   return false;             } else if (!validateString(this.getPublisher())){                   return false;             } else if (!validateNumber(this.getSingleIssuePrice())){                   return false;             } else {                   return true;             }       }       private function validateString(string:String):Boolean{             if(string.length == 0){                   return false;             } else {                   for(var i:Number=0;i<string.length;i++){                         if(string.charAt(i) != " "){                               return true;                               break;                         }                   }             }       }       private function validateNumber(number:Number):Boolean{             if (isNaN(number)){                   return false;             } else {                   return true;       }       }       // setter functions       public function setValues(name:String,publisher:String,price:Number,description:String){              setName(name);              setPublisher(publisher);              setSingleIssuePrice(price);              setDescription(description);       }       public function setName(name:String):Void{             this.name = name;       }       public function setPublisher(publisher:String):Void{             this.publisher= publisher;       }       public function setSingleIssuePrice(price:Number):Void{             this.singleIssuePrice= price;       }       public function setDescription(description:String):Void{             this.description = description;       }       // getter functions       public function serializeValues():Object{             var prod = new Object();             prod.name = this.getName();             prod.publisher = this.getPublisher();             prod.singleIssuePrice = this.getSingleIssuePrice();             prod.description = this.getDescription();             return prod; }       public function getName():String{             return this.name;       }       public function getPublisher():String{             return this.publisher;       }       public function getSingleIssuePrice():Number{             return this.singleIssuePrice;       }       public function getDescription():String{             return this.description;       } } 

N OTE

Attempting to test this code as written will generate errors. This is due to the fact that the DataAccess class isn't defined yet (it's defined in Listing 16.4) or that it's not being tested in the proper context. Please see the note after Listing 16.4 for information about properly testing this class.


This class begins in a fairly straightforward manner. The class file is broken into seven sections, with a comment block between them indicating what type of code will go into each section. The first section declares all instance variables and their data types, as was discussed in Chapter 3, "We're All Described by Our Properties." The instance variables are all set to be private so that they can be accessed only by the getter and setter methods.

This is followed by the static properties section. Static properties were covered in detail in Chapter 3 as well. For the Product class, the dataAccessObject property is set to be static, as we don't want a separate dataAccessObject for each instance of the class; instead, we want all products to use the same object for accessing remote data.

Following the static properties is the constructor method. As was discussed in Chapter 2, "All the World Is an Object," the constructor defines how new instances of a class are created.

Following the constructor are the data access methods. These are methods which are responsible for sending and retrieving data to be used by the class.

The data access section is followed by data validation routines, and finally the getter and setter methods of the class.

For this class, there are only two validation rules that need to be obeyed. The first is that some fields need to be non-empty strings. The second is that some need to be numbers. To enforce the first rule, a method called validateString() is created to ensure the length of a string is not zero and that it contains something other than just spaces.

This is done by first testing the length property of the string. If the length is 0, the method returns false , indicating it is not a valid string. If the length test is passed, each character of the string is looped over, comparing it to a space. If any of the characters are not spaces, it breaks out of the loop, and returns true , indicating it is valid. If the string contains nothing but spaces, it will return false .

The validateNumber() method is much simpler, as it checks whether the data passed in is a valid number, using the isNaN() function. If the data is not a number, the method returns false ; otherwise, it returns true .

The validateData method calls the appropriate validation routine on each of the properties of the object. We can start to surmise that by the time we are done, the validateString and validateNumber methods will be moved to an external class which will handle all generic validations for this class as well as others. For now, as this is the only class built, it is not necessary to further abstract it.

There are a few other non-standard methods added to this class, one in the getter and one in the setter sections. In the setter section, the method setValues() is defined as a single method to set all the properties. Internally, setValues() uses the standard setter methods. The benefit of abstracting the setters to a single method is that there are potentially multiple methods which may attempt to set data (although currently the constructor is the only one in use, it's easy to imagine that Product data may come from a database, and need to be set into an object in our application). The other new method is serializeValues() which returns an object containing all the properties of a Product but none of the methods. This object can be used both to insert a new Product into the database as well as to update an existing one. It can even be used to create a copy of a Product for use elsewhere in the system, without having to worry about other system entities changing the product.

As defined in Listing 16.1, aside from having the entire infrastructure (properties, validation, getters, and setters), the only use case realized is the Create Product use case. To create a new product, data will be passed to its constructor, which will pass it to the setValues() method, which in turn uses the setter methods to populate the properties. To persist this data to the database, the insert() method will be called. Insert() uses the validateData() method to determine if the data for the product is valid. If so, it passes that data on to the accessRemote() method of the dataAccessObject , and passes it the name of the method on the server responsible for inserting product data and the serialized version of the product.

So far, the class is now built to handle creating an object. Adding a method to update is the next logical step. Listing 16.2 shows one possible approach to the update() method. This method will use a few methods written earlier, such as setValues() and validateData() , to populate the properties and check for their validity. After validation, the serialized data is passed to the update method on the server to enable the data to be persisted .

The refactored method is seen in Listing 16.2.

Listing 16.2. The Product Class Has Been Refactored to Enable Data to be Updated and Inserted
 public function update (name:String, publisher:String, price:Number, description:String){          setValues(name,publisher,price,description);          if(this.validateData()){ Product.dataAccessObject.accessRemote("update",this.serializeValues());          }     } 

The final two methods we need to add are data retrieval methods. One will retrieve an array of matching products (aptly named search ) and one will retrieve a single product named getById . Listing 16.3 shows these two methods.

Listing 16.3. To Enable Retrieval of Products from the Server, search() and getByID() Are Added
 public function search(propertyObj:Object, resultsMethod:Function, statusMethod:Function):Void{     dataAccessObject.accessRemote("retrieveByProp", propertyObj, resultsMethod, statusMethod); } public static function getById(ID:Number, resultsMethod:Function, statusMethod:Function):Void{     dataAccessObject.accessRemote("retrieveByID",ID, resultsMethod, statusMethod); } 

The Product class relied heavily on an external class, which is called DataAccess . This class was created back in Chapter 12. Its purpose is to act as a centralized place where all communications between the client and the server will be encapsulated. By setting this as a static property of the Product class, we are able to create a single instance of DAO to be used by all instances of the class. The structure of the DataAccess class is shown in Listing 16.4, as a reminder of the class we wrote in Chapter 12.

Listing 16.4. The DataAccess Class
 class DataAccess{     private var webService;     public var callBack:Object = new Object();     public function DataAccess(address:String){         this.webService = new mx.services.WebService(address);         this.webService.WSDLURL = address;     }     public function accessRemote(methodName:String, object:Object, resultFunction:Function, statusFunction:Function){         this.callBack = this.webService[methodName](object);         this.callBack.resultFunction = resultFunction;         this.callBack.statusFunction = statusFunction;         this.callBack.onResult = function(result){             resultFunction(result);         }         this.callBack.onFault = function(status){             statusFunction(status);         }     } } 

N OTE

If you insert this code as-is in a new ActionScript file and run the syntax checker, you will see an error from the DataAccess class saying the following:

 "DataAccess.as: Line 9: There is no method with the name 'mx'.this.webService = new mx.services.WebService(address);" 

The reason for this is that the DataAccess class requires the underlying Web Services classes be loaded into a movie clip before they can be used. To effectively test the syntax of this code, create a new movie clip in the same directory as the DataAccess and Product class files. Next, the Web Services classes must be added to the movie. This can be done by dragging a WebServicesConnector onto the Stage or by opening the Classes library (Window > Other Panels > Common Libraries > Classes) and dragging WebServiceClasses to the Stage. After the classes are available, we can test our new class by opening the Actions panel, adding var myProd:Product = new Product(); , and testing the movie.


To begin testing, we'll open a new Flash movie, drag four TextInput components onto the Stage, and drag four static text boxes next to them to act as labels. Give an instance name to each of the TextInput boxes, so that from top to bottom they are named name , pub , price , and desc . Below these add a Button component and give it an instance name of submit_pb . The button will be used to trigger the validate and insert methods of the Product class. Figure 16.5 shows the Stage with the various elements positioned on it.

Figure 16.5. A simple create product form consists of textInput components, four static text labels, and a button.

graphics/16fig05.gif

In our Actions panel, as shown in Listing 16.5, an event handler is assigned to handle a user clicking submit_pb . This handler will create a new instance of the Product class from the data the user has input. Next, the insert method of the Product class will be invoked. Insert will first validate the data, and if valid, the dataAccessObject will be used to insert this new object into the database. If the validation fails, the newly created object is deleted. Listing 16.5 shows the code which should be entered on the first frame of the timeline.

Listing 16.5. Building a Simple Movie Clip to Test the DataAccess and Product Classes
 var listener:Object = new Object(); listener.click = function(){     var prod:Product = new Product(name_txt.text, pub_txt.text, price_txt.text, desc_txt.text);     if(prod.insert()){         trace("inserting...");     } else {         //show error message         trace("invalid data");         delete prod;     } } this.submit_pb.addEventListener("click", listener); 

Once completed, event handlers will be added to handle successful results or status (error) messages returning from the Web Services call.

The Offer Class

With the Product and DataAccess classes built, we can move onto the next class, Offer . An offer consists of a product, a number of issues for that product, and a price. When the system is completed, it is an offer to which a customer will be subscribed.

There can be several different offers for each product (for example, an offer can be made for 12 issues of a product at one price, and another offer can be for 24 issues of that same product at a different price). Walking through the use cases of an offer will help clarify how an offer is used.

Use Cases

Use Case: Create offer

Actors: SM

Preconditions: Actor has been authenticated to the system and identified as a member of the Subscription Manager group.

Primary Flow:

  1. Actor chooses to enter an offer.

  2. A screen is presented to the actor with the following fields:

    • Menu with all products

    • Number of issues

    • Price

    • Check box indicating whether the offer is active

  3. Actor fills out screen, and clicks button to insert offer.

  4. Validation routines are run over inputs, assuring that price and number of issues are numeric, and greater than zero.

  5. Once validated, data is sent to server to be inserted.

  6. After successful insertion by server, server will return a message to the client indicating successful insertion.

  7. User is asked if he/she wishes to create another offer. If he or she answers yes, he or she starts again with Step 2. Otherwise, he or she is returned to the Subscription Manager screen.

Alternative Flows:

4a. If any of the fields fail the validation routines, a message box will alert the actor of the problems.

4b. When a user closes the message box, focus will be given to the first field that failed the validation routines.

6a. If server returns a message other than that the data was successfully inserted, the user will be notified that there was a problem inserting the data and prompted with a link to mail the issue to the applications administrator.

Use Case: Search for offer

Actors: CCRs and SM

Preconditions: Actor has been authenticated to the system, and identified as a member of one of the groups listed as actors.

Primary Flow:

  1. Actor chooses to find an offer.

  2. A search screen is presented to the actor with the following fields:

    • Menu of products

    • Number of issues

    • Price

  3. Actor fills out screen and clicks button to search for an offer.

  4. Matching offers are presented to user.

  5. User chooses an offer from the list.

  6. Based on role, user is either presented with a screen to edit the offer (SM) or presented with a screen to view the offer (CCR).

Alternative Flows:

4a. If there are no matching offers, users are redirected to Step 2, with a message indicating that no offers have matched. If there is only one matching offer, user skips Step 5, and proceeds directly to Step 6.

Use Case: View and/or edit an offer

Actors: CCRs and SM

Preconditions: Actor has been authenticated to the system and identified as a member of one of the groups listed as actors. Actor has chosen an offer following the "Search for Offer" use case.

Primary Flow:

  1. A screen is presented to the actor with the following fields:

    • Products

    • Number of issues

    • Price

    • Status (is offer active or not)

  2. Based on user's role, the fields are either dynamic text (CCR) or input (SM) fields, as shown in Table 16.1. For a CCR, this is the end of the use case.

    Table 16.1. Based on the Actor's Role, Fields Shown for the View and/or Edit Offer Use Case Will Vary
     

    CCR

    SM

    Products

    Dynamic text

    Menu

    Number of issues

    Dynamic text

    Input text

    Price

    Dynamic text

    Input text

    Status

    Dynamic text

    Check box

  3. Actor makes appropriate edits to the fields.

  4. Actor chooses to submit updates to the server.

  5. Data is validated, as per "Enter Offer" use case, Step 4.

  6. Once validated, data is sent to server to be inserted.

  7. After successful update by server, server will return a message to the client indicating successful insertion.

  8. User is asked if he or she wishes to edit another offer. If he or she answers yes, he or she starts again with the "Search for an Offer" use case. Otherwise, he or she is returned to the main screen for users of their role.

Alternative Flows:

5a. If any of the fields fail validation routines, a message box will alert the actor of the problems.

5b. When the user closes the message box, focus will be given to the first field that failed the validation routines.

7a. If server returns a message other than that the data was successfully inserted, the user will be notified that there was a problem inserting the data and prompted with a link to mail the issue to the applications administrator.

Sequence Diagram

With the Offer 's use understood, a sequence diagram can be created showing interactions with the Offer class. Unlike the Product class, which only interacted with the DataAccess class, the Offer class needs to interact with the Product class as well as the DataAccess class. Figure 16.6 shows the sequence diagram for the Offer class.

Figure 16.6. The sequence diagram of the Offer class shows that it interacts with the Product class as well as the DataAccessObject class.

graphics/16fig06.gif

Class Diagram

Glancing at the sequence diagram, the public methods necessary for an Offer become apparent. In fact, we will find that the methods for an offer closely mirror those of the Product : insert (), update() , search() , getByID() , and validate() . These are enumerated into the class diagram, seen in Figure 16.7.

Figure 16.7. The Offer class diagram shows the properties and methods of an Offer .

graphics/16fig07.gif

With the class diagram as a blueprint, a first iteration of the code making up the Offer class can be created.

Much like we did with the Product class, we'll start with the infrastructure (properties, constructor, data access, validation, getters, and setters) for the Offer class. This is shown in Listing 16.6.

Listing 16.6. The Core Infrastructure of the Offer Class is Assembled
 class Offer{     private var offerID:Number;     private var product:Product;     private var numIssues:Number;     private var price:Number;     private var isActive:Boolean = false;     //static properties     private static var dataAccessObject:DataAccess = new DataAccess("http://www.tapper.net/oopas2/components/product.cfc?wsdl");     //constructor     public function Offer(product:Product, numIssues:Number, price:Number,isActive:Boolean){         this.setValues(product,numIssues,price,isActive);     }     // data access     //data access functions     public function insert(resultsMethod:Function, statusMethod:Function):Boolean{         if(this.validateData()){             Offer.dataAccessObject.accessRemote("create", this.serializeValues(),resultsMethod, statusMethod);             return true;         } else {             return false;         }     }     public function update (product:Product, numIssues:Number, price:Number,isActive:Boolean, resultsMethod:Function, statusMethod:Function):Boolean{         this.setValues(product,numIssues,price,isActive);         if(this.validateData()){             Offer.dataAccessObject.accessRemote("update", this.serializeValues(),resultsMethod, statusMethod);             return true;         } else {             return false;         }     }     public function search(propertyObj:Object, resultsMethod:Function, statusMethod:Function):Void{         Offer.dataAccessObject.accessRemote("retrieveByProp", propertyObj, resultsMethod, statusMethod);     }     public static function getById(ID:Number, resultsMethod:Function, statusMethod:Function):Void{         Offer.dataAccessObject.accessRemote("retrieveByID",ID, resultsMethod, statusMethod);     }     // validation         public function validateData():Boolean{                 if(!validateNonZeroNumber(this.getNumIssues())){                         return false;                 } else if (!validateNonZeroNumber(this.getPrice())){                         return false;                 } else {                         return true;                 }         }         private function validateNonZeroNumber(number:Number):Boolean{                 if (isNaN(number)  Number(number) <= 0){                         return false;                 } else {                         return true;                 }         }     // setters     public function setValues(product:Product, numIssues:Number, price:Number,isActive:Boolean):Void{         this.setProduct(product);         this.setNumIssues(numIssues);         this.setPrice(price);         this.setActive(isActive);     }     public function setProduct(product:Product):Void{         this.product = product;     }     public function setNumIssues(numIssues:Number):Void{         this.numIssues = numIssues;     }     public function setPrice(price):Void{         this.price = price;     }     public function setActive(isActive:Boolean):Void{         this.isActive = isActive;     }     // getters     public function serializeValues():Object{         var tmpObject = new Object();         tmpObject.productID = this.getProduct.getID();         tmpObject.numIssues = this.getNumIssues();         tmpObject.price = this.getPrice();         tmpObject.active = this.getActive();         return tmpObject;     }     public function getProduct():Product{         return this.product;     }     public function getNumIssues():Number{         return this.numIssues;     }     public function getPrice():Number{         return this.price;     }     public function getActive():Boolean{         return this.isActive;     } } 

The core services of this class closely mirror that of the Product class. The specific properties of an offer are declared first, along with their datatypes. One thing to note here is that the Offer class will use an instance of the Product class as a property called product .

Next, the static properties are listed. These are nearly identical to those of the Product class, with the exception of the arguments passed into the constructor of the DataAccess class. If other classes continue to follow these conventions, it may be better to create a superclass that implements our data access methods. As was discussed in Chapter 5, "The Meek Shall Inherit the Earth," it often makes sense to create a superclass when more than one class has similar methods and properties and those classes can all be effectively described as something else. So far, we could safely describe both products and offers as persistable objects.

After all the instance and static properties are declared, the constructor method is shown, which simply takes in arguments and passes them to the set values method for the class.

The constructor is followed by the data access methods. These closely mirror the data access methods from the Product class.

Next, the validation methods are defined. The Offer class uses only a single validation method, validateNonZeroNumber , which is similar to the validateNumber method from the Product class. With this defined, it's starting to become apparent that it would make more sense to add a superclass to both the Product class and the Offer class, which also contains the validation routines for each. We'll investigate this shortly.

The final section of the Offer class contains the getter and setter functions. Just like the Product class, there is one setValues() method that takes all the properties and uses the setter methods, and there is one serializeValues() method that uses all the getter methods to return a flat snapshot of the object. With the data access properties and methods as well as validation routines, we have started to identify certain redundancies between the Product and Offer classes. To simplify the construction and maintenance of the application, we will refactor both, and move the common methods to a superclass of each, which we will call PersistableObject . Our first attempt at the PersistableObject class is shown in Listing 16.7.

Listing 16.7. The PersistableObject Class Will Act as a Superclass to Any Classes Needing Data Access and Validation Behaviors
 class PersistableObject{        // static properties        private static var dataAccessObject:DataAccess;        private var validateData:Function;        private var serializeValues:Function;        //empty constructor        function PersistableObject(){}        // data access methods       public function insert(resultsMethod:Function, statusMethod:Function):Boolean{              if(this.validateData()){ PersistableObject.dataAccessObject.accessRemote("create",this.serialize Values(),resultsMethod, statusMethod);                    return true;              } else {                    trace("Data invalid");                    return false;              }        }       public function search(propertyObj:Object, resultsMethod:Function, statusMethod:Function):Void{ PersistableObject.dataAccessObject.accessRemote("retrieveByProp", propertyObj, resultsMethod, statusMethod);              }       public static function getById(ID:Number, resultsMethod:Function, statusMethod:Function):Void{ PersistableObject.dataAccessObject.accessRemote("retrieveByID",ID, resultsMethod, statusMethod);             }        // validation methods        private function validateString(string:String):Boolean{                if(string.length == 0){                      return false;               } else {                            for(var i:Number=0;i<string.length;i++){                             if(string.charAt(i) != " "){                                    return true;                                    break;                             }                      }               }        }        private function validateNumber(number:Number):Boolean{               if (isNaN(number)){                      return false;               } else {                      return true;               }        }        private function validateNonZeroNumber(number:Number):Boolean{               if (!validateNumber(number)  number <= 0){                      return false;               } else {                      return true;               }              } } 

The PersistableObject class simply defines our static properties, validation routines, and the common data access methods ( insert() , getByID() , and search() ). One thing to note is that anything specific to the Product or Offer classes is not defined within the PersistableObject class, but instead is left to be implemented at the specific class level.

For this reason the update() method is not implemented here; as it currently stands, the update() method takes specific arguments for each class. With a bit more reworking, we could create a separate method for updating the objects data within each class, and then have an update() method in the PersistableObject class as well, but for simplicity sake, we will keep that one data access method to be implemented within each subclass.

The validateData () method of the Offer and Product classes is not implemented in PersistableObject , as they specifically determine how the individual properties of those classes will be validated.

However, the methods validateData() uses are implemented within the superclass. The validateNonZeroNumber () method has also been modified to make use of the validateNumber () method to determine if the argument is indeed a number, before checking to see if it is greater than zero. Next, we need to tell the Product and Offer classes that they are to inherit from PersistableObject , and any code moved to the superclass will be removed from these two classes. The modified versions of the Product and Offer classes can be seen in Listing 16.8.

Listing 16.8. Product and Offer Classes Are Modified to Extend the PersistableObject Class
 class Product  extends PersistableObject  {       // instance properties       private var name:String;       private var publisher:String;       private var singleIssuePrice:Number;       private var description:String;       private var prodID:Number;       //static properties       private static var dataAccessObject:DataAccess = new DataAccess("http://www.tapper.net/oopas2/components/product.cfc?wsdl");       //consturctor       function Product(name:String,publisher:String,price:Number,description:String){              setValues(name,publisher,price,description);       }  //data access functions   public function update (name:String, publisher:String, price:Number, description:String, resultsMethod:Function, statusMethod:Function){   setValues(name,publisher,price,description);   if(this.validateData()){   Product.dataAccessObject.accessRemote(  "  update  "  ,this.serializeValues());   }   }   //validation   public function validateData():Boolean{   var isValid:Boolean = true;   if(!validateString(this.getName())){   return false;   } else if (!validateString(this.getPublisher())){   return false;   } else if (!validateNumber(this.getSingleIssuePrice())){   return false;   } else {   return true;   }   }  // setter functions       public function setValues(name:String,publisher:String,price:Number,description:String){             setName(name);             setPublisher(publisher);             setSingleIssuePrice(price);             setDescription(description);       }       public function setName(name:String):Void{             this.name = name;       }       public function setPublisher(publisher:String):Void{             this.publisher= publisher;       }       public function setSingleIssuePrice(price:Number):Void{             this.singleIssuePrice= price;       }       public function setDescription(description:String):Void{             this.description = description;       }       // getter functions       public function serializeValues():Object{             var tmpObject = new Object();             tmpObject.name = this.getName();             tmpObject.publisher = this.getPublisher();             tmpObject.singleIssuePrice = this.getSingleIssuePrice();             tmpObject.description = this.getDescription();             return tmpObject;       }       public function getName():String{             return this.name;       }       public function getPublisher():String{             return this.publisher;       }       public function getSingleIssuePrice():Number{             return this.singleIssuePrice;       }       public function getDescription():String{             return this.description;       }       public function getID():Number{             return this.prodID;       } } class Offer  extends PersistableObject  {       private var offerID:Number;       private var product:Product;       private var numIssues:Number;       private var price:Number;       private var isActive:Boolean = false;       //static properties       private static var dataAccessObject:DataAccess = new DataAccess("http://www.tapper.net/oopas2/components/product.cfc?wsdl");       public function Offer(product:Product,numIssues:Number,price:Number,isActive:Boolean){             this.setValues(product,numIssues,price,isActive);       }  //data access functions   public function update (product:Product,numIssues:Number,price:Number,isActive:Boolean, resultsMethod:Function, statusMethod:Function):Boolean{   this.setValues(product,numIssues,price,isActive);   if(this.validateData()){   Offer.dataAccessObject.accessRemote("update", this.serializeValues(), resultsMethod, statusMethod);   return true;   } else {   return false;   }   }   // validation   public function validateData():Boolean{   if(!validateNonZeroNumber(this.getNumIssues())){   return false;   } else if (!validateNonZeroNumber(this.getPrice())){   return false;   } else {   return true;   }   }  // setters       public function setValues(product:Product,numIssues:Number,price:Number,isActive:Boolean):Void{              this.setProduct(product);              this.setNumIssues(numIssues);              this.setPrice(price);              this.setActive(isActive);       }       public function setProduct(product:Product):Void{             this.product = product;       }       public function setNumIssues(numIssues:Number):Void{             this.numIssues = numIssues;       }       public function setPrice(price):Void{             this.price = price;       }       public function setActive(isActive:Boolean):Void{             this.isActive = isActive;       }       // getters       public function serializeValues():Object{             var tmpObject = new Object();             tmpObject.productID = this.getProduct.getID();             tmpObject.numIssues = this.getNumIssues();             tmpObject.price = this.getPrice();             tmpObject.active = this.getActive();             return tmpObject;       }       public function getProduct():Product{             return this.product;       }       public function getNumIssues():Number{             return this.numIssues;       }       public function getPrice():Number{             return this.price;       }       public function getActive():Boolean{             return this.isActive;       } } 

With the Products and Offers ready, we should next consider the customer.

Customer

A customer is a person who buys subscriptions from the company. They are a key aspect of the system, as without customers, the company would cease to exist. The following use cases will help understand how users of the system interact with a customer.

Use Case

Use Case: Search for a customer

Actors: CCR and PCM

Preconditions: Actor has been authenticated to the system and identified as a member of one of the groups listed as actors.

Primary Flow:

  1. Actor receives a phone call from a customer.

  2. A screen is presented to the actor with the following fields:

    • First name

    • Last name

    • Street address

    • City

    • Combo box with all U.S. states

    • Zip code

    • Phone number (divided into two fields, area code and phone number)

  3. As actor fills out screen, each time he or she clicks away from the Last name or Phone number fields, those fields will be passed to the search engine.

  4. Matching customers are presented to actor.

  5. Actor chooses a customer from the list.

  6. Based on role, actor is either presented with a screen to view the customer (CCR) or is presented with a menu allowing him or her to either edit customer or add subscription. They will be redirected to appropriate use case based on this choice.

Alternative Flows:

4a. If there are no matching customers, actors can complete the form, click the Insert Customer button, and continue with the "Insert Customer" use case.

Use Case: Insert customer

Actors: CCR

Preconditions:

  • Actor has been authenticated to the system and identified as a member of the Subscription Manager group.

  • New customer has called call center with the intention of subscribing to a magazine.

  • Actor has searched for a record matching a customer and found no matches.

Primary Flow:

  1. Actor fills out screen and clicks button to insert customer.

  2. Validation routines are run over inputs, assuring that firstname , lastname, streetAddress, city, and zip are non-empty strings, that area code is a three-digit number, and that phone number is a seven-digit number.

  3. Once validated, data is sent to server to be inserted.

  4. After successful insertion by server, server will return a message to the client indicating successful insertion.

  5. Actor will then proceed to the "Add Subscription" use case.

Alternative Flows:

2a. If any of the fields fail validation routines, a message box will alert the actor of the problems.

2b. When actor closes the message box, focus will be given to the first field that failed the validation routines.

4a. If server returns a message other than that the data was successfully inserted, the actor will be notified that there was a problem inserting the data and prompted with a link to mail the issue to the applications administrator.

Use Case: Edit customer

Actors: CCR

Preconditions:

  • Actor has been authenticated to the system, and identified as a member of the applicable group(s).

  • Customer has called call center and been identified by the actor.

  • Actor has searched for a record matching a customer and chosen to edit the customer.

Primary Flow:

  1. Actor is presented with a prefilled screen with the following fields:

    • First name

    • Last name

    • Street address

    • City

    • Combo box with all U.S. states

    • Zip code

    • Phone number (divided into two fields, area code and phone number)

  2. Actor edits data on the screen and clicks button to update customer.

  3. Validation routines are run over inputs, assuring that firstname, lastname, streetaddress, city, and zip are non-empty strings, that area code is a three-digit number, and that phone number is a seven-digit number.

  4. Once validated, data is sent to server to be inserted.

  5. After successful insertion by server, server will return a message to the client indicating successful insertion.

  6. Actor will be presented with menu of options for that customer.

Alternative Flows:

3a. If any of the fields fail validation routines, a message box will alert the actor of the problems.

3b. When actor closes the message box, focus will be given to the first field that failed validation routines.

5a. If server returns a message other than that the data was successfully inserted, the actor will be notified that there was a problem inserting the data and prompted with a link to mail the issue to the applications administrator.

Sequence Diagram

From these use cases, it can clearly be seen that the Customer class is central to the application. Further details about this class can be derived by examining its interactions through the use of a sequence diagram, as seen in Figure 16.8.

Figure 16.8. The sequence diagram for the Customer class shows interactions between users of the system and customers.

graphics/16fig08.gif

Class Diagram

Much like the previous classes, the core interactions with the Customer class involve methods to create, update, validate, find, and view customers. These methods and the necessary properties are enumerated in the class diagram shown in Figure 16.9.

Figure 16.9. The properties and methods of the Customer class are enumerated in the class diagram.

graphics/16fig09.gif

Learning from our experiences with the Product and Offer classes, we will implement Customer as a subclass of PersistableObject . This will provide the core infrastructure for validation and data access, leaving us the rest of the infrastructure to define, such as properties, constructor, getters, and setters. The first iteration of the Customer class is shown in Listing 16.9

Listing 16.9. The Customer Class Is Built as a Subclass of PersistableObject
 class Customer extends PersistableObject{     // instance properties     private var firstName:String;     private var lastName:String;     private var areaCode:Number;     private var phoneNum:Number;     private var street1:String;     private var city:String;     private var state:String;     private var zipCode:String;     //static properties     private static var dataAccessObject:DataAccess = new DataAccess("http://localhost:8500/oopas2/components/Customer.cfc? wsdl");     //constructor     public function Customer(firstName:String, lastName:String, areaCode:Number, phoneNum:Number, street1:String, city:String, state:String, zipCode:String){         this.setValues(firstName, lastName, areaCode, phoneNum, street1, city, state,zipCode);     } //data access functions     public function update(firstName:String, lastName:String, areaCode:Number, phoneNum:Number, street1:String, city:String, state:String, zipCode:String, resultsMethod:Function, statusMethod:Function):Boolean{ this.setValues(firstName,lastName,areaCode,phoneNum,street1,city,state, zipCode);         if(this.validateData()){ Customer.dataAccessObject.accessRemote("update",this.serializeValues(), resultsMethod, statusMethod);             return true;         } else {             return false;         }     }     //validation     public function validateData():Boolean{         if(!validateString(this.getFirstName())){             return false;         } else if (!validateString(this.getLastName())){             return false;         }  else if (!validateNumberForLength(this.getAreaCode(),3)){             return false;         } else if (!validateNumberForLength(this.getPhoneNum(),7)){             return false;         } else if (!validateString(this.getStreet1())){             return false;         }  else if (!validateString(this.getCity())){             return false;         }  else if (!validateString(this.getState())){             return false;         }  else if (!validateString(this.getZipCode())){             return false;         } else {             return true;             }         }         // setter functions     public function setValues(firstName:String, lastName:String, areaCode:Number, phoneNum:Number, street1:String, city:String, state:String, zipCode:String):Void{         this.setFirstName(firstName);         this.setLastName(lastName);         this.setAreaCode(areaCode);         this.setPhoneNum(phoneNum);         this.setStreet1(street1);         this.setCity(city);         this.setState(state);         this.setZipCode(zipCode);     }     public function setFirstName(firstName:String):Void{         this.firstName = firstName;     }     public function setLastName(lastName:String):Void{         this.lastName = lastName;     }     public function setAreaCode(areaCode:Number):Void{         this.areaCode = areaCode;     }     public function setPhoneNum(phoneNum:Number):Void{         this.phoneNum = phoneNum;     }     public function setStreet1(street1:String):Void{         this.street1 = street1;     }     public function setCity(city:String):Void{         this.city = city;     }     public function setState(state:String):Void{         this.state = state;     }     public function setZipCode(zipCode:String):Void{         this.zipCode = zipCode;     }     // getter functions     public function serializeValues():Object{         var tmpObject = new Object();         tmpObject.firstName = this.getFirstName();         tmpObject.lastName = this.getLastName();         tmpObject.areaCode = this.getAreaCode();         tmpObject.phoneNum = this.getPhoneNum();         tmpObject.street1 = this.getStreet1();         tmpObject.city = this.getCity();         tmpObject.state = this.getState();         tmpObject.zipCode = this.getZipCode();         return tmpObject;     }     public function getFirstName():String{         return this.firstName;     }     public function getLastName():String{         return this.lastName;     }     public function getAreaCode():Number{         return this.areaCode;     }     public function getPhoneNum():Number{         return this.phoneNum;     }     public function getStreet1():String{         return this.street1;     }     public function getCity():String{         return this.city;     }     public function getState():String{         return this.state;     }     public function getZipCode():String{         return this.zipCode;     } } 

Structurally, this class is just like the ones defined earlier. It begins by declaring the class Customer as a subclass of PersistableObject , which defines the validation and data access details. This is followed by the declarations of the instance properties and the datatypes of each property. The properties and their types are defined exactly as they were designed in the class diagram in Figure 16.9. Next, the specific dataAccessObject property for this class is created as a static property, with the URL for the server-side Customer object passed as an argument.

After all the properties are defined, the constructor is declared. Much like the constructors of the previous classes, the arguments are passed to a setValues() method to reduce duplication of effort across the constructor and update methods.

The update() method definition follows . Like the classes previously defined, the update() method will validate the data before passing it on to the accessRemote() method of the dataAccessObject . Next, the data validation method is defined. For this class, we needed a new validation rule to handle validating phone numbers and area codes. According to the use cases, not only do those fields need to be numeric, but also they are required to have a certain number of characters. To facilitate this, we will add a new method to the PersistableObject class, named validateNumberForLength() . This method is shown in Listing 16.10. The Customer class is completed with the definitions for the getter and setter methods for the class.

Listing 16.10. The PersistableObject Class Is Enhanced with the Addition of a validateNumberForLength() Method
 class PersistableObject{      // static properties      private static var dataAccessObject:DataAccess;      private var validateData:Function;      private var serializeValues:Function;      //empty constructor      function PersistableObject(){}      // data access methods     public function insert(resultsMethod:Function, statusMethod:Function):Boolean{         if(this.validateData()){ PersistableObject.dataAccessObject.accessRemote("create",this.serialize Values(),resultsMethod, statusMethod);             return true;         } else {             trace("Data invalid");             return false;         }     }     public function search(propertyObj:Object, resultsMethod:Function, statusMethod:Function):Void{ PersistableObject.dataAccessObject.accessRemote("retrieveByProp", propertyObj, resultsMethod, statusMethod);         }     public static function getById(ID:Number, resultsMethod:Function, statusMethod:Function):Void{ PersistableObject.dataAccessObject.accessRemote("retrieveByID",ID, resultsMethod, statusMethod);         }      // validation methods      private function validateString(string:String):Boolean{           if(string.length == 0){                return false;           } else {                for(var i:Number=0;i<string.length;i++){                     if(string.charAt(i) != " "){                          return true;                          break;                     }                }           }      }      private function validateNumber(number:Number):Boolean{           if (isNaN(number)){                return false;           } else {                return true;           }      }      private function validateNonZeroNumber(number:Number):Boolean{           if (!validateNumber(number)  number <= 0){                return false;           } else {                return true;           }      }  private function validateNumberForLength(number:Number, length:Number):Boolean{   if (!validateNumber(number)){   return false;   } else if (String(number).length != length){   return false;   } else {   return true;   }   }  } 

The new method, validateNumberForLength() , begins by using the validateNumber() method to check whether the specified value is indeed a number. If that test is passed, the value's length is checked by casting it as a String and using the length property of the String class. If the length matches, the method returns true ; otherwise, it fails and returns false.

The Employee Class

We now need to create a class to represent the users of the system. We need to detail only a single use case. It describes how an employee is authenticated to the system.

Use Case

Use Case: Log in

Actors: CCR, PCM, SM, and DER

Preconditions: None

Primary Flow:

  1. Actor launches application.

  2. A screen is presented to the actor with the following fields:

    • User name

    • Password

  3. When fields are filled, actor clicks the Submit" button.

  4. Server attempts to authenticate actor.

  5. If authenticated, user is redirected to the primary page for the Users group.

Alternative Flows:

4a. If username and password do not match username and password on the server, user is redirected to login screen and presented with a message that he or she did not enter a valid username and password.

Sequence Diagram

The only use case identified here for the Employee class deals with how they are authenticated to the system. Let's look at the sequence diagram (Figure 16.10) showing how that login routine works within the system.

Figure 16.10. The sequence diagram for the Employee class shows interactions between employees and the system.

graphics/16fig10.gif

Class Diagram

The login routine diagrammed here is pretty straightforward. It passes the login data to the data access object, which in turn passes it on to the server-side method. While not specifically required for the application, a few more properties and methods will be added when we create the class diagram, as we know that employees are at their core, people, and therefore have some inherent properties, such as name, address, and phone number. These are detailed along with the login specific properties and methods in Figure 16.11.

Figure 16.11. The initial version of the Employee class is very simple, yet its similarity to the Customer class may hint at a better solution.

graphics/16fig11.gif

Comparing the Employee class diagram to that of the Customer class shows some similarities. Both have names , addresses, and phone numbers, and both have methods for retrieving them. This might seem a good place for a Person superclass, which implements the common properties and methods of both Employee and Customer . However, there is a key difference between the two: customers are a PersistableObject , meaning that the system will be responsible for creating and updating instances of Customer . Employees, however, are not a PesistableObject ; the means to add, update, and delete employees exists in a separate HR application, which has no bearing on the application being designed here. The only overlap between the two applications is that the application designed here will use the Web Services the HR application has exposed for authenticating users and retrieving their information. That web service exposes methods to allow for neither creating nor updating employees.

Without improperly forcing the Employee class to subclass PersistableObject , it won't be possible to have both the Employee and Customer classes inherit from a common superclass;

ActionScript 2.0 does not allow multiple inheritance. The only remaining way to enforce the commonalities between these classes will be to create an interface that both classes can implement.

N OTE

For details on interfaces, see Chapter 6, "Understanding Interfaces in ActionScript 2.0."


Because a class cannot inherit from more than one superclass in ActionScript 2.0, we will instead implement an interface to ensure that common methods such as getFullName , getPhone , and getAddress exist for all people, whether they are part of the Employee or Customer class. With this interface in place, the existing Customer class needs to be refactored to implement the interface, and the newly created Employee class also needs to be set to implement it.

Figure 16.12 shows the modified class diagrams for the Employee and Customer classes, including a newly defined interface, Person .

Figure 16.12. An interface Person is created to enforce that both Employees and Customers have common methods.

graphics/16fig12.gif

Listing 16.11 shows the new Person interface, the Employee class, and the modifications to the Customer class. For the Customer , the changes are shown in bold.

Listing 16.11. The Employee Class, with Person Defined
 interface Person{     public function getFullName():String;     public function getPhone():String;     public function getAddress():String; } class Employee implements Person{       private var empId:Number;       private var firstName:String;       private var lastName:String;       private var areaCode:Number;       private var phoneNum:Number;       private var street:String;       private var city:String;       private var state:String;       private var zipCode:String;       private var userName:String;       private var password:String;       private static var dataAccessObject:DataAccess = new DataAccess("http://localhost:8500/oopas2/components/Employee.cfc?wsdl");       public function Employee(firstName:String, lastName:String, areaCode:Number, phoneNum:Number, street1:String, city:String, state:String, zipCode:String){             this.setValues(firstName, lastName, areaCode, phoneNum, street1, city, state,zipCode);       }       // data access       public function login(userName:String, password:String, resultsMethod:Function, statusMethod:Function):Void{ Employee.dataAccessObject.accessRemote("login",[userName,password], resultsMethod, statusMethod);       }       //interface functions       public function getFullName():String{             return this.getFirstName() + " " + this.getLastName();       }       public function getAddress():String{             var theAddress = this.getStreet + newline + this.getCity() + " " + this.getState() + ", " + this.getZipCode();       return theAddress;       }       public function getPhone():String{             return "(" + this.getAreaCode() +") " +this.getPhoneNum() ;       }       // setter functions       public function setValues(firstName:String, lastName:String, areaCode:Number, phoneNum:Number, street1:String, city:String, state:String, zipCode:String):Void{             this.setFirstName(firstName);             this.setLastName(lastName);             this.setAreaCode(areaCode);             this.setPhoneNum(phoneNum);             this.setStreet(street);             this.setCity(city);             this.setState(state);             this.setZipCode(zipCode);       }       public function setFirstName(firstName:String):Void{             this.firstName = firstName;       }       public function setLastName(lastName:String):Void{             this.lastName = lastName;       }       public function setAreaCode(areaCode:Number):Void{             this.areaCode = areaCode;       }       public function setPhoneNum(phoneNum:Number):Void{             this.phoneNum = phoneNum;       }       public function setStreet(street1:String):Void{             this.street = street1;       }       public function setCity(city:String):Void{             this.city = city;       }       public function setState(state:String):Void{             this.state = state;       }       public function setZipCode(zipCode:String):Void{             this.zipCode = zipCode;       }       // getter functions       public function serializeValues():Object{             var tmpObject = new Object();             tmpObject.firstName = this.getFirstName();             tmpObject.lastName = this.getLastName();             tmpObject.areaCode = this.getAreaCode();             tmpObject.phoneNum = this.getPhoneNum();             tmpObject.street = this.getStreet();             tmpObject.city = this.getCity();             tmpObject.state = this.getState();             tmpObject.zipCode = this.getZipCode();             return tmpObject;       }       public function getFirstName():String{             return this.firstName;       }       public function getLastName():String{             return this.lastName;       }       public function getAreaCode():Number{             return this.areaCode;       }       public function getPhoneNum():Number{             return this.phoneNum;       }       public function getStreet():String{             return this.street;       }       public function getCity():String{             return this.city;       }       public function getState():String{             return this.state;       }       public function getZipCode():String{             return this.zipCode;       } } class Customer extends PersistableObject  implements Person  {       // instance properties       private var firstName:String;       private var lastName:String;       private var areaCode:Number;       private var phoneNum:Number;       private var street1:String;       private var city:String;       private var state:String;       private var zipCode:String;       //static properties       private static var dataAccessObject:DataAccess = new DataAccess("http://localhost:8500/oopas2/components/Customer.cfc?wsdl");       //constructor       public function Customer(firstName:String, lastName:String, areaCode:Number, phoneNum:Number, street1:String, city:String, state:String, zipCode:String){             this.setValues(firstName, lastName, areaCode, phoneNum, street1, city, state,zipCode);       }  //interface functions   public function getFullName():String{   return this.getFirstName() + " " + this.getLastName();   }   public function getAddress():String{   var theAddress = this.getStreet1 + newline + this.getCity() + " " + this.getState() + ", " + this.getZipCode();   return theAddress;   }   public function getPhone():String{   return "(" + this.getAreaCode() +") " +this.getPhoneNum() ;   }  //data access functions       public function update(firstName:String, lastName:String, areaCode:Number, phoneNum:Number, street1:String, city:String, state:String, zipCode:String, resultsMethod:Function, statusMethod:Function):Boolean{ this.setValues(firstName,lastName,areaCode,phoneNum,street1,city,state, zipCode);             if(this.validateData()){ Customer.dataAccessObject.accessRemote("update",this.serializeValues(), resultsMethod, statusMethod);                   return true;             } else {                   return false;             }       }       //validation       public function validateData():Boolean{             if(!validateString(this.getFirstName())){                   return false;             } else if (!validateString(this.getLastName())){                   return false;             }  else if (!validateNumberForLength(this.getAreaCode(),3)){                   return false;             } else if (!validateNumberForLength(this.getPhoneNum(),7)){                   return false;             } else if (!validateString(this.getStreet1())){                   return false;             }  else if (!validateString(this.getCity())){                   return false;             }  else if (!validateString(this.getState())){                   return false;             }  else if (!validateString(this.getZipCode())){                   return false;             } else {                   return true;                   }             }             // setter functions public function setValues(firstName:String, lastName:String, areaCode:Number, phoneNum:Number, street1:String, city:String, state:String, zipCode:String):Void{             this.setFirstName(firstName);             this.setLastName(lastName);             this.setAreaCode(areaCode);             this.setPhoneNum(phoneNum);             this.setStreet1(street1);             this.setCity(city);             this.setState(state);             this.setZipCode(zipCode);       }       public function setFirstName(firstName:String):Void{             this.firstName = firstName;       }       public function setLastName(lastName:String):Void{             this.lastName = lastName;       }       public function setAreaCode(areaCode:Number):Void{             this.areaCode = areaCode;       }       public function setPhoneNum(phoneNum:Number):Void{             this.phoneNum = phoneNum;       }       public function setStreet1(street1:String):Void{             this.street1 = street1;       }       public function setCity(city:String):Void{             this.city = city;       }       public function setState(state:String):Void{             this.state = state;       }       public function setZipCode(zipCode:String):Void{             this.zipCode = zipCode;       }       // getter functions       public function serializeValues():Object{             var tmpObject = new Object();             tmpObject.firstName = this.getFirstName();             tmpObject.lastName = this.getLastName();             tmpObject.areaCode = this.getAreaCode();             tmpObject.phoneNum = this.getPhoneNum();             tmpObject.street1 = this.getStreet1();             tmpObject.city = this.getCity();             tmpObject.state = this.getState();             tmpObject.zipCode = this.getZipCode();             return tmpObject;       }       public function getFirstName():String{             return this.firstName;       }       public function getLastName():String{             return this.lastName;       }       public function getAreaCode():Number{             return this.areaCode;       }       public function getPhoneNum():Number{             return this.phoneNum;       }       public function getStreet1():String{             return this.street1;       }       public function getCity():String{             return this.city;       }       public function getState():String{             return this.state;       }       public function getZipCode():String{             return this.zipCode;       } } 

First, we see the newly defined Person interface, which requires that all implementing classes have methods to retrieve the person's nameone to retrieve the address and one to retrieve the phone number. Next, the refactored portions of the Customer class are shown, including the class definition, which now includes the command to implement the Person interface. Also shown are the specific methods required by the interface and their specific implementation for Customers .

The interface definition is followed by the newly created Employee class. Notice that the Employee class definition specifies that it will implement the Person interface. The class definition is very similar to the classes previously defined, except Employee does not extend PersistableObject class and has no validation rules. The only data access method defined is the login() method, which passes a username and password to the login() method on the server. The getter and setter methods are straightforward and closely mirror the structure of the previously defined classes.

The Employee class is followed by the modified Customer class. In Listing 16.11, the changes from the previous definitions of this class are set in bold.

With the addition of the ability to authenticate users, all that remains to be defined is the Subscription class. We discuss that next.

The Subscription Class

When a customer signs up for one of the offers, a subscription is created for that customer.

Use Case

Use Case: Create subscription

Actors: CCR

Preconditions:

  • Actor has been authenticated to the system and identified as a member of a group listed previously.

  • Customer has called, indicating he or she wishes to subscribe to a magazine.

  • Customer record has either been found or created.

Primary Flow:
  1. Actor is presented with screen containing a menu of all products.

  2. Actor chooses a product from the menu and is shown a second menu, containing all active offers for that product.

  3. Actor clicks button indicating that the chosen offer is the one to which the customer wishes to subscribe.

  4. Actor is presented with a verification screen, showing the customer and the address on file for that customer. Actor will check if the address is both the shipping address and the billing address for the customer.

  5. Actor confirms the subscription details, telling the customer that he or she will be billed at their billing address soon and that the subscription will begin shortly.

Alternative Flows

1a. Actor can choose to see details of currently selected product.

2a. Actor can choose to see details of currently selected offer.

5a. Actor can enter a separate billing address or change the customer's ship ping address.

Use Case: Search and view subscription

Actors: CCR and PCM

Preconditions:

  • Actor has been authenticated to the system and identified as a member of a group listed.

Primary Flow:

  1. Actor requests to search subscriptions.

  2. Actor is presented with an interface representing the various aspects on which he or she can search:

    • Text input for customer name

    • Menu populated with products

    • Menu populated with offers (which shows offers only for the currently selected product in the product menu)

  3. Actor clicks button to initiate a search.

  4. Matching results are shown to the actor.

  5. Actor can choose a single subscription from the matching results.

  6. All details of chosen subscription are shown to actor.

Alternative Flows:

4a. If no subscriptions match the search criteria, actor is informed that there are no results.

Sequence Diagram

Subscriptions are potentially the most difficult of the system entities to understand because they interact with nearly every other entity we have designed. Looking to the sequence diagrams in Figure 16.13 shows the interactions necessary to implement the use cases.

Figure 16.13. The sequence diagram for the Subscription class shows that it requires interactions with the Offers and Products classes.

graphics/16fig13.gif

Class Diagram

As we begin to define the class diagram for the Subscription class, we find a few shortcomings in the other classes which will need to be addressed. There are many reasons for this, among them, a subscription will need an end date; we also need a way to determine how frequently issues are sent so that the number of issues for an offer can be computed to a certain length of time. To facilitate this, a new property of Product is added, called issuesPerYear . To make use of this from the Offer class, a new method named getDuration() is added. It will return the number of months for which the issues will be sent for a particular offer. (Considering that different publications are published on different schedules, 12 issues of a weekly will last only three months, while 12 issues of a monthly will last a year.)

A second change that impacts several classes is the need for a subscription to have both a billing and shipping address. With this information, XYZ Subscriptions can send the bills for a given subscription to an address that is different than the shipping address. There are different possible approaches to this.

One approach would be to store an array of addresses for each customer and then store the array index for those values in the subscription. While this approach is valid, the fact remains that the existing data structure (from the legacy application) for a customer has only a single address record. To have a minimal impact on the existing database, but yet retain the maximum flexibility for the new system, an Address class will be created. The class will store all the address fields (street, city, state, and zip). Then, any classes needing to make use of an address will have one of their properties assigned an instance of the Address class.

To implement the Address class, we will need to go back and refactor the Customer and Employee classes (both of which made use of addresses) as well as the Person interface, which initially required the getAddress() methods to return a string. Instead, the getAddress() methods of classes implementing that interface will return an instance of the Address class. Lastly, the properties shippingAddress and billingAddress will be added to the Subscription class, and each will hold an instance of the Address class. With the knowledge of the changes that need to be made, we refactor the classes and derive the class diagram, shown in Figure 16.14.

Figure 16.14. In creating the class diagram for the Subscription class, nearly every other class has been refactored.

graphics/16fig14.gif

After the structure of the Subscription class is understood, as well as the other changes, we can make an attempt at implementing them. Listing 16.12 shows the new and refactored classes.

Listing 16.12. The Definition of the Subscription Class Necessitates Refactoring Many Other Classes
 class Subscription extends PersistableObject{       private var offer:Offer;       private var customer:Customer;       private var startDate:Date;       private var endDate:Date;       private var billingAddress:Address;       private var shippingAddress:Address;       private static var dataAccessObject = new DataAccess("http://localhost:8500/oopas2/components/Subscription.cfc?wsdl");       public function Subscription(offer:Offer, customer:Customer, startDate:Date, billingAddress:Address, shippingAddress:Address){             setOffer(offer);             setCustomer(customer);             setStartDate(startDate);             setBillingAddress(billingAddress);             setShippingAddress(shippingAddress);             setEndDate(this.computeEndDate());       }       private function computeEndDate():Date{             var year:Number = this.startDate.getFullYear();             var endMonth:Number = this.startDate.getMonth() + this.offer.getDuration();             if (endMonth > 11){                   var yearsOver = Math.floor(endMonth/12);                   year += yearsOver;                   endMonth -= (yearsOver*12);             }             return new Date(year,endMonth,startDate.getDate());       }       //setter functions       public function setOffer(offer:Offer){             this.offer = offer;       }       public function setCustomer(customer:Customer){             this.customer = customer;       }       public function setBillingAddress(billingAddress:Address){             this.billingAddress = billingAddress;       }       public function setShippingAddress(shippingAddress:Address){             this.shippingAddress = shippingAddress;       }       public function setStartDate(startDate:Date){             this.startDate = startDate;       }       public function setEndDate(endDate:Date){             this.endDate = endDate;       }       //setter functions       public function getOffer():Offer{             return this.offer;       }       public function getCustomer():Customer{             return this.customer;       }       public function getStartDate():Date{             return this.startDate;       }       public function getEndDate():Date{             return this.endDate;       } } class Product extends PersistableObject{       // instance proprerties       private var name:String;       private var publisher:String;       private var singleIssuePrice:Number;       private var description:String;       private var prodID:Number;  private var issuesPerYear:Number;  //static properties       private static var dataAccessObject:DataAccess = new DataAccess("http:/localhost:8500/oopas2/components/product.cfc?wsdl");       //consturctor       function Product(name:String, publisher:String ,price:Number, description:String,  issuesPerYear:Number  ){             setValues(name,publisher,price,description,  issuesPerYear  );       }       //data access functions           public function update (name:String, publisher:String, price:Number, description:String,  issuesPerYear:Number  , resultsMethod:Function, statusMethod:Function){ setValues(name,publisher,price,description,  issuesPerYear  );                     if(this.validateData()){ Product.dataAccessObject.accessRemote("update",this.serializeValues());                     }           }       //validation       public function validateData():Boolean{             var isValid:Boolean = true;             if(!validateString(this.getName())){                   return false;             } else if (!validateString(this.getPublisher())){                   return false;             } else if (!validateNumber(this.getSingleIssuePrice())){                   return false;             }  else if (!validateNonZeroNumber(this.getIssuesPerYear())){   return false;  } else {                   return true;             }       }       // setter functions       public function setValues(name:String,publisher:String,price:Number,description:String,  issuesPerYear:Number  ){              setName(name);              setPublisher(publisher);              setSingleIssuePrice(price);              setDescription(description);  setIssuesPerYear(issuesPerYear);  }       public function setName(name:String):Void{             this.name = name;       }       public function setPublisher(publisher:String):Void{             this.publisher= publisher;       }       public function setSingleIssuePrice(price:Number):Void{             this.singleIssuePrice= price;       }       public function setDescription(description:String):Void{             this.description = description;       }  public function setIssuesPerYear(issuesPerYear:Number):Void{   this.issuesPerYear = issuesPerYear;   }  // getter functions       public function serializeValues():Object{             var tmpObject  = new Object();             tmpObject.name = this.getName();             tmpObject.publisher = this.getPublisher();             tmpObject.singleIssuePrice = this.getSingleIssuePrice();             tmpObject.description = this.getDescription();  tmpObject.issuesPerYear = this.getIssuesPerYear();  return tmpObject;       }       public function getName():String{             return this.name;       }       public function getPublisher():String{             return this.publisher;       }       public function getSingleIssuePrice():Number{             return this.singleIssuePrice;       }       public function getDescription():String{             return this.description;       }       public function getID():Number{             return this.prodID;       }  public function getIssuesPerYear():Number{   return this.issuesPerYear;   }  }class Address extends PersistableObject{       private var street1:String;       private var street2:String;       private var city:String;       private var state:String;       private var zipCode:String;       public function Address(street1:String,city:String,state:String,zipCode:String,street2: String){             this.setStreet1(street1);             this.setStreet2(street2);             this.setCity(city);             this.setState(state);             this.setZipCode(zipCode);       }       // validation       public function validateData():Boolean{             if (!validateString(this.getStreet1())){                   return false;             }  else if (!validateString(this.getCity())){                   return false;             }  else if (!validateString(this.getState())){                   return false;             }  else if (!validateString(this.getZipCode())){                   return false;             } else {                   return true;             }       }       // setter functions       public function setStreet1(street1:String):Void{             this.street1 = street1;       }       public function setStreet2(street2:String):Void{             this.street2 = street2;       }       public function setCity(city:String):Void{             this.city = city;       }       public function setState(state:String):Void{             this.state = state;       }       public function setZipCode(zipCode:String):Void{             this.zipCode = zipCode;       }       // getter functions       public function serializeValues():Object{             var tmpObject = new Object();             tmpObject.street1 = this.getStreet1();             tmpObject.street2 = this.getStreet2();             tmpObject.city = this.getCity();             tmpObject.state = this.getState();             tmpObject.zipCode = this.getZipCode();             return tmpObject;       }       public function getStreet1():String{             return this.street1;       }       public function getStreet2():String{             return this.street2;       }       public function getCity():String{             return this.city;       }       public function getState():String{             return this.state;       }       public function getZipCode():String{             return this.zipCode;       } } interface Person{       public function getFullName():String;       public function getPhone():String;       public function getAddress():  Address  ; } class Customer extends PersistableObject implements Person{       // instance properties       private var firstName:String;       private var lastName:String;       private var areaCode:Number;       private var phoneNum:Number;  private var address:Address;  //static properties       private static var dataAccessObject:DataAccess = new DataAccess("http://localhost:8500/oopas2/components/Customer.cfc?wsdl");       //constructor       public function Customer(firstName:String, lastName:String, areaCode:Number, phoneNum:Number,  address:Address  ){             this.setValues(firstName, lastName, areaCode, phoneNum,  address  );       }       //interface functions           public function getFullName():String{                     return this.getFirstName() + " " + this.getLastName();           }  public function getAddress():Address{   return this.address;   }  public function getPhone():String{                     return "(" + this.getAreaCode() +") " +this.getPhoneNum() ;           } //data access functions       public function update(firstName:String, lastName:String, areaCode:Number, phoneNum:Number,  address:Address  , resultsMethod:Function, statusMethod:Function):Boolean{ this.setValues(firstName,lastName,areaCode,phoneNum,  address  );             if(this.validateData()){ Customer.dataAccessObject.accessRemote("update",this.serializeValues(), resultsMethod, statusMethod);                   return true;             } else {                   return false;             }       }       //validation       public function validateData():Boolean{             if(!validateString(this.getFirstName())){                   return false;             } else if (!validateString(this.getLastName())){                   return false;             }  else if (!validateNumberForLength(this.getAreaCode(),3)){                   return false;             } else if (!validateNumberForLength(this.getPhoneNum(),7)){                   return false;             }  else if (!this.getAddress().validateData()){   return false;   }  else {                   return true;                   }             }             // setter functions       public function setValues(firstName:String, lastName:String, areaCode:Number, phoneNum:Number,  address:Address  ):Void{             this.setFirstName(firstName);             this.setLastName(lastName);             this.setAreaCode(areaCode);             this.setPhoneNum(phoneNum);  this.setAddress(address);  }       public function setFirstName(firstName:String):Void{             this.firstName = firstName;       }       public function setLastName(lastName:String):Void{             this.lastName = lastName;       }       public function setAreaCode(areaCode:Number):Void{             this.areaCode = areaCode;       }       public function setPhoneNum(phoneNum:Number):Void{             this.phoneNum = phoneNum;       }  public function setAddress(address:Address):Void{   this.address = address;   }  // getter functions       public function serializeValues():Object{             var tmpObject = new Object();             tmpObject.firstName = this.getFirstName();             tmpObject.lastName = this.getLastName();             tmpObject.areaCode = this.getAreaCode();             tmpObject.phoneNum = this.getPhoneNum();  tmpObject.address = this.getAddress().serializeValues();  return tmpObject;       }       public function getFirstName():String{             return this.firstName;       }       public function getLastName():String{             return this.lastName;       }       public function getAreaCode():Number{             return this.areaCode;       }       public function getPhoneNum():Number{             return this.phoneNum;       } } class Employee implements Person{       private var empId:Number;       private var firstName:String;       private var lastName:String;       private var areaCode:Number;       private var phoneNum:Number;  private var address:Address;  private var userName:String;       private var password:String;       private static var dataAccessObject:DataAccess = new DataAccess("http://localhost:8500/oopas2/components/Employee.cfc?wsdl");       public function Employee(firstName:String, lastName:String, areaCode:Number, phoneNum:Number,  address:Address  ){             this.setValues(firstName, lastName, areaCode, phoneNum,  address  );       }       // data access       public function login(userName:String, password:String, resultsMethod:Function, statusMethod:Function):Void{         Employee.dataAccessObject.accessRemote("login",[userName, password],resultsMethod, statusMethod);       }       //interface functions       public function getFullName():String{             return this.getFirstName() + " " + this.getLastName();       }       public function getAddress():  Address  {  return this.getAddress();  }       public function getPhone():String{            return "(" + this.getAreaCode() +") " +this.getPhoneNum() ;       }       // setter functions       public function setValues(firstName:String, lastName:String, areaCode:Number, phoneNum:Number,  address:Address  ):Void{             this.setFirstName(firstName);             this.setLastName(lastName);             this.setAreaCode(areaCode);             this.setPhoneNum(phoneNum);  this.setAddress(address);  }       public function setFirstName(firstName:String):Void{             this.firstName = firstName;       }       public function setLastName(lastName:String):Void{             this.lastName = lastName;       }       public function setAreaCode(areaCode:Number):Void{             this.areaCode = areaCode;       }       public function setPhoneNum(phoneNum:Number):Void{             this.phoneNum = phoneNum;       }  public function setAddress(address:Address):Void{   this.address = address;   }  // getter functions       public function serializeValues():Object{             var tmpObject = new Object();             tmpObject.firstName = this.getFirstName();             tmpObject.lastName = this.getLastName();             tmpObject.areaCode = this.getAreaCode();             tmpObject.phoneNum = this.getPhoneNum();  tmpObject.address = this.getAddress().serializeValues();  return tmpObject;       }       public function getFirstName():String{             return this.firstName;       }       public function getLastName():String{             return this.lastName;       }       public function getAreaCode():Number{             return this.areaCode;       }       public function getPhoneNum():Number{             return this.phoneNum;       } } 

As can be seen, nearly every class already defined has been refactored to implement subscriptions properly. Remember, as discussed back in Chapter 9, "Building and Using UI Components," refactoring is a natural process within object-oriented design. You shouldn't fear it; you should embrace it. It's much quicker and cheaper to make these changes now rather than later.

Listing 16.12 begins with the Subscription class. Like the rest, we begin by defining the properties, which match those shown in the class diagram. Next, in the constructor, all the properties, except for endDate , are passed in. The reason for the exception is that endDate is a computed field, based on the number of issues for an offer and the number of times a year a product is published. Therefore, in the final line of the Constructor, the endDate is set, like so:

 setEndDate(this.computeEndDate()); 

This is the final line of the constructor, so it can be known that the rest of the properties have already been properly set.

The constructor is followed by the definition of the computeEndDate() method. This method begins by determining the current year. Next, the duration of the subscription (derived by calling the getDuration method of the Offer instance) is added to the current month to begin computing the end month of the subscription. If the resulting sum is greater than 11 (remember that in ActionScript, December is seen as month 11 because months are a zero-indexed array), the number of years until the subscription expires is computed by dividing the sum of the months by 12 and rounding down. The number of years is added to the current year. Then, the number of years multiplied by 12 is subtracted from the end month to determine the correct ending month. Finally, the method returns a new instance of the Date class, using the computed year and month. The remaining methods are simple getter and setters.

After the end of the Subscription class definition, Listing 16.12 next shows the newly created method of the refactored Offer class. This method simply determines the number of issues per month a product will ship by dividing the number of issues per year by 12. Without this method, the subscription would not be able to determine an end date.

Next, the refactored Product class is shown. All the refactoring surrounds the declaration, getting and setting of the issuesPerYear property.

This definition is followed by a newly created Address class. This class is set as a subclass of PersistableObject , not because it will make use of a DataAccessObject on its own, but to allow use of the validation methods. There are no surprises to the Address class definition. It has a standard set of properties and a constructor, followed by data validation and the getters and setters. If you review where we started with the Customer class, the data validation, getters, and setters were lifted wholesale from their origins in the Customer class.

The new Address class definition is followed by the revised Person interface. All that has changed here is the return type of the getAddress() method. Rather than returning a string, it now returns an Address object. This, of course, necessitates a reworking of the Customer and Employee classes so that they can properly implement this interface.

The Customer class is addressed next. The properties are revised to show an address property which will be an instance of the Address class. This is done instead of using separate properties for each piece of an address. Then, the constructor is modified, showing the passing in of an Address object, which is then passed on to the setValues method. The update method has also been similarly revised.

Next, the validateData() method is revised. To validate an address, we will invoke the Address classes validateData() method. The setValues() method is modified to also set the address field. Then, a setAddress() method is created to assign new Address objects to instances of Customer . The modifications to the serializeValues() method simply use the Address classes serializeValues() method to embed an Address object into the tmpObject .

The final part of Listing 16.12 is the revised Employee class. The only changes to this class are the removal of the address specific properties (street, city, and so on) and the replacing of them with the new address property. This change is reflected in the properties declaration, the constructor, the update() method, the validation rules, and the getters and setters.

With all this in place, we're ready to pull it all together into a cohesive application.

 < Day Day Up > 


Object-Oriented Programming with ActionScript 2.0
Object-Oriented Programming with ActionScript 2.0
ISBN: 0735713804
EAN: 2147483647
Year: 2004
Pages: 162

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