http://www.developer.com/

Back to article

Working With Design Patterns: Prototype


April 2, 2008

Imagine a library clerk adding stacks of new books and movies into a library system. The clerk uses a scanner to swipe the bar code that appears on the book. (For simplicity's sake, assume that the bar code is the same as the book's classification number.)

For the first copy of a book added to the library, the system makes an external API call to determine basic book information, such as author (or director, for movies), title, and year published. The lookup is slow. Further, the clerk must visually verify the information coming back, and sometimes must correct inaccurate information.

Listing 1 shows an implementation for the catalog based on these requirements. The test demonstrates that client code is responsible for creating book or movie objects and populating them with appropriate data. Presume that this client is where the call to the external API takes place. Once the material object exists, client code adds it to the catalog by calling addNew.

Listing 1: A simple catalog.

// CatalogTest.java
import static org.junit.Assert.*;

import java.util.*;
import org.junit.*;

public class CatalogTest {
   private Catalog catalog;

   @Before
   public void initialize() {
      catalog = new Catalog();
   }

   @Test
   public void isEmptyOnCreation() {
      assertEquals(0, catalog.size());
   }

   @Test
   public void addNewBook() {
      final Book book = new Book("QA123", "author", "title",
                                 "1999", 1);
      catalog.addNew(book);
      assertEquals(1, catalog.size());
      List<Material> materials = catalog.get("QA123");
      assertEquals(1, materials.size());
      assertSame(book, materials.get(0));
   }
}

// Catalog.java
import java.util.*;

public class Catalog {
   private MultiMap<String, Material> materials =
      new MultiMap<String, Material>();

   public int size() {
      return materials.valuesSize();
   }

   public void addNew(Material material) {
      materials.put(material.getClassification(), material);
   }

   public List<Material> get(String classification) {
      return materials.get(classification);
   }
}

