September 18, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

Working With Design Patterns: State

  • June 19, 2008
  • By Jeff Langr
  • Send Email »
  • More Articles »

The next story I want to tackle is to allow patrons to place a hold on a book. If checked in, the librarian places a held book behind the desk. Attempts to check it out by a different patron are rejected. If checked out, the system marks the book on hold for when it's returned. Holds are only valid for three days; a daily update routine hits all holdings, giving each an opportunity to clear any expired holds. The three-day timer starts only once a book is returned, if it's checked out. A hold cannot be placed on a book already on hold.

Based on these requirements, the code to manage patron holds (see Listing 3) really isn't that bad, but I'm starting to wonder where it's headed. I must ensure I have if statements in the right place, for example, when doing an update: If I add code to check to see whether any hold needs to be cleared, but neglect to guard against the case where the holding isn't already on hold, my code throws a NullPointerException. As I consider similar features, such as transfers and placing books on reserve, I'm thinking that the code easily could start getting unwieldy and confusing.

Tracking State

A holding can be in a number of states: checked out, checked in, on hold or not, in transit between branches, and so on. For the existing code, a holding can be in one of four possible states:

  • checked out, on hold
  • checked out, not on hold
  • checked in, on hold
  • checked out, not on hold

Defining the states allows me to draw a state diagram (see Figure 1). The state diagram captures each separate state as a separate box. A holding can transition between states when specific events occur, such as the holding being checked in. Events usually trigger actions as part of transitioning to another state.



Click here for a larger image.

Figure 1: State transitions.

The state pattern says that I can take each of these states and represent them as a separate class. The current state is tracked within Holding as a reference to a state object. I define the possible events as methods on all states. Each state handles the set of events however appropriately by invoking an action on the Holding object. The event handler also is responsible for updating the state reference on the Holding object if a state transition is required.

Listing 3: Holding, revised.

// HoldingTest.java
import static org.junit.Assert.*;
import java.util.*;
import org.junit.*;

public class HoldingTest {
   private Holding holding;
   private static final Date NOW = new Date();
   private static final Date LATER = new Date(NOW.getTime() + 1);
   private static final String PATRON_ID1 = "12345";
   private static final String PATRON_ID2 = "22345";

   @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_ID1);
      assertTrue(holding.isOnLoan());
      assertEquals(NOW, holding.getLoanDate());
   }

   @Test
   public void checkin() {
      holding.checkout(NOW, PATRON_ID1);
      holding.checkin(LATER);
      assertFalse(holding.isOnLoan());
   }

   @Test
   public void placeHoldOnCheckedInHolding() {
      assertFalse(holding.isOnHold());
      holding.placeHold(NOW, PATRON_ID1);
      assertTrue(holding.isOnHold());
   }

   @Test
   public void rejectsDuplicateHolds() {
      holding.placeHold(NOW, PATRON_ID1);
      try {
         holding.placeHold(NOW, PATRON_ID2);
         fail();
      } catch (HoldException expected) {
         assertTrue(holding.isOnHold());
      }
   }

   @Test(expected = HoldException.class)
   public void holdOnCheckedOutBookRetainedOnCheckin() {
      holding.checkout(NOW, PATRON_ID2);
      holding.placeHold(NOW, PATRON_ID1);
      holding.checkin(LATER);
      assertTrue(holding.isOnHold());
      holding.checkout(NOW, PATRON_ID2);
   }

   @Test
   public void releaseHold() {
      holding.placeHold(NOW, PATRON_ID1);
      holding.releaseAnyHold();
      assertFalse(holding.isOnHold());
   }

   @Test
   public void holdReleasedAfter3Days() {
      holding.placeHold(NOW, PATRON_ID1);
      holding.update(DateUtil.addDays(NOW, 1));
      assertTrue(holding.isOnHold());
      holding.update(DateUtil.addDays(NOW, 2));
      assertTrue(holding.isOnHold());
      holding.update(DateUtil.addDays(NOW, 3));
      assertFalse(holding.isOnHold());
   }

   @Test
   public void holdReleasedAfter3DaysAfterCheckin() {
      holding.checkout(NOW, PATRON_ID1);
      holding.placeHold(LATER, PATRON_ID1);
      Date checkinDate = DateUtil.addDays(NOW, 3);
      holding.update(checkinDate);
      assertTrue(holding.isOnHold());
      holding.checkin(checkinDate);
      assertTrue(holding.isOnHold());
      holding.update(DateUtil.addDays(checkinDate, 3));
      assertFalse(holding.isOnHold());
   }

   @Test
   public void releaseHoldHarmlessIfNoHolds() {
      holding.releaseAnyHold();
      assertFalse(holding.isOnHold());
   }

   @Test
   public void checkoutReleasesHold() {
      holding.placeHold(NOW, PATRON_ID1);
      holding.checkout(LATER, PATRON_ID1);
      assertFalse(holding.isOnHold());
   }

   @Test
   public void rejectCheckoutForHoldByDifferentPatron() {
      holding.placeHold(NOW, PATRON_ID1);
      try {
         holding.checkout(NOW, PATRON_ID2);
         fail();
      } catch (HoldException expected) {
      }
   }
}

// Holding.java
import java.util.*;

public class Holding {
   private final Book book;
   private final int copyNumber;
   private Date checkoutDate;
   private String holdPatron;
   private Date holdDate;

   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) {
      if (isOnHold() && patronId != holdPatron)
         throw new HoldException();
      releaseAnyHold();
      checkoutDate = date;
   }

   public void checkin(Date date) {
      checkoutDate = null;
   }

   public void placeHold(Date date, String patronId) {
      if (isOnHold())
         throw new HoldException();
      this.holdPatron = patronId;
      holdDate = new Date();
   }

   public boolean isOnHold() {
      return holdPatron != null;
   }

   public void releaseAnyHold() {
      holdPatron = null;
   }

   public void update(Date date) {
      if (isOnHold() && !isOnLoan() &&
         DateUtil.daysBetween(holdDate, date) >= 3)
         releaseAnyHold();
   }
}




Page 2 of 4



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel