Working With Design Patterns: State
Conditional logic is essential to building any application, yet too much can make an application incomprehensible. Many of the applications I build require that an object exist in many different states, with behavior differing from state to state. A straightforward implementation involves lots of if statements and complex conditionals, producing overly convoluted solutions in short order. As a remedy, I use the state design pattern to keep my code from getting out of hand.
Holdings in a library provide a good example. A holding is a copy of a book (see Listing 1). (In my implementation, the book is simply the ISBN classification information. Thus, each holding object references a copy number and a book object.) Holdings can be checked out, checked in, they can be moved from branch to branch, they can be held by a patron, they can be warehoused, and so on. Each of these events puts the holding into a state where different rules apply. For example, a book that's checked out obviously can't be warehoused.
Listing 1: The Book class.
// BookTest.java
import static org.junit.Assert.*;
import org.junit.*;
public class BookTest {
public static final Book CATCH22 = new Book("0-671-12805-1",
"Catch-22", "Heller, Joseph", "1961");
@Test
public void create() {
assertEquals("0-671-12805-1", CATCH22.getIsbn());
assertEquals("Catch-22", CATCH22.getTitle());
assertEquals("Heller, Joseph", CATCH22.getAuthor());
assertEquals("1961", CATCH22.getYear());
}
}
// Book.java
public class Book {
private final String isbn;
private final String title;
private final String author;
private final String year;
public Book(String isbn, String title, String author,
String year) {
this.isbn = isbn;
this.title = title;
this.author = author;
this.year = year;
}
public String getIsbn() {
return isbn;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
public String getYear() {
return year;
}
}
Listing 2 shows a starter implementation for Holding. (Note that I'm not yet concerned with the relevancy of the patron ID.)
Listing 2: An initial Holding implementation.
import static org.junit.Assert.*;
import java.util.Date;
import org.junit.*;
public class HoldingTest {
private Holding holding;
private static final Date NOW = new Date();
private static final String PATRON_ID = "12345";
@Before
public void initialize() {
Book book = BookTest.CATCH22;
int copyNumber = 1;
holding = new Holding(book, copyNumber);
}
@Test
public void create() {
assertSame(BookTest.CATCH22, holding.getBook());
assertEquals(1, holding.getCopyNumber());
assertFalse(holding.isOnLoan());
}
@Test
public void checkout() {
holding.checkout(NOW, PATRON_ID);
assertTrue(holding.isOnLoan());
assertEquals(NOW, holding.getLoanDate());
}
@Test
public void checkin() {
Date later = new Date(NOW.getTime() + 1);
holding.checkout(NOW, PATRON_ID);
holding.checkin(later);
assertFalse(holding.isOnLoan());
}
}
// Holding.java
import java.util.Date;
public class Holding {
private final Book book;
private final int copyNumber;
private Date checkoutDate;
public Holding(Book book, int copyNumber) {
this.book = book;
this.copyNumber = copyNumber;
}
public Book getBook() {
return book;
}
public int getCopyNumber() {
return copyNumber;
}
public boolean isOnLoan() {
return checkoutDate != null;
}
public Date getLoanDate() {
return checkoutDate;
}
public void checkout(Date date, String patronId) {
checkoutDate = date;
}
public void checkin(Date date) {
checkoutDate = null;
}
}
