Chapter 13. The Prototype Pattern

The Prototype pattern is another tool you can use when you can specify the general class needed in a program but need to defer the exact class until execution time. It is similar to the Builder in that some class decides what components or details make up the final class. However, it differs in that the target classes are constructed by cloning one or more prototype classes and then changing or filling in the details of the cloned class to behave as desired.

Prototypes can be used whenever you need classes that differ only in the type of processing they offer—for example, in parsing of strings representing numbers in different radixes. In this sense, the prototype is nearly the same as the Examplar pattern described by Coplien (1992).

Let’s consider the case of an extensive database where you need to make a number of queries to construct an answer. Once you have this answer as a result set, you might like to manipulate it to produce other answers without issuing additional queries.

In a case like the one we have been working on, we’ll consider a database of a large number of swimmers in a league or statewide organization. Each swimmer swims several strokes and distances throughout a season. The “best times” for swimmers are tabulated by age group, and even within a single four-month season many swimmers will pass their birthdays and fall into new age groups. Thus, the query to determine which swimmers did the best in their age group that season is dependent on the date of each meet and on each swimmer’s birthday. The computational cost of assembling this table of times is therefore fairly high.

Once we have an instance of a class containing this table sorted by sex, we could imagine wanting to examine this information sorted just by time or by actual age rather than by age group. It would not be sensible to recompute these data, and we don’t want to destroy the original data order, so some sort of copy of the data object is desirable.

Cloning in C#

The idea of cloning a class instance (making an exact copy) is not a designed-in feature of C#, but nothing actually stops you from carrying out such a copy yourself. The only place the Clone method appears in C# is in ADO DataSet manipulation. You can create a DataSet as a result of a database query and move through it a row at a time. If for some reason you need to keep references to two places in this DataSet, you would need two “current rows.” The simplest way to handle this in C# is to clone the DataSet.


DataSet cloneSet;
cloneSet = myDataSet.Clone();

Now this approach does not generate two copies of the data. It just generates two sets of row pointers to use to move through the records independently of each other. Any change you make in one clone of the DataSet is immediately reflected in the other because there is in fact only one data table. We discuss a similar problem in the following example.

Using the Prototype

Now let’s write a simple program that reads data from a database and then clones the resulting object. In our example program, we just read these data from a file, but the original data were derived from a large database, as we discussed previously. That file has the following form.


Kristen Frost, 9, CAT, 26.31, F
Kimberly Watcke, 10, CDEV,27.37, F
Jaclyn Carey, 10, ARAC, 27.53, F
Megan Crapster, 10, LEHY, 27.68, F

We’ll use the csFile class we developed earlier. First, we create a class called Swimmer that holds one name, club name, sex, and time, and read them in using the csFile class and the StringTokenizer class we developed in Chapter 4.


