Đây là mini-challenge khép lại Module 5. Bạn sẽ model một thư viện nhỏ: có sách (Book — record), có người mượn (Borrower — record), có thư viện (Library — class quản lý state). Kết hợp đủ các khái niệm: class, object, constructor, encapsulation, equals/hashCode, record, validation. Không dùng thư viện ngoài — chỉ JDK.
Sau bài này bạn có một module nhỏ đúng chuẩn: value object immutable cho dữ liệu, mutable class cho state có hành vi, API public gọn, nội bộ private.
🎯 Đề bài
Viết 3 type quản lý một thư viện:
1. record Book(String isbn, String title, String author)
- Validation trong compact constructor:
isbn,title,authorđều khôngnull, không blank. - Chính
isbnđịnh danh một cuốn sách — cho dù record so tất cả field, bạn không phải lo thêm gì (nhưng phải hiểu record mặc định so 3 field).
2. record Borrower(String id, String name)
- Validate tương tự: cả 2 field không
null/blank.
3. class Library
- Field
private:Map<Book, Borrower> loans— tra ai đang mượn sách nào (nullnghĩa là còn trong kho).Set<Book> catalog— danh mục sách thư viện có.
- Method
public:void addBook(Book book)— thêm sách vào catalog. Throw nếu đã có.void borrow(Book book, Borrower borrower)— mượn sách. Throw nếu sách không có trong catalog hoặc đang được mượn.void returnBook(Book book)— trả sách. Throw nếu sách không phải đang được mượn.Optional<Borrower> whoBorrowed(Book book)— ai đang giữ sách;Optional.empty()nếu còn trong kho.List<Book> availableBooks()— danh sách sách còn trong kho (immutable snapshot).
- Override
toStringchoLibraryhiển thị tổng số sách + số sách đang mượn.
Output mẫu:
Library added 3 books
Alice borrowed Clean Code
Book Clean Code is held by Borrower[id=B001, name=Alice]
Available books: [Book[isbn=2, title=Effective Java, author=Bloch], Book[isbn=3, title=Java Concurrency, author=Goetz]]
Library{total=3, onLoan=1}
Alice returned Clean Code
Library{total=3, onLoan=0}
🔍 Phân tích I-P-O
Input: Tạo Book, Borrower, thao tác qua API Library.
Processing: Quản lý state trong Library (catalog, loans), validate mọi operation.
Output: Trạng thái library cập nhật, exception khi operation sai.
Key decisions
Book,Borrowerlà record: immutable, so nội dung, dùng làm key trongMap/Set— cầnequals/hashCode(record tự sinh).Librarylà class: có mutable state (catalog lớn dần, loans thay đổi mỗi lần mượn/trả). Record không phù hợp.- Encapsulation:
catalogvàloanslàprivate, chỉ lộ qua method có validate. OptionalchowhoBorrowed: rõ hơn returnnull, caller buộc phải xử lý case "không có".
📦 Concept dùng trong bài
| Concept | Bài | Dùng ở đây |
|---|---|---|
| Class và object | Module 5, bài 1 | Library |
Constructor + this | Module 5, bài 2 | Library constructor khởi tạo field |
| Instance vs static field | Module 5, bài 3 | catalog, loans là instance field |
| Encapsulation | Module 5, bài 4 | Field private, method public, trả snapshot |
| equals/hashCode | Module 5, bài 5 | Record Book dùng làm key Map (cần contract) |
| Record | Module 5, bài 6 | Book, Borrower với compact constructor validate |
Optional | (preview) | whoBorrowed — sẽ học sâu ở module sau |
▶️ Starter code
import java.util.*;
public class Library {
// TODO: record Book(String isbn, String title, String author) voi compact constructor validate
// TODO: record Borrower(String id, String name) voi compact constructor validate
// TODO: field private: Set<Book> catalog, Map<Book, Borrower> loans
// TODO: constructor khoi tao hai collection rong
// TODO: public void addBook(Book book)
// TODO: public void borrow(Book book, Borrower borrower)
// TODO: public void returnBook(Book book)
// TODO: public Optional<Borrower> whoBorrowed(Book book)
// TODO: public List<Book> availableBooks()
// TODO: @Override public String toString()
public static void main(String[] args) {
Library lib = new Library();
Book b1 = new Book("1", "Clean Code", "Martin");
Book b2 = new Book("2", "Effective Java", "Bloch");
Book b3 = new Book("3", "Java Concurrency", "Goetz");
lib.addBook(b1);
lib.addBook(b2);
lib.addBook(b3);
System.out.println("Library added 3 books");
Borrower alice = new Borrower("B001", "Alice");
lib.borrow(b1, alice);
System.out.println("Alice borrowed " + b1.title());
System.out.println("Book " + b1.title() + " is held by " + lib.whoBorrowed(b1).orElse(null));
System.out.println("Available books: " + lib.availableBooks());
System.out.println(lib);
lib.returnBook(b1);
System.out.println("Alice returned " + b1.title());
System.out.println(lib);
}
}
javac Library.java
java Library
Dành 25–30 phút tự làm. Gợi ý ở phần dưới nếu kẹt.
💡 Gợi ý
💡 💡 Gợi ý — đọc khi bị kẹt
Record với compact constructor validate:
record Book(String isbn, String title, String author) {
public Book {
if (isbn == null || isbn.isBlank()) {
throw new IllegalArgumentException("isbn must not be blank");
}
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("title must not be blank");
}
if (author == null || author.isBlank()) {
throw new IllegalArgumentException("author must not be blank");
}
}
}
Library constructor khởi tạo empty collection:
private final Set<Book> catalog;
private final Map<Book, Borrower> loans;
public Library() {
this.catalog = new HashSet<>();
this.loans = new HashMap<>();
}
Chú ý: catalog và loans là final — reference không thay đổi; nhưng nội dung vẫn mutable (thêm/xoá entry).
addBook — throw nếu trùng:
public void addBook(Book book) {
Objects.requireNonNull(book, "book must not be null");
if (!catalog.add(book)) {
throw new IllegalStateException("Book already in catalog: " + book.isbn());
}
}
Set.add() trả false nếu phần tử đã có (dựa vào equals/hashCode của record).
borrow — validate catalog + available:
public void borrow(Book book, Borrower borrower) {
Objects.requireNonNull(book);
Objects.requireNonNull(borrower);
if (!catalog.contains(book)) {
throw new IllegalStateException("Book not in catalog: " + book.isbn());
}
if (loans.containsKey(book)) {
throw new IllegalStateException("Book already on loan: " + book.isbn());
}
loans.put(book, borrower);
}
returnBook — remove từ loans:
public void returnBook(Book book) {
if (loans.remove(book) == null) {
throw new IllegalStateException("Book not on loan: " + book.isbn());
}
}
Map.remove trả null nếu key không có — gộp check + action trong 1 dòng.
whoBorrowed — Optional:
public Optional<Borrower> whoBorrowed(Book book) {
return Optional.ofNullable(loans.get(book));
}
availableBooks — snapshot immutable, loại sách đang mượn:
public List<Book> availableBooks() {
return catalog.stream()
.filter(b -> !loans.containsKey(b))
.toList(); // Java 16+, immutable
}
toString:
@Override
public String toString() {
return "Library{total=" + catalog.size() + ", onLoan=" + loans.size() + "}";
}
✅ Lời giải
ℹ️ ✅ Lời giải — xem sau khi đã thử
import java.util.*;
public class Library {
public record Book(String isbn, String title, String author) {
public Book {
if (isbn == null || isbn.isBlank())
throw new IllegalArgumentException("isbn must not be blank");
if (title == null || title.isBlank())
throw new IllegalArgumentException("title must not be blank");
if (author == null || author.isBlank())
throw new IllegalArgumentException("author must not be blank");
}
}
public record Borrower(String id, String name) {
public Borrower {
if (id == null || id.isBlank())
throw new IllegalArgumentException("id must not be blank");
if (name == null || name.isBlank())
throw new IllegalArgumentException("name must not be blank");
}
}
private final Set<Book> catalog;
private final Map<Book, Borrower> loans;
public Library() {
this.catalog = new HashSet<>();
this.loans = new HashMap<>();
}
public void addBook(Book book) {
Objects.requireNonNull(book, "book must not be null");
if (!catalog.add(book)) {
throw new IllegalStateException("Book already in catalog: " + book.isbn());
}
}
public void borrow(Book book, Borrower borrower) {
Objects.requireNonNull(book);
Objects.requireNonNull(borrower);
if (!catalog.contains(book))
throw new IllegalStateException("Book not in catalog: " + book.isbn());
if (loans.containsKey(book))
throw new IllegalStateException("Book already on loan: " + book.isbn());
loans.put(book, borrower);
}
public void returnBook(Book book) {
if (loans.remove(book) == null) {
throw new IllegalStateException("Book not on loan: " +
(book == null ? "null" : book.isbn()));
}
}
public Optional<Borrower> whoBorrowed(Book book) {
return Optional.ofNullable(loans.get(book));
}
public List<Book> availableBooks() {
return catalog.stream()
.filter(b -> !loans.containsKey(b))
.toList();
}
@Override
public String toString() {
return "Library{total=" + catalog.size() + ", onLoan=" + loans.size() + "}";
}
public static void main(String[] args) {
Library lib = new Library();
Book b1 = new Book("1", "Clean Code", "Martin");
Book b2 = new Book("2", "Effective Java", "Bloch");
Book b3 = new Book("3", "Java Concurrency", "Goetz");
lib.addBook(b1);
lib.addBook(b2);
lib.addBook(b3);
System.out.println("Library added 3 books");
Borrower alice = new Borrower("B001", "Alice");
lib.borrow(b1, alice);
System.out.println("Alice borrowed " + b1.title());
System.out.println("Book " + b1.title() + " is held by " + lib.whoBorrowed(b1).orElse(null));
System.out.println("Available books: " + lib.availableBooks());
System.out.println(lib);
lib.returnBook(b1);
System.out.println("Alice returned " + b1.title());
System.out.println(lib);
}
}
Giải thích từng phần:
-
Bookrecord — value object. 3 field là canonical component; record tự sinhequals/hashCodedựa trên cả 3 → 2Bookcùng isbn+title+author được coi là bằng nhau, dùng trực tiếp trongSet/Mapkey không cần viết thêm. -
Borrowerrecord — tương tự. -
Compact constructor validate — kiểm
nullvàisBlank()cho mọi string, throwIllegalArgumentException. Sau khi pass, compiler gán field. -
catalog: Set<Book>— danh mục.HashSetđảm bảo thêm O(1) và phát hiện trùng quaequals/hashCodecủa record. -
loans: Map<Book, Borrower>— ai đang mượn gì. Không có entry = sách còn trong kho. -
addBook—catalog.addtrảfalsenếu đã tồn tại (contract củaSet). Tận dụng để throw nếu add trùng. -
borrow— 2 check: catalog chứa book? loans đã có entry cho book? Cả 2 pass thì put entry. -
returnBook—loans.removetrảnullnếu key không có → throw. Gộp check + mutate trong 1 step atomic. -
whoBorrowed—Optional.ofNullablewrap kết quảMap.get(có thể null) thànhOptional. Caller phải.orElse/.isPresent()— không sợ quên check null. -
availableBooks— stream filter lấy sách không có trong loans,.toList()trả immutable list. Caller nhận snapshot, không mutate được catalog qua list này. -
toString— ngắn gọn, an toàn (không dump toàn bộ state vào log).
🎓 Mở rộng
Mức 1 — Hạn mức mượn per borrower:
Thêm field Map<Borrower, Integer> loanCount và method borrow từ chối nếu borrower đã mượn ≥ 5 sách.
Mức 2 — Lịch sử mượn:
Thêm record LoanRecord(Book book, Borrower borrower, LocalDateTime borrowedAt, LocalDateTime returnedAt) và List<LoanRecord> history. Mỗi lần borrow/returnBook push record mới. Method historyOf(Borrower) trả lịch sử của 1 người.
Mức 3 — Unit test với JUnit 5:
@Test
void borrowBookNotInCatalogThrows() {
Library lib = new Library();
Book b = new Book("1", "X", "Y");
Borrower br = new Borrower("B1", "A");
assertThrows(IllegalStateException.class, () -> lib.borrow(b, br));
}
@Test
void availableBooksReflectsState() {
Library lib = new Library();
Book b1 = new Book("1", "X", "Y");
lib.addBook(b1);
assertEquals(List.of(b1), lib.availableBooks());
lib.borrow(b1, new Borrower("B1", "A"));
assertTrue(lib.availableBooks().isEmpty());
}
Mức 4 — Expose API chỉ qua interface:
public interface LibraryApi {
void addBook(Book book);
void borrow(Book book, Borrower borrower);
void returnBook(Book book);
Optional<Borrower> whoBorrowed(Book book);
List<Book> availableBooks();
}
public class Library implements LibraryApi { ... }
Caller dùng LibraryApi lib = new Library(); — thay implementation (memory → database) mà không đổi caller. Pattern strong encapsulation cho module lớn.
✨ Điều bạn vừa làm được
Hoàn thành mini-challenge này, bạn đã:
- Phân biệt rõ khi dùng record (immutable value object) vs class (mutable state + behaviour). Book/Borrower là record, Library là class — đúng ngữ cảnh.
- Áp dụng encapsulation đầy đủ: field
private finalgiữ collection, lộ qua method có validate;availableBookstrả snapshot immutable. - Tận dụng contract
equals/hashCodecủa record để dùng trực tiếp làm keyMap/Set— không phải viết thủ công. - Dùng
Optionalthay returnnullchowhoBorrowed— API rõ nghĩa, caller buộc xử lý case vắng. - Validate input trong compact constructor (throw
IllegalArgumentException) và trong method (throwIllegalStateException) — phân biệt đúng exception cho "input sai" vs "state sai". - Viết
toStringan toàn (không dump collection) cho logging hữu ích nhưng không rò thông tin.
Chúc mừng — bạn đã hoàn thành Module 5! Bạn đã có công cụ mô hình hoá mọi "thực thể" trong domain. Module 6 sẽ bước vào kế thừa và đa hình: extends, super, @Override, abstract class, interface, sealed class — nơi nhiều class chia sẻ behaviour chung và JVM chọn method đúng tại runtime.