第 46 章 / 共 57 章 · 约 9 分钟
LLD 案例:图书馆管理系统
图书馆管理系统考的是建模:哪些是“书”,哪些是“书架上那一本”,借阅和预约分别挂在谁身上。模型一旦理顺,借书、还书、预约排队、罚款这些流程写起来都很直接。
需求澄清
功能需求
- 馆藏目录:一种书(ISBN、书名、作者)可以有多本实体副本,每本有自己的条码。
- 读者借书:同时最多借 5 本,借期 14 天;有未缴罚款超过 10 元的不能借。
- 还书:超期按天罚款,每天 0.5 元,单本封顶 20 元。
- 预约:所有副本都借出时可以预约;同一本书的预约按先来后到排队。有副本归还时,留给队首读者 3 天,过期就轮到下一位。
- 按书名、作者、ISBN 检索。
非功能需求
- 同一本副本不能同时借给两个人。
- 预约公平:严格按先来后到。
核心类
| 类 | 职责 |
|---|---|
Book | 目录信息:ISBN、书名、作者 |
BookCopy | 实体副本:条码、状态(在架、借出、预约架、遗失),在预约架上时留给谁、留到哪天 |
Member | 读者:当前借阅、未缴罚款 |
Loan | 一次借阅:哪本副本、谁借的、应还日期、实际归还日期 |
Hold | 一次预约:哪种书、谁预约的、什么时候预约的 |
LibraryService | 借书、还书、预约的业务流程 |
FinePolicy | 罚款规则(策略) |
图表可横向滚动,展开后可缩放查看。
查看图表源码
classDiagram
accTitle: 图书馆类图:书目、副本、借阅与预约
class Book {
-String isbn
-String title
-List authors
}
class BookCopy {
-String barcode
-CopyStatus status
-Member heldFor
-LocalDate holdUntil
}
class Member {
-String id
-long unpaidFines
+activeLoans() int
}
class Loan {
-LocalDate dueDate
-LocalDate returnedOn
}
class Hold {
-LocalDateTime placedAt
}
class LibraryService {
+checkout(Member m, BookCopy c) Loan
+returnCopy(BookCopy c)
+placeHold(Member m, Book b)
}
class FinePolicy {
<<interface>>
+fine(LocalDate due, LocalDate returned) long
}
Book "1" *-- "many" BookCopy
Loan --> BookCopy
Loan --> Member
Hold --> Book
Hold --> Member
LibraryService ..> FinePolicy
LibraryService ..> Notifier : hold ready
建模的关键:Book 和 BookCopy 分开
这一步做对了,后面的问题都好回答:
- 书名、作者属于
Book,所有副本共享;状态、条码、应还日期属于某一本实体副本。 - 借阅挂在副本上:读者借走的是“条码 0042 的那一本”。
- 预约挂在书上:读者要的是“任意一本《算法导论》”,哪本先还回来就给他哪本。如果预约挂在副本上,一本迟迟不还,读者就会一直等,而另一本早就回来了。
Loan 是独立的类,而不是 BookCopy 上的几个字段:一本书会被借很多次,借阅历史、逾期统计都需要它。
用到的设计模式
- 策略模式:
FinePolicy把罚款规则抽出来,学生和教师、普通书和期刊可以用不同的规则。 - 观察者模式:预约的书到了,通过
Notifier发邮件或短信;业务流程不关心具体怎么通知。 - 服务层:
LibraryService负责编排流程,实体类只管自己的数据和不变量。
核心代码
class LibraryService {
private final Map<String, Deque<Hold>> holds = new HashMap<>(); // isbn -> FIFO queue
private final Map<String, Loan> loans = new HashMap<>(); // barcode -> open loan
private final FinePolicy fines;
private final Notifier notifier;
private final Clock clock;
LibraryService(FinePolicy fines, Notifier notifier, Clock clock) {
this.fines = fines; this.notifier = notifier; this.clock = clock;
}
synchronized Loan checkout(Member m, BookCopy copy) {
if (m.activeLoans() >= 5 || m.unpaidFines() > 1_000)
throw new IllegalStateException("Member may not borrow now");
boolean heldForThem = copy.status() == CopyStatus.ON_HOLD_SHELF && m.equals(copy.heldFor());
if (copy.status() != CopyStatus.AVAILABLE && !heldForThem)
throw new IllegalStateException("Copy not available");
copy.lend(); // status = ON_LOAN, hold cleared
Loan loan = new Loan(copy, m, today().plusDays(14));
loans.put(copy.barcode(), loan);
m.addLoan(loan);
return loan;
}
synchronized void returnCopy(BookCopy copy) {
Loan loan = loans.remove(copy.barcode());
if (loan == null) throw new IllegalArgumentException("Copy is not on loan");
loan.close(today());
long fine = fines.fine(loan.dueDate(), today());
if (fine > 0) loan.member().charge(fine);
shelve(copy);
}
/** A free copy goes to the first hold in line, else back to the shelf. Also run for expired holds. */
synchronized void shelve(BookCopy copy) {
Hold next = holds.getOrDefault(copy.book().isbn(), new ArrayDeque<>()).poll();
if (next == null) {
copy.makeAvailable();
return;
}
copy.holdFor(next.member(), today().plusDays(3)); // status = ON_HOLD_SHELF
notifier.holdReady(next.member(), copy);
}
private LocalDate today() { return LocalDate.now(clock); }
}
class DailyFine implements FinePolicy {
public long fine(LocalDate due, LocalDate returned) {
long late = Math.max(0, ChronoUnit.DAYS.between(due, returned));
return Math.min(late * 50, 2_000); // 0.50 a day, at most 20.00
}
}
还书时有一个容易漏掉的分支:有人在排队,这本书就不能回到书架,而是进预约架,并且只留给队首那个人。checkout 里的 heldForThem 判断保证别人借不走它。
预约过期的处理复用 shelve:每天跑一次定时任务,找出预约架上过了期限的副本,对每一本调用 shelve,它会自动轮到下一位,没人排队就回到书架。
并发
上面用 synchronized 把整个服务锁住,一个图书馆的借还频率很低,这完全够用。如果数据存在数据库里,就把“检查状态再修改”写成一条条件更新:
UPDATE book_copies SET status = 'ON_LOAN'
WHERE barcode = ? AND status = 'AVAILABLE';
受影响行数为 1 才算借书成功,为 0 说明别人抢先一步。这和停车场那一章(LLD 案例 1)里“取出即占用”是同一个思路。
扩展讨论
- 续借:没人预约时可以续借一次,应还日期顺延 14 天;有人预约就不能续借。
- 检索:按书名或作者模糊搜索,数据量小用数据库的全文索引,大了再上搜索引擎。
- 多个分馆:
BookCopy加上所在分馆;预约可以指定取书地点,副本在馆际之间调拨。 - 电子书:许可证数量就是“副本数”,借期到了自动归还,不会产生罚款。