Architecture & DesignWorking With Design Patterns: State

Working With Design Patterns: State

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

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;
   }
}

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.

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();
   }
}

I refactor my code slowly, running tests continually. The first class I create is the HoldingState class (see Listing 4), an abstract class intended to provide null behavior for each of the possible events.

Listing 4: HoldingState.

// HoldingState
import java.util.*;

abstract class HoldingState {
   protected final Holding holding;
   HoldingState(Holding holding) {
      this.holding = holding;
   }
   void checkout(Date date, String patronId) {
   }
   void checkin(Date date) {
   }
   void placeHold(Date date, String patronId) {
   }
   void update(Date date) {
   }
}

I then create state derivatives, one for each of the three states. After I create the derivatives, I begin to move code over from the Holding class. For each event (checkout, checkin, placeHold, update), the job of the Holding object is simply to delegate the event to the current HoldingState object. For example, the checkout method in Holding ends up reading:

public void checkout(Date date, String patronId) {
   state.checkout(date, patronId);
}

Because these are simple delegations, I could consider using the Java proxy mechanism. Right now, I’m not concerned about a rapidly growing public interface on Holding, so I’ll defer that enhancement.

States that need to do something with the checkout event call an action method and/or transition to another state. Other states may choose to ignore that event. One interesting result of implementing the state pattern, though, is that it may help point out a potential problem in the system by making ignored events apparent. For example, my CheckedOut state object provides no behavior for the checkout event. That unhandled event triggers me to think that I must determine what should happen if someone does try to check out a book twice.

The benefit from moving all the code into the state objects is that the conditionals begin to disappear. When in the CheckedInHeld state, I no longer have to check whether or not a book is held before I proceed:

class CheckedInHeld extends HoldingState {
   // ...
  @Override
   public void checkout(Date date, String patronId) {
      if (patronId != holding.holdPatron)
         throw new HoldException();
      holding.doCheckout(date, patronId);
      holding.state = new CheckedOut(holding);
   }
   // ...
}

Previously, the code in checkout in Holding had to ask whether or not the book was already held. Because this code is executing when the Holding references the CheckedInHeld state, that question no longer needs to be asked.

The state classes are shown in Listing 5. Each of the state classes is small, simple, and easily could be tested directly.

Listing 5: State derivatives.

// CheckedOut.java
import java.util.*;

class CheckedOut extends HoldingState {
   CheckedOut(Holding holding) {
      super(holding);
   }

   @Override
   public void checkin(Date date) {
      holding.doCheckin(date);
      holding.state = new CheckedIn(holding);
   }

   @Override
   public void placeHold(Date date, String patronId) {
      holding.doHold(date, patronId);
      holding.state = new CheckedOutHeld(holding);
   }
}

// CheckedIn.java
import java.util.*;

class CheckedIn extends HoldingState {
   CheckedIn(Holding holding) {
      super(holding);
   }

   @Override
   public void checkout(Date date, String patronId) {
      holding.doCheckout(date, patronId);
      holding.state = new CheckedOut(holding);
   }

   @Override
   public void placeHold(Date date, String patronId) {
      holding.doHold(date, patronId);
      holding.state = new CheckedInHeld(holding);
   }
}

// CheckedInHeld.java
import java.util.*;

public class CheckedInHeld extends CheckedIn {
   CheckedInHeld(Holding holding) {
      super(holding);
   }

   @Override
   public void checkout(Date date, String patronId) {
      if (patronId != holding.holdPatron)
         throw new HoldException();
      holding.doCheckout(date, patronId);
      holding.state = new CheckedOut(holding);
   }

   @Override
   public void placeHold(Date date, String patronId) {
      throw new HoldException();
   }

   @Override
   public void update(Date date) {
      holding.doReleaseOldHold(date);
   }
}

// CheckedOutHeld.java
import java.util.*;


public class CheckedOutHeld extends HoldingState {
   CheckedOutHeld(Holding holding) {
      super(holding);
   }

   @Override
   public void checkin(Date date) {
      holding.doCheckin(date);
      holding.state = new CheckedInHeld(holding);
   }

   @Override
   public void placeHold(Date date, String patronId) {
      throw new HoldException();
   }
}

The Holding class that remains (see Listing 6) is now devoid of most conditional logic! I can now test it, too, with excruciating ease.

The state pattern is one of the few design patterns that requires tight coupling between its classes: The Holding class is dependent upon the initial state, and each of the states is in turn dependent upon Holding. Although there are some clever ways to break this, it’s probably not necessary, because the classes represent a self-enclosed subsystem that can remain isolated from the rest of the system. For the implementation of the state classes, I took advantage of Java’s package-level access, directly having the states access Holding variables as needed, but not exposing these fields to external clients of Holding. It’s one of the rare cases where I won’t insist that a field remain private.

Listing 6: Holding, using the state design pattern.

import java.util.*;

public class Holding {
   private final Book book;
   private final int copyNumber;

   Date checkoutDate;
   String holdPatron;
   Date holdDate;
   HoldingState state = new CheckedIn(this);
   Date checkinDate;

   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) {
      state.checkout(date, patronId);
   }

   public void checkin(Date date) {
      state.checkin(date);
   }

   public void placeHold(Date date, String patronId) {
      state.placeHold(date, patronId);
   }

   public void update(Date date) {
      state.update(date);
   }

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

   public void releaseAnyHold() {
      holdPatron = null;
   }

   // callback actions
   void doHold(Date date, String patronId) {
      holdPatron = patronId;
      holdDate = date;
   }

   void doCheckout(Date date, String patronId) {
      checkoutDate = date;
      releaseAnyHold();
   }

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

   public void doReleaseOldHold(Date updatedAt) {
      if (DateUtil.daysBetween(holdDate, updatedAt) >= 3)
         releaseAnyHold();
   }
}

Adding new functionality into the state diagram usually is simple and straightforward. The separate state classes help keep conditional logic to a minimum by avoiding confusion about the conditions under which something can occur. Often, modifications will necessitate changes to transitions between states, requiring updates to multiple state derivatives. Drawing a state diagram can be a good idea, but there’s another possible solution to help manage the transitions.

State diagrams can be represented as simple tables. If I were to refactor to a more disciplined implementation of the state pattern, I could dramatically simplify my work. The table would define event methods and callback action method names; these would be used to automatically generate code for the state derivatives. This automation can be critical in a highly dynamic state system. The Object Mentor web site contains a download for just such a Java code generator, SMC (State Machine Compiler). There also are state pattern variants that can simplify state system changes in a more dynamic subsystem.

Figure 2: The State pattern.

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 has contributed a couple chapters 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.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories