次は、利用者が本の取り置きを依頼できる状況について考えてみます。本が返却された場合、図書館員は取り置き本をカウンターの奥に置きます。別の利用者がその本を借りようとしても拒否されます。対象の本が貸し出されていた場合、システムはその返却時に本を取り置き中としてマークします。取り置きが有効なのは3日間だけです。更新ルーチンが毎日実行されてすべての取り置き情報を調べ、有効期限が切れたものをクリアします。3日間のタイマーは、本が貸し出されている場合、返却時に1回だけ開始されます。既に取り置き中の本に、取り置き依頼を入れることはできません。
これらの要件に基づき、利用者の取り置き状況を管理するために作成したコードがリスト3です。それほど悪くはないのですが、どこで何をしているのかだんだんわからなくなってきました。例えば、取り置き情報を更新するときにはif文を正しい場所に置かなければなりません。もしも、取り置きをクリアする必要があるかどうかを調べるコードを追加するときに、そのHoldingがもう取り置き中でないというケースに対する配慮を忘れてしまうと、NullPointerExceptionがスローされます。本の転送や予約の設定など、同様の機能について考えていくと、コードはあっという間に手に負えなくなり、混乱をきたすことでしょう。
// 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();
}
}