public class Swimmer      {
      private string name;              //name
      private string lname, frname;     //split names
      private int age;                  //age
      private string club;              //club initials
      private float time;               //time achieved
      private bool female;              //sex

//---------
public Swimmer(string line) {
      StringTokenizer tok = new StringTokenizer(line,",");
      splitName(tok);
      age = Convert.ToInt32 (tok.nextToken());
      club = tok.nextToken();
      time = Convert.ToSingle (tok.nextToken());
      string sx = tok.nextToken().ToUpper ();
      female = sx.Equals ("F");
}
//---------
private void splitName(StringTokenizer tok) {
      name = tok.nextToken();
      int i = name.IndexOf (" ");
      if(i >0 ) {
             frname = name.Substring (0, i);
             lname = name.Substring (i+1).Trim ();
      }
}
//---------
public bool isFemale() {
      return female;
}
//---------
public int getAge() {
      return age;
}
//---------
public float getTime() {
      return time;
}
//---------
public string getName() {
      return name;
}
//---------
public string getClub() {
      return club;
}

Then we create a class called SwimData that maintains an ArrayList of the Swimmers we read in from the database.


public class SwimData     {
      protected ArrayList swdata;
      private int index;
//-----
public SwimData() {
swdata = new ArrayList ();
}
//-----
public SwimData(ArrayList swd) {
      swdata = swd;
      index=0;
}
//-----
public SwimData(string filename)                {
      swdata = new ArrayList ();
      csFile fl = new csFile(filename);
      fl.OpenForRead ();
      string s = fl.readLine ();
      while(s != null) {
             Swimmer sw = new Swimmer(s);
             swdata.Add (sw);
             s = fl.readLine ();
      }
      fl.close ();
}
//-----
public void moveFirst() {
      index = 0;
}
//-----
public bool hasMoreElements() {
      return (index < swdata.Count-1 );
}
//-----
public void sort() {
}
//-----
public Swimmer getSwimmer() {
      if(index < swdata.Count )
             return (Swimmer)swdata[index++];
      else
             return null;
}
}

We can then use this class to read in the swimmer data and display it in a list box.


private void init() {
      swdata = new SwimData ("swimmers.txt");
      reload();
}
//-----
private void reload() {
      lsKids.Items.Clear ();
      swdata.moveFirst ();
      while (swdata.hasMoreElements() ) {
             Swimmer sw = swdata.getSwimmer ();
             lsKids.Items.Add (sw.getName() );
      }
}

This is illustrated in Figure 13-1.

Figure 13-1. A simple prototype program

image

When we click on the → button, we are cloning this class and sorting the data differently in the new class. Again, we clone the data because creating a new class instance would be much slower, and we want to keep the data in both forms.


private void btClone_Click(object sender, EventArgs e) {
      SwimData newSd = (SwimData)swdata.Clone ();
      newSd.sort ();
      while(newSd.hasMoreElements() ) {
             Swimmer sw = (Swimmer)newSd.getSwimmer ();
             lsNewKids.Items.Add (sw.getName() );
      }
}

We show the sorted results in Figure 13-2.

Figure 13-2. The sorted results of our Prototype program

image

Cloning the Class

While it may not be strictly required, we can make the SwimData class implement the ICloneable interface.


public class SwimData:ICloneable     {

All this means is that the class must have a Clone method that returns an object.


public object Clone() {
      SwimData newsd = new SwimData(swdata);
      return newsd;
}

Of course, using this interface implies that we must cast the object type back to the SwimData type when we receive the clone as we did previously.


SwimData newSd = (SwimData)swdata.Clone ();

Now, let’s click on the ← button to reload the left-hand list box from the original data. The somewhat disconcerting result is shown in Figure 13-3.

Figure 13-3. The Prototype showing the re-sort of the left list box

image

Why have the names in the left-hand list box also been re-sorted? Our sort routine looks like this.


public void sort() {
      //sort using IComparable interface of Swimmer
      swdata.Sort (0,swdata.Count ,null);
}

Note that we are sorting the actual ArrayList in place. This sort method assumes that each element of the ArrayList implements the IComparable interface.


public class Swimmer:IComparable     {

All this means is that it must have an integer CompareTo method that returns –1, 0, or 1, depending on whether the comparison between the two objects returns less than, equal or greater than. In this case, we compare the two last names using the string class’s CompareTo method and return that.


public int CompareTo(object swo) {
      Swimmer sw = (Swimmer)swo;
      return lname.CompareTo (sw.getLName() );
}

Now we can understand the unfortunate result in Figure 13-3. The original array is re-sorted in the new class, and there is really only one copy of this array. This occurs because the clone method is a shallow copy of the original class. In other words, the references to the data objects are copies, but they refer to the same underlying data. Thus, any operation we perform on the copied data will also occur on the original data in the Prototype class.

In some cases, this shallow copy may be acceptable, but if you want to make a deep copy of the data, you must write a deep cloning routine of your own as part of the class you want to clone. In this simple class, you just create a new ArrayList and copy the elements of the old class’s ArrayList into the new one.


public object Clone() {
      //create a new ArrayList
      ArrayList swd = new ArrayList ();
      //copy in swimmer objects
      for(int i = 0; i < swdata.Count ; i++)
             swd.Add (swdata[i]);
      //create new SwimData object with this array
      SwimData newsd = new SwimData (swd);
      return newsd;
}

Using the Prototype Pattern

You can use the Prototype pattern whenever any of a number of classes might be created or when the classes are modified after being created. As long as all the classes have the same interface, they can actually carry out rather different operations.

Let’s consider a more elaborate example of the listing of swimmers we just discussed. Instead of just sorting the swimmers, let’s create subclasses that operate on that data, modifying it and presenting the result for display in a list box. We start with the same basic class, SwimData.

Then it becomes possible to write different derived SwimData classes, depending on the application’s requirements. We always start with the SwimData class and then clone it for various other displays. For example, the SexSwimData class re-sorts the data by sex and displays only one sex. This is shown in Figure 13-4.

Figure 13-4. The OneSexSwimData class displays only one sex on the right.

image

In the SexSwimData class, we sort the data by name but return them for display based on whether girls or boys are supposed to be displayed. This class has this polymorphic sort method.


public void sort(bool isFemale) {
      ArrayList swd = new ArrayList();
      for (int i = 0; i < swdata.Count ; i++) {
             Swimmer sw =(Swimmer)swdata[i];
             if (isFemale == sw.isFemale() ) {
                    swd.Add (sw);
             }
      }
      swdata = swd;
}

Each time you click on the one of the sex option buttons, the class is given the current state of these buttons.


private void btClone_Click(object sender, System.EventArgs e) {
      SexSwimData newSd = (SexSwimData)swdata.Clone ();
      newSd.sort (opFemale.Checked );
      lsNewKids.Items.Clear() ;
      while(newSd.hasMoreElements() ) {
             Swimmer sw = (Swimmer)newSd.getSwimmer ();
             lsNewKids.Items.Add (sw.getName() );
      }
}

Note that the btClone_Click event clones the general SexSwimdata class instance swdata and casts the result to the type SexSwimData. This means that the Clone method of SexSwimData must override the general SwimData Clone method because it returns a different data type.


public object Clone() {
      //create a new ArrayList
      ArrayList swd = new ArrayList ();
      //copy in swimmer objects
      for(int i=0; i< swdata.Count ; i++)
             swd.Add (swdata[i]);
      //create new SwimData object with this array
      SexSwimData newsd = new SexSwimData (swd);
      return newsd;
}

Having to rewrite the Clone method each time we derive a new highly similar class is not a satisfactory approach. A better solution is to do away with implementing the ICloneable interface where each class has a Clone method and reverse the process where each receiving class clones the data inside the sending class. Here we show a revised portion of the SwimData class that contains the cloneMe method. It takes the data from another instance of SwimData and copies it into the ArrayList inside this instance of the class.


public class SwimData      {
       protected ArrayList swdata;
       private int index;
       //-----
       public void cloneMe(SwimData swdat) {
              swdata = new ArrayList ();
              ArrayList swd=swdat.getData ();
              //copy in swimmer objects
              for(int i=0; i < swd.Count ; i++)
                     swdata.Add (swd[i]);
       }

This approach will then work for all child classes of SwimData without having to cast the data between subclass types.

Dissimilar Classes with the Same Interface

Classes, however, do not have to be even that similar. The AgeSwimData class takes the cloned input data array and creates a simple histogram by age. If you click on “F,” you see the girls’ age distribution, and if you click on “M,” you see the boys’ age distribution, as shown in Figure 13-5.

Figure 13-5. The AgeSwimData class displays an age distribution.

image

This is an interesting case: The AgeSwimData class inherits the cloneMe method from the base SwimData class but overrides the sort method with one that creates a proto-swimmer with a name made up of the number of kids in that age group.


public class AgeSwimData:SwimData {
      ArrayList swd;
      public AgeSwimData() {
             swdata = new ArrayList ();
      }
      //------
      public AgeSwimData(string filename):base(filename){}
      public AgeSwimData(ArrayList ssd):base(ssd){}
      //------
      public override void cloneMe(SwimData swdat) {
             swd = swdat.getData ();
      }
      //------
      public override void sort() {
             Swimmer[] sws = new Swimmer[swd.Count ];
             //copy in swimmer objects
             for(int i=0; i < swd.Count ; i++) {
                    sws[i] = (Swimmer)swd[i];
             }
           //sort into increasing order
             for( int i=0; i< sws.Length ; i++) {
                    for (int j = i; j< sws.Length ; j++) {
                           if (sws[i].getAge ()>sws[j].getAge ())
                                  Swimmer sw = sws[i];
                                  sws[i]=sws[j];
                                  sws[j]=sw;
                           }
                    }
             }
             int age = sws[0].getAge ();
             int agecount = 0;
             int k = 0;
             swdata = new ArrayList ();
             bool quit = false;