Many of the added materials are duplicate copies of the same title, where every piece of information remains the same (and you'll assume that the information need not be verified for these copies). The catalog can make a duplicate of an already existing material—the prototype—thus sparing the API call.

The test in Listing 2 presents the scenario where a first book is externally created and then added to the catalog. A second book is scanned. Because the scanned classification represents one that is already in the system, the client can call addCopy with only the classification. Internally, the catalog is responsible for finding the first book and using its information to create a copy.

Listing 2.

@Test
public void addCopyViaPrototype() {
   final Book book = new Book("QA123", "title", "author",
                              "1999", 1);
   catalog.addNew(book);

   catalog.addCopy("QA123");

   assertEquals(2, catalog.size());

   List<Material> materials = catalog.get("QA123");
   assertEquals(2, materials.size());
   assertSame(book, materials.get(0));

   Material copy = materials.get(1);
   assertTrue(copy instanceof Book);
   assertEquals("QA123", copy.getClassification());
   assertEquals("author", copy.getAuthor());
   assertEquals("title", copy.getTitle());
   assertEquals("1999", copy.getYear());
   assertEquals(2, copy.getCopyNumber());
}

A simple implementation, good enough to get the tests to pass, appears in Listing 3. It's not a very good implementation. Never mind that the algorithm for obtaining the copy number is probably inadequate. The real problem is that the copy method is going to get ugly. Once the test supports movies in addition to books, the copy method will require an if statement to check the material type. The code will also start to get messy with the construction details of the various object types. Changes to how books or movies get constructed will ripple into the Catalog class.

Listing 3.

public void addCopy(String classification) {
   List<Material> copies = materials.get(classification);
   materials.put(classification, copy(copies.get(0)));
}

private Material copy(Material material) {
   Book book = new Book(material.getClassification(),
         material.getTitle(),
         material.getAuthor(),
         material.getYear(),
         material.getCopyNumber() + 1);
   return book;
}

The prototype pattern suggests that the catalog tell the prototype object (in this case, the Book object) to return a clone of itself. In other words, the Book class should do all the work. Adding the Movie type, in turn, suggests that the solution should be polymorphic.

A more robust and refactored solution that incorporates the notion of a prototype appears in Listings 4 (the tests) and 5 (the production code). Listing 6 shows the Material, Book, and Movie classes.

Listing 4: Tests to drive out use of prototype.

import static org.junit.Assert.*;

import java.util.*;
import org.junit.*;

public class CatalogTest {
   private Catalog catalog;

   @Before
   public void initialize() {
      catalog = new Catalog();
   }

   @Test
   public void isEmptyOnCreation() {
      assertEquals(0, catalog.size());
   }

   @Test
   public void addNewBook() {
      final Book book = new Book("QA123", "author", "title",
                                 "1999", 1);
      catalog.addNew(book);
      assertEquals(1, catalog.size());
      List<Material> materials = catalog.get("QA123");
      assertEquals(1, materials.size());
      assertSame(book, materials.get(0));
   }

   @Test(expected=NoExistingCopyException.class)
   public void addNewCopyThrowsIfNoExistingCopy() {
      catalog.addCopy("QA123.4");
   }

   @Test
   public void addCopyViaPrototype() {
      final Book book = new Book("QA123", "title", "author",
                                 "1999", 1);
      catalog.addNew(book);
      catalog.addCopy("QA123");

      assertEquals(2, catalog.size());
      assertMaterialCopies("QA123", 2, book);
   }

   @Test
   public void addMovieCopyViaPrototype() {
      final Movie movie = new Movie("DD890", "Shining", "Kubrick",
                                    "2006", 1, Movie.Format.DVD);
      catalog.addNew(movie);
      catalog.addCopy("DD890");

      assertEquals(2, catalog.size());
      assertMovieCopies("DD890", 2, movie);
   }

   private void assertMaterialCopies(String classification,
      int number, Material material) {
      List<Material> materials = catalog.get(classification);
      assertEquals(number, materials.size());

      for (int i = 0; i < number; i++) {
         Material copy = materials.get(i);
         assertEquals(material.getClass(), copy.getClass());
         assertEquals(material.getClassification(),
                      copy.getClassification());
         assertEquals(material.getAuthor(), copy.getAuthor());
         assertEquals(material.getTitle(), copy.getTitle());
         assertEquals(material.getYear(), copy.getYear());
         assertEquals(i + 1, copy.getCopyNumber());
      }
   }

   private void assertMovieCopies(String classification,
                                  int number, Movie material) {
      assertMaterialCopies(classification, number, material);
      for (Material copy: catalog.get(classification))
         assertEquals(material.getFormat(),
                      ((Movie)copy).getFormat());
   }
}

Listing 5: A Catalog that uses the prototype pattern.

import java.util.*;

public class Catalog {
   private MultiMap<String, Material> materials =
      new MultiMap<String, Material>();

   public void addCopy(String classification) {
      List<Material> copies = materials.get(classification);
      if (copies.isEmpty())
         throw new NoExistingCopyException();

      materials.put(classification, copies.get(0).copy());
   }

   public int size() {
      return materials.valuesSize();
   }

   public void addNew(Material material) {
      materials.put(material.getClassification(), material);
   }

   public List<Material> get(String classification) {
      return materials.get(classification);
   }
}

Listing 6: The Material hierarchy.

// Material.java
abstract public class Material {
   private final String title;
   private final String classification;
   private final String author;
   private final String year;
   private String branch;
   private int copyNumber;

   public Material(String classification, String title,
                   String author, String year, int copyNumber) {
      this.classification = classification;
      this.title = title;
      this.author = author;
      this.year = year;
      this.copyNumber = copyNumber;
      branch = "checked out";
   }

   abstract public Material copy();

   public String getAuthor() {
      return author;
   }

   public String getClassification() {
      return classification;
   }

   public String getTitle() {
      return title;
   }

   public String getYear() {
      return year;
   }

   public int getCopyNumber() {
      return copyNumber;
   }

   public String getBranch() {
      return branch;
   }
}

// Book.java
public class Book extends Material {
   public Book(String classification, String title,
               String author, String year,
         int copyNumber) {
      super(classification, title, author, year, copyNumber);
   }

   public Book copy() {
      return new Book(getClassification(), getTitle(),
                      getAuthor(), getYear(),
                      getCopyNumber() + 1);
   }
}

// Movie.java
public class Movie extends Material {
   public static enum Format {
      DVD, BluRay, VHS
   }

   private Format format;

   public Movie(String classification, String title, String author,
         String year, int copyNumber, Format format) {
      super(classification, title, author, year, copyNumber);
      this.format = format;
   }

   public Format getFormat() {
      return format;
   }

   public Movie copy() {
      return new Movie(getClassification(), getTitle(),
                       getAuthor(), getYear(),
                       getCopyNumber() + 1, getFormat());
   }
}
Note: I did not choose to use the clone method in Java for this implementation, a design choice that I made based on the current construction design. If you do implement clone, make sure you read up on all the ramifications of doing so, including discussions of shallow versus deep copies. Joshua Bloch's book Effective Java is a good source.

The focus of prototype is the bolded line in the Catalog addCopy method (refer to Listing 5) that tells an existing material to return a copy of itself. This concise single line allows the Catalog to be closed to any existing Material or Material subclass changes and also to additions of new Material subclasses. The prototype pattern also adheres to other basic notions of good OO design: It is polymorphic, and fulfills the dependency inversion and single responsibility principles.



Click here for a larger image.

Figure 1: Prototype

About the Author

Jeff Langr is a veteran software developer with over a quarter century of professional software development experience. He's written two books, including Agile Java: Crafting Code With Test-Driven Development (Prentice Hall) in 2005. Jeff is contributing a chapter to Uncle Bob Martin's upcoming book, Clean Code. Jeff has written over 75 articles on software development, with over thirty appearing at Developer.com. You can find out more about Jeff at his site, http://langrsoft.com, or you can contact him via email at jeff at langrsoft dot com.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date