學完本章你將能夠:
從需求清單出發,規劃套件結構與類別職責,完成一個分層的主控台專案。
把中級篇八章的知識(封裝、介面、例外、集合、泛型、I/O、enum 與日期)整合進同一個系統。
用自訂例外表達商業規則,讓錯誤處理集中且友善。
以「對介面寫程式」設計儲存層,讓記憶體版與檔案版可以互換。
循「照打就能跑」的完整程式碼,獨立完成建置、執行與驗證。
前七章的知識點都是單兵訓練,本章進行合成演練:打造一個可以實際操作的圖書管理系統——新增書籍、註冊會員、借書還書、資料存檔,全部在主控台完成。麻雀雖小,五臟俱全:這個專案的分層方式(model、exception、repository、service、app)正是業界後端系統的標準縮影,進階篇的資料庫與網路章節會在同一副骨架上繼續長大。
本章請開著編輯器逐檔輸入:每個檔案一個程式碼框,照著套件路徑建立目錄、照打就能執行;打完一層,再回頭讀該層的設計說明。
需求分析
功能需求(系統要做什麼):
書籍管理:新增書籍(ISBN、書名、作者)、列出全部書籍與借閱狀態。
會員管理:註冊會員(編號、姓名)。
借書:書必須存在且未被借出;會員必須存在且未達借閱上限(2 本);借期 14 天。
還書:找到對應的借閱記錄結案,回報是否逾期與逾期天數。
持久化:把書目存進文字檔,下次啟動可載回。
非功能需求(系統要做得多好):
分層:畫面、商業規則、資料存取徹底分離,各層只依賴下一層的介面。
可測試:商業規則不依賴 |Scanner| 與檔案,可以在測試中用記憶體儲存庫直接驗證。
錯誤友善:任何規則違反都以自訂例外回報,使用者看到的是清楚的中文訊息,程式不崩潰。
系統設計
套件結構(目錄即套件,一層一責):
|library.model|:資料模型——|Book|、|Member|、|Loan|。
|library.exception|:領域例外——|LibraryException| 家族。
|library.repository|:資料存取——|LibraryRepository| 介面與兩個實作。
|library.service|:商業規則——|LibraryService|。
|library.app|:使用者介面——|LibraryApp| 主程式。
各類別職責一覽:
| 類別 | 職責 | 主要知識點 |
|---|---|---|
| |Book| | 書籍資料與借閱狀態 | 封裝、equals 與 hashCode |
| |Member| | 會員資料與借閱額度 | 封裝、類別常數 |
| |Loan| | 借閱記錄與逾期計算 | 不可變欄位、java.time |
| 例外家族 | 表達違反的商業規則 | 繼承、自訂例外 |
| |LibraryRepository| | 儲存層合約 | 介面、多型 |
| |InMemoryLibraryRepository| | 記憶體儲存 | Map、集合操作 |
| |FileLibraryRepository| | 文字檔持久化 | 繼承、檔案 I/O |
| |LibraryService| | 借還書規則的權威來源 | 例外、防禦式檢查 |
| |LibraryApp| | 選單迴圈與訊息呈現 | Scanner、switch、try-catch |
依賴方向嚴格單向:|app| 依賴 |service|,|service| 依賴 |repository| 介面與 |model|,誰都不回頭依賴上層。
為什麼要分層?因為變化的理由不同:畫面想從主控台換成網頁、儲存想從檔案換成資料庫、規則想把上限改成 5 本——三種變化互不相干。分層讓每種變化只衝擊一層;|service| 依賴 |repository| 介面而非實作(第 2 章的開閉原則),更讓「換儲存方式」變成換一行 |new|。單機小專案就養成這個習慣,將來接手大系統時才不會水土不服。
model 層:三個資料類別
|Book| 以 ISBN 作為唯一識別:|equals| 與 |hashCode| 都只看 ISBN(第 1 章的標準範本),將來放進任何集合都行為正確。
package library.model;
import java.util.Objects;
/** 書籍:ISBN 為唯一識別;基本資料不可變,只有借閱狀態會變動 */ public class Book
private final String isbn; // 唯一識別,永不改變 private final String title; // 書名 private final String author; // 作者 private boolean available = true; // 新書進館預設可借
public Book(String isbn, String title, String author) this.isbn = isbn; this.title = title; this.author = author;
public String getIsbn() return isbn;
public String getTitle() return title;
public String getAuthor() return author;
public boolean isAvailable() return available;
/** 出借與歸還都經由這裡改變狀態(唯一的寫入口) */ public void setAvailable(boolean available) this.available = available;
/** 邏輯相等:ISBN 相同即同一本書(第 1 章範本) */ @Override public boolean equals(Object obj) if (this == obj) return true; if (!(obj instanceof Book)) return false; return isbn.equals(((Book) obj).isbn);
@Override public int hashCode() return Objects.hash(isbn);
@Override public String toString() return "《" + title + "》(" + author + ")";
|Member| 管理借閱額度:上限做成類別常數,額度的加減提供具名方法而不是裸露的 setter——呼叫端讀起來是「借出一本」而不是「把某個數字加一」:
package library.model;
/** 會員:個人資料與借閱額度 */ public class Member
/** 借閱上限:每人同時最多 2 本(規則改這裡就好) */ public static final int MAX_LOANS = 2;
private final String id; // 會員編號 private final String name; // 姓名 private int activeLoans; // 目前借閱中的本數
public Member(String id, String name) this.id = id; this.name = name;
public String getId() return id;
public String getName() return name;
/** 是否還有借閱額度 */ public boolean canBorrow() return activeLoans < MAX_LOANS;
/** 借出一本:額度佔用加一 */ public void incrementLoans() activeLoans++;
/** 歸還一本:額度釋放,防呆不讓它變負數 */ public void decrementLoans() if (activeLoans > 0) activeLoans–;
|Loan| 是借閱記錄:借出當下就推算出應還日(|LocalDate| 不可變,|plusDays| 回傳新物件——第 7 章),歸還時計算逾期天數:
package library.model;
import java.time.LocalDate; import java.time.temporal.ChronoUnit;
/** 借閱記錄:一次借書行為的完整生命週期 */ public class Loan
/** 借期規則:14 天 */ public static final int LOAN_DAYS = 14;
private final String isbn; // 借了哪本書 private final String memberId; // 誰借的 private final LocalDate borrowedOn; // 借出日 private final LocalDate dueOn; // 應還日 private LocalDate returnedOn; // 實際還書日(未還為 null)
public Loan(String isbn, String memberId, LocalDate borrowedOn) this.isbn = isbn; this.memberId = memberId; this.borrowedOn = borrowedOn; // 應還日由借出日推算:plusDays 回傳新物件 this.dueOn = borrowedOn.plusDays(LOAN_DAYS);
/** 尚未歸還的記錄才是「進行中」 */ public boolean isActive() return returnedOn == null;
/** 結案:記下還書日並回傳逾期天數(準時或提早為 0) */ public long close(LocalDate returnDate) this.returnedOn = returnDate; long overdue = ChronoUnit.DAYS.between(dueOn, returnDate); return Math.max(0, overdue); // 提早還書不是負逾期
public String getIsbn() return isbn;
public String getMemberId() return memberId;
public LocalDate getDueOn() return dueOn;
exception 層:讓錯誤會說話
所有領域例外繼承共同基底 |LibraryException|(checked——這些都是營運中必然發生的狀況,呼叫端必須面對,第 3 章的選擇準則)。基底提供例外鏈建構子,供包裝底層 I/O 例外使用:
package library.exception;
/** 領域例外基底:checked,強迫呼叫端面對可預期的營運狀況 */ public class LibraryException extends Exception
public LibraryException(String message) super(message);
/** 例外鏈版本:包裝底層例外(如 IOException)時使用 */ public LibraryException(String message, Throwable cause) super(message, cause);
三個具體例外各對應一條核心商業規則,訊息在建構子裡組好,呼叫端拿到就能直接顯示。注意不必為每種錯誤都開一個類別:像「查無此書」這種呼叫端不需要特別區分的情況,直接用基底例外帶清楚訊息即可:
package library.exception;
/** 規則:已被借走的書不能再借 */ public class BookNotAvailableException extends LibraryException
public BookNotAvailableException(String title) super("《" + title + "》目前出借中,請改借其他書");
package library.exception;
/** 規則:借書者必須是已註冊的會員 */ public class MemberNotFoundException extends LibraryException
public MemberNotFoundException(String memberId) super("查無會員,編號:" + memberId);
package library.exception;
/** 規則:會員同時借閱的數量有上限 */ public class LoanLimitExceededException extends LibraryException
public LoanLimitExceededException(String name, int max) super("會員 " + name + " 已達借閱上限(" + max + " 本),請先還書");
repository 層:可抽換的儲存
先立合約。|service| 只認識這個介面,永遠不知道資料實際放在哪裡:
package library.repository;
import java.util.Collection;
import library.model.Book; import library.model.Member;
/** 儲存層合約:底層用記憶體、檔案還是資料庫,實作自己決定 */ public interface LibraryRepository
void addBook(Book book);
/** 依 ISBN 查書;找不到回傳 null(由服務層轉成例外) */ Book findBook(String isbn);
Collection<Book> allBooks();
void addMember(Member member);
/** 依編號查會員;找不到回傳 null */ Member findMember(String memberId);
記憶體版用兩個 |Map| 實作「以鍵查值」,查詢時間接近常數(第 4 章)。選 |LinkedHashMap| 是刻意的:列表輸出維持加入順序,畫面穩定好對照:
package library.repository;
import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map;
import library.model.Book; import library.model.Member;
/** 記憶體版儲存庫:LinkedHashMap 讓遍歷順序等於加入順序 */ public class InMemoryLibraryRepository implements LibraryRepository
private final Map<String, Book> books = new LinkedHashMap<>(); private final Map<String, Member> members = new LinkedHashMap<>();
@Override public void addBook(Book book) // 同 ISBN 再加入視為覆蓋(Map 的天然行為) books.put(book.getIsbn(), book);
@Override public Book findBook(String isbn) return books.get(isbn); // 查無此鍵回傳 null
@Override public Collection<Book> allBooks() return books.values();
@Override public void addMember(Member member) members.put(member.getId(), member);
@Override public Member findMember(String memberId) return members.get(memberId);
檔案版繼承記憶體版:日常操作全部沿用(is-a 成立:檔案版儲存庫「是一種」記憶體儲存庫,只是多了持久化能力),額外提供存檔與載入。存檔格式刻意簡單——一本書一行、欄位用直線符號分隔:
package library.repository;
import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List;
import library.exception.LibraryException; import library.model.Book;
/** 檔案版儲存庫:沿用記憶體版行為,追加文字檔存讀能力 */ public class FileLibraryRepository extends InMemoryLibraryRepository
private final Path file; // 存檔位置
public FileLibraryRepository(Path file) this.file = file;
/** 存檔:一本書一行,欄位以 | 分隔,回傳筆數 */ public int save() throws LibraryException List<String> lines = new ArrayList<>(); for (Book b : allBooks()) lines.add(b.getIsbn() + "|" + b.getTitle() + "|" + b.getAuthor() + "|" + b.isAvailable()); try Files.write(file, lines, StandardCharsets.UTF_8); catch (IOException e) // 例外鏈(第 3 章):把底層 IO 例外包成領域例外 throw new LibraryException("存檔失敗:" + file, e); return lines.size();
/** 載入:檔案不存在視為第一次啟動,不算錯誤 */ public int load() throws LibraryException if (!Files.exists(file)) return 0; try List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8); for (String line : lines) // split 的參數是正規表示式,| 須跳脫成
| String[] cols = line.split("
|"); Book book = new Book(cols[0], cols[1], cols[2]); book.setAvailable(Boolean.parseBoolean(cols[3])); addBook(book); // 沿用父類別的加入邏輯 return lines.size(); catch (IOException e) throw new LibraryException("載入失敗:" + file, e);
service 層:商業規則的權威
所有借還書規則只住在這裡:畫面層不重複檢查,儲存層也不管規則。留意 |borrowBook| 的結構——先完成全部檢查,才開始改動狀態,永遠不會出現「額度扣了、書卻沒借到」的半套結果:
package library.service;
import java.time.LocalDate; import java.util.ArrayList; import java.util.List;
import library.exception.BookNotAvailableException; import library.exception.LibraryException; import library.exception.LoanLimitExceededException; import library.exception.MemberNotFoundException; import library.model.Book; import library.model.Loan; import library.model.Member; import library.repository.LibraryRepository;
/** 服務層:借還書規則集中地;規則違反一律拋自訂例外 */ public class LibraryService
private final LibraryRepository repo; // 只認識介面 private final List<Loan> loans = new ArrayList<>();
/** 建構子注入:測試時可以塞進記憶體版儲存庫 */ public LibraryService(LibraryRepository repo) this.repo = repo;
/** 借書:四道檢查全過,才建立借閱記錄 */ public Loan borrowBook(String isbn, String memberId, LocalDate date) throws LibraryException
Book book = repo.findBook(isbn); if (book == null) throw new LibraryException("查無此書,ISBN:" + isbn); Member member = repo.findMember(memberId); if (member == null) throw new MemberNotFoundException(memberId); if (!book.isAvailable()) throw new BookNotAvailableException(book.getTitle()); if (!member.canBorrow()) throw new LoanLimitExceededException( member.getName(), Member.MAX_LOANS);
// 檢查全數通過,才真正改動狀態 book.setAvailable(false); member.incrementLoans(); Loan loan = new Loan(isbn, memberId, date); loans.add(loan); return loan;
/** 還書:結案借閱記錄,回傳逾期天數(0 表示準時) */ public long returnBook(String isbn, String memberId, LocalDate date) throws LibraryException
// 找「這位會員、這本書、進行中」的那筆記錄 Loan target = null; for (Loan loan : loans) if (loan.isActive() && loan.getIsbn().equals(isbn) && loan.getMemberId().equals(memberId)) target = loan; break; if (target == null) throw new LibraryException( "查無進行中的借閱記錄:" + memberId + " / " + isbn);
long overdueDays = target.close(date); repo.findBook(isbn).setAvailable(true); // 書變回可借 repo.findMember(memberId).decrementLoans(); // 額度釋放 return overdueDays;
檢查的順序是有意設計的:先確認「東西存在」(書、會員),再檢查「狀態允許」(可借、有額度)。反過來寫,可能對不存在的物件呼叫方法而引爆 |NullPointerException|——那是程式臭蟲,不是商業例外。另外切記「先檢查、後改動」:只要有任何一道檢查可能失敗,就不要先動手改狀態,否則失敗時得寫復原邏輯,複雜度立刻翻倍。
app 層:選單迴圈
最外層負責人機互動:印選單、讀輸入、呼叫服務、把例外翻譯成友善訊息。每一輪操作都包在 try-catch 裡——任何商業規則失敗都只影響那一輪,系統繼續服務(第 3 章 ATM 範例的放大版):
package library.app;
import java.nio.file.Path; import java.time.LocalDate; import java.util.Scanner;
import library.exception.LibraryException; import library.model.Book; import library.model.Loan; import library.model.Member; import library.repository.FileLibraryRepository; import library.service.LibraryService;
/** 主控台進入點:只做三件事——讀輸入、呼叫 service、顯示結果 */ public class LibraryApp
// 教學用固定營業日,讓輸出可重現;實務改用 LocalDate.now() private static final LocalDate TODAY = LocalDate.of(2026, 8, 14);
private final Scanner scanner = new Scanner(System.in); private final FileLibraryRepository repo = new FileLibraryRepository(Path.of("books.txt")); private final LibraryService service = new LibraryService(repo);
public static void main(String[] args) LibraryApp app = new LibraryApp(); app.seedData(); app.run();
/** 種子資料:讓系統一啟動就有東西可操作 */ private void seedData() repo.addBook(new Book("978-1", "Java 核心技術", "王小凡")); repo.addBook(new Book("978-2", "演算法圖解", "林大同")); repo.addBook(new Book("978-3", "資料庫實戰", "陳美心")); repo.addMember(new Member("M001", "小明"));
/** 主迴圈:直到使用者選擇離開 */ private void run() System.out.println("=== 圖書管理系統(營業日 " + TODAY + ")==="); boolean running = true; while (running) printMenu(); String choice = scanner.nextLine().trim(); try switch (choice) case "1" -> addBook(); case "2" -> listBooks(); case "3" -> registerMember(); case "4" -> borrowBook(); case "5" -> returnBook(); case "6" -> saveData(); case "0" -> running = false; default -> System.out.println("無效選項,請重新輸入"); catch (LibraryException e) // 統一的錯誤出口:訊息友善,程式不中斷 System.out.println("[操作失敗] " + e.getMessage()); System.out.println("感謝使用,再見!");
private void printMenu() System.out.println(); System.out.println("[1]新增書籍 [2]書籍列表 [3]註冊會員 " + "[4]借書 [5]還書 [6]存檔 [0]離開"); System.out.print("請選擇:");
private void addBook() System.out.print("ISBN:"); String isbn = scanner.nextLine().trim(); System.out.print("書名:"); String title = scanner.nextLine().trim(); System.out.print("作者:"); String author = scanner.nextLine().trim(); repo.addBook(new Book(isbn, title, author)); System.out.println("已新增《" + title + "》");
private void listBooks() System.out.println("— 書籍列表(共 " + repo.allBooks().size() + " 本)—"); for (Book b : repo.allBooks()) String state = b.isAvailable() ? "可借" : "出借中"; System.out.println("[" + state + "] " + b.getIsbn() + " " + b.getTitle() + "/" + b.getAuthor());
private void registerMember() System.out.print("會員編號:"); String id = scanner.nextLine().trim(); System.out.print("姓名:"); String name = scanner.nextLine().trim(); repo.addMember(new Member(id, name)); System.out.println("已註冊會員 " + name + "(" + id + ")");
private void borrowBook() throws LibraryException System.out.print("會員編號:"); String memberId = scanner.nextLine().trim(); System.out.print("書籍 ISBN:"); String isbn = scanner.nextLine().trim(); Loan loan = service.borrowBook(isbn, memberId, TODAY); System.out.println("借閱成功,應還日期:" + loan.getDueOn());
private void returnBook() throws LibraryException System.out.print("會員編號:"); String memberId = scanner.nextLine().trim(); System.out.print("書籍 ISBN:"); String isbn = scanner.nextLine().trim(); long overdue = service.returnBook(isbn, memberId, TODAY); if (overdue > 0) System.out.println("還書成功,逾期 " + overdue + " 天,請補繳逾期費"); else System.out.println("還書成功,未逾期");
private void saveData() throws LibraryException int count = repo.save(); System.out.println("書目已寫入 books.txt(共 " + count + " 筆)");
輸入一律用 |nextLine()| 再自行轉換,是 |Scanner| 的保命守則:混用 |nextInt()| 與 |nextLine()| 時,|nextInt| 會把換行符留在緩衝區,下一個 |nextLine| 直接吃到空字串——這是初學者論壇的月經題。統一 |nextLine| 加 |trim|,世界清靜。
建置與執行:在 |library| 目錄的上層執行 |javac -d out library/app/LibraryApp.java|——編譯器會沿著 |import| 自動把其餘檔案一併編譯進 |out|;接著用 |java -cp out library.app.LibraryApp| 啟動。
完整執行示範
以下是一次完整操作:新增第 4 本書、註冊會員小美、連借兩本、第三本被上限擋下、還書、存檔、離開。提示文字(如「請選擇:」「ISBN:」)後面緊接的內容,是使用者輸入的回顯:
=== 圖書管理系統(營業日 2026-08-14)===
[1]新增書籍 [2]書籍列表 [3]註冊會員 [4]借書 [5]還書 [6]存檔 [0]離開 請選擇:1 ISBN:978-4 書名:設計模式入門 作者:張以芯 已新增《設計模式入門》
[1]新增書籍 [2]書籍列表 [3]註冊會員 [4]借書 [5]還書 [6]存檔 [0]離開 請選擇:3 會員編號:M002 姓名:小美 已註冊會員 小美(M002)
[1]新增書籍 [2]書籍列表 [3]註冊會員 [4]借書 [5]還書 [6]存檔 [0]離開 請選擇:4 會員編號:M002 書籍 ISBN:978-1 借閱成功,應還日期:2026-08-28
[1]新增書籍 [2]書籍列表 [3]註冊會員 [4]借書 [5]還書 [6]存檔 [0]離開 請選擇:4 會員編號:M002 書籍 ISBN:978-2 借閱成功,應還日期:2026-08-28
[1]新增書籍 [2]書籍列表 [3]註冊會員 [4]借書 [5]還書 [6]存檔 [0]離開 請選擇:4 會員編號:M002 書籍 ISBN:978-3 [操作失敗] 會員 小美 已達借閱上限(2 本),請先還書
[1]新增書籍 [2]書籍列表 [3]註冊會員 [4]借書 [5]還書 [6]存檔 [0]離開 請選擇:5 會員編號:M002 書籍 ISBN:978-2 還書成功,未逾期
[1]新增書籍 [2]書籍列表 [3]註冊會員 [4]借書 [5]還書 [6]存檔 [0]離開 請選擇:6 書目已寫入 books.txt(共 4 筆)
[1]新增書籍 [2]書籍列表 [3]註冊會員 [4]借書 [5]還書 [6]存檔 [0]離開 請選擇:0 感謝使用,再見!
驗證存檔成果:工作目錄出現 |books.txt|,每行一本書,例如第二行是 !978-2|演算法圖解|林大同|true!(歸還後回到可借),而 |978-1| 的最後一欄是 |false|(仍在小美手上)。想體驗載入,可在 |seedData| 後加一行呼叫 |repo.load()| 再觀察書籍列表(選項 2)的變化。
知識點回顧與延伸挑戰
這個專案把中級篇各章的知識用在哪裡?對照如下:
| 章 | 知識點 | 在本專案的落點 |
|---|---|---|
| 第 1 章 | 封裝、equals 與 hashCode | |Book| 以 ISBN 定義相等 |
| 第 1 章 | 繼承與 super | |FileLibraryRepository| 繼承記憶體版 |
| 第 2 章 | 介面與多型 | |service| 依賴 |LibraryRepository| 介面 |
| 第 3 章 | 自訂例外、例外鏈 | |LibraryException| 家族、包裝 IOException |
| 第 4 章 | Map 與集合選型 | |LinkedHashMap| 存書與會員 |
| 第 5 章 | 泛型 | |Map<String, Book>|、|List<Loan>| |
| 第 6 章 | 檔案 I/O | |Files.write| 與 |readAllLines| 持久化 |
| 第 7 章 | java.time、switch 箭頭 | 借期與逾期計算、選單分派 |
延伸挑戰(依難度遞增):
搜尋:新增「依書名關鍵字搜尋」選項(|title.contains(keyword)| 掃一遍 |allBooks|)。
序列化存檔:仿照第 6 章,把整個館藏(書加會員加借閱記錄)改用 |ObjectOutputStream| 一次存讀,比較與文字檔方案的優缺點。
統計報表:用 |Map| 統計每本書的歷史借閱次數,排序印出排行榜——進階篇學完 Stream API 之後,回來把這段改寫成一行管線,體會演進。
本章小結
分層架構:model 放資料、exception 放錯誤語意、repository 放存取、service 放規則、app 放互動;依賴方向嚴格由外而內單向。
商業規則集中在 service 層,且「先檢查、後改動」,避免半套狀態。
儲存層以介面立約,記憶體版與檔案版可互換;檔案版繼承記憶體版,只追加持久化能力。
自訂例外家族讓每條規則違反都有明確型別與友善訊息;底層 |IOException| 以例外鏈包裝上拋。
app 層把每輪操作包進 try-catch:單次失敗不中斷服務;|Scanner| 統一用 |nextLine| 讀取。
|LinkedHashMap| 讓輸出順序穩定;|Book| 覆寫 |equals| 與 |hashCode| 後可安心放進任何集合。
借期與逾期全靠 |LocalDate| 與 |ChronoUnit| 計算,不可變物件讓日期邏輯無副作用。
這副骨架就是實務系統的縮影:進階篇會在同樣的分層上換上資料庫、多執行緒與網路介面。
本章練習
實作題:實作「列出某會員借閱中書單」:輸入會員編號,顯示每本書的書名與應還日期。(提示:需要讓 |LibraryService| 提供查詢 |loans| 的方法,回傳該會員的進行中記錄。)
實作題:加入逾期罰金規則:每逾期一天 5 元,還書時一併顯示罰金。(提示:規則放 service 層或 |Loan| 內,app 層只負責顯示。)
實作題:讓 |FileLibraryRepository| 連會員也一起持久化(另存 |members.txt|),啟動時自動載入。(提示:儲存庫要先補上「列出全部會員」與讀取 |activeLoans| 的方法,再仿照 |save| 與 |load| 寫一對方法。)
觀念題:如果要把主控台介面換成網頁介面,本專案哪些類別完全不用改?這說明了分層的什麼價值?(提示:想想依賴方向——誰依賴誰,誰就會被改動波及。)