ADD A CATEGORY
Add support for a movie to have a single category, from a fixed list of alternatives.
To add support for categories through the application we will want these tests:
Test 35. A movie that hasn't explicitly been given a category should answer that it is uncategorized when asked for its category.
Test 36. If a movie is given a category when it is created, it answers that when asked for its category.
Test 37. Trying to create a movie with an invalid category (i.e., not from the predefined set) throws an exception.
We begin by adding support for a category to Movie. Notice that the fleshed out task description mentions that the categories are from a fixed list. That means that the category of a movie can't be just anything (like an arbitrary string), but has to be picked from a fixed set of alternatives. So let's pick an initial list to work with: Science Fiction, Drama, Comedy, Horror, Western, and Fantasy. We will also need an Uncategorized setting.
Test 35: A movie that hasn't explicitly been given a category should answer that it is uncategorized when asked for its category
We'll start with the last case, since it doesn't require actually setting the category of a Movie:
public void testUncategorized() { assertEquals("starWars should be uncategorized.", "Uncategorized", starWars.getCategory()); }
Getting to green bar by doing the simplest thing results in:
public String getCategory() { return "Uncategorized"; }
Test 36: If a movie is given a category when it is created, it answers that when asked for its category
Now, when we are testing for the other categories, we need a way to set the category of a Movie. The simplest thing is to add a constructor that takes a category string. So here is the test:
public void testScienceFiction() { Movie alien = new Movie("Alien", "Science Fiction"); assertEquals("alien should be Science Fiction.", "ScienceFiction", alien.getCategory()); }
We know we'll have to store the category value and return it from getCategory(), so let's take a bigger step and just do it. And here's the new code:
private String category = null;
public Movie(String aName, String aCategory) { this(aName, -1); category = aCategory; }
public Movie(Movie original) { name = original.name; rating = original.rating; category = original.category; }
public String getCategory() { return (category != null) ? category : "Uncategorized"; }
Green bar. The constructors of Movie are proliferating, so let's make a more general one and retrofit our tests to use it:
public Movie(String aName, String aCategory, int aRating) { checkNull(aName); checkEmpty(aName); name = aName; category = (aCategory != null) ? aCategory : "Uncategorized"; rating = aRating; }
While we're at it we notice that some refactoring can be applied to TestMovie. We do that as well, removing unneeded instance creation from the tests and using starWars that is created in setUp().
Exercises
1. |
Refactor TestMovie to use a single Constructor Method that we just added to Movie.
|
2. |
Refactor the tests again, this time removing the unneeded Movie creation.
|
Test 37: Trying to create a movie with an invalid category (i.e., not from the predefined set) throws an exception
Now, we need to enforce the constraint that only a fixed set of alternatives can be used:
public void testBadCategory() { try { Movie alien = new Movie("Alien", "SciFi", -1); fail("Bad category accepted"); } catch (IllegalArgumentException ex) { } }
So how do we constrain the category values to a closed, fixed set of alternatives? We could use guard statements and the string comparison:
public Movie(String aName, String aCategory, int aRating) { checkNull(aName); checkEmpty(aName); checkCategory(aCategory); name = aName; category = (aCategory != null) ? aCategory : "Uncategorized"; rating = aRating; }
private void checkCategory(String aCategory) { if (aCategory == null) return; if (aCategory.equals("Uncategorized")) return; if (aCategory.equals("Science Fiction")) return; if (aCategory.equals("Horror")) return; throw new IllegalArgumentException("Bad category: " +aCategory); }
This smells! Imagine changing the spelling of a category once we have a large, complex system built. We could refactor the string literals to constants and use them everywhere:
public static final String UNCATEGORIZED = "Uncategorized"; public static final String SCIFI = "Science Fiction"; public static final String HORROR = "Horror";
public Movie(String aName, String aCategory, int aRating) { checkNull(aName); checkEmpty(aName); checkCategory(aCategory); name = aName; category = (aCategory != null) ? aCategory : UNCATEGORIZED; rating = aRating; }
private void checkCategory(String aCategory) { if (aCategory == null) return; if (aCategory.equals(UNCATEGORIZED)) return; if (aCategory.equals(SCIFI)) return; if (aCategory.equals(HORROR)) return; throw new IllegalArgumentException("Bad category: " +aCategory); }
The corresponding version of the tests look like:
public void testUncategorized() { assertEquals("starWars should be uncategorized.", Movie.UNCATEGORIZED, starWars.getCategory()); }
public void testScienceFiction() { Movie alien = new Movie("Alien",Movie.SCIFI, -1); assertEquals("alien should be Science Fiction.", Movie.SCIFI, alien.getCategory()); }
This still has a smell. It would be nice to refactor this in a way that would let us leverage Java to enforce the constraints for us. We can do that by refactoring to a type-safe enumeration. Joshua Kerievsky discusses this refactoring in [29].
First, we create the type-safe enumeration class:
public class Category { private String name=null;
private Category(String categoryName) { name = categoryName; }
public static final Category UNCATEGORIZED = new Category("Uncategorized"); public static final Category SCIFI = new Category("Science Fiction"); public static final Category HORROR = new Category("Horror"); }
Next, we update the related tests. One advantage of using a type-safe enumeration is that we no longer need to test for bad values; the compiler enforces that for us. Here are the remaining tests:
public void testUncategorized() { assertEquals("starWars should be uncategorized.", Category.UNCATEGORIZED, starWars.getCategory()); }
public void testScienceFiction() { Movie alien = new Movie("Alien",Category.SCIFI, -1); assertEquals("alien should be Science Fiction.", Category.SCIFI, alien.getCategory()); }
And, finally, here are the affected bits of Movie (note that the string constants have been deleted):
private Category category = Category.UNCATEGORIZED;
public Movie(String aName, Category aCategory, int aRating) { checkNull(aName); checkEmpty(aName); name = aName; category = (aCategory != null) ? aCategory : Category.UNCATEGORIZED; rating = aRating; }
public Category getCategory() { return category; }
Now we can go back to the customer and get a list of the categories that should be supported and add them to Category.
Exercises
3. |
The customer identified these categories: Science Fiction, Horror, Comedy, Western, Drama, Fantasy, Kids, Adult, Mystery, Thriller. Add these to Category.
|
|
No comments:
Post a Comment