             while( k < sws.Length && ! quit ) {
                    while(sws[k].getAge() ==age && ! quit) {
                           agecount++;
                           if(k < sws.Length -1)
                                  k++;
                           else
                                  quit= true;
                    }
      //create a new Swimmer with a series of X's for a name
      //for each new age
             string name = "";
             for(int j = 0; j < agecount; j++)
                    name +="X";
             Swimmer sw = new Swimmer(age.ToString() + " " +
                    name + "," + age.ToString() +
                    ",club,0,F");
                    swdata.Add (sw);
                    agecount = 0;
                    if(quit)
                           age = 0;
                    else
                           age = sws[k].getAge ();
             }
      }
}

Now, since our original classes display first and last names of selected swimmers, note that we achieve this same display, returning Swimmer objects with the first name set to the age string and the last name set to the histogram.

The UML diagram in Figure 13-6 illustrates this system fairly clearly. The main GUI class keeps two instances of SwimData but does not specify which ones. AgeSwimData is derived from SwimData, and both contain instances of the Swimmer class.

Figure 13-6. The UML diagram for the various SwimData classes

image

You should also note that you are not limited to the few subclasses we demonstrated here. It would be quite simple to create additional concrete classes and register them with whatever code selects the appropriate concrete class. In our example program, the user is the deciding point or Factory because he or she simply clicks on one of several buttons. In a more elaborate case, each concrete class could have an array of characteristics, and the decision point could be a class registry or prototype manager that examines these characteristics and selects the most suitable class. You could also combine the Factory Method pattern with the Prototype, where each of several concrete classes uses a different concrete class from those available.

Prototype Managers

A prototype manager class can be used to decide which of several concrete classes to return to the client. It can also manage several sets of prototypes at once. For example, in addition to returning one of several classes of swimmers, it could return different groups of swimmers who swam certain strokes and distances. It could also manage which of several types of list boxes are returned in which to display them, including tables, multicolumn lists, and graphical displays. It is best that whichever subclass is returned, it not require conversion to a new class type to be used in the program. In other words, the methods of the parent abstract or base class should be sufficient, and the client should never need to know which actual subclass it is dealing with.

Consequences of the Prototype Pattern

Using the Prototype pattern, you can add and remove classes at runtime by cloning them as needed. You can revise the internal data representation of a class at runtime, based on program conditions. You can also specify new objects at runtime without creating a proliferation of classes.

One difficulty in implementing the Prototype pattern in C# is that if the classes already exist, you may not be able to change them to add the required clone methods. In addition, classes that have circular references to other classes cannot really be cloned.

Like the registry of Singletons discussed before, you can also create a registry of Prototype classes that can be cloned and ask the registry object for a list of possible prototypes. You may be able to clone an existing class rather than writing one from scratch.

Note that every class that you might use as a prototype must itself be instantiated (perhaps at some expense) in order for you to use a Prototype Registry. This can be a performance drawback.

Finally, the idea of having prototype classes to copy implies that you have sufficient access to the data or methods in these classes to change them after cloning. This may require adding data access methods to these prototype classes so that you can modify the data once you have cloned the class.

Thought Question

An entertaining banner program shows a slogan starting at different places on the screen at different times and in different fonts and sizes. Design the program using a Prototype pattern.

Programs on the CD-ROM

image

Summary of Creational Patterns

The Factory pattern is used to choose and return an instance of a class from a number of similar classes, based on data you provide to the factory.

The Abstract Factory pattern is used to return one of several groups of classes. In some cases, it actually returns a Factory for that group of classes.

The Builder pattern assembles a number of objects to make a new object, based on the data with which it is presented. Frequently, the choice of which way the objects are assembled is achieved using a Factory.

The Prototype pattern copies or clones an existing class, rather than creating a new instance, when creating new instances is more expensive.

The Singleton pattern is a pattern that ensures there is one and only one instance of an object and that it is possible to obtain global access to that one instance.

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

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