从设计到落地:校园图书馆系统的面向对象实现全流程
很多小白学面向对象时总困惑:“类图、用例图我会画,但怎么把这些设计变成能跑的代码?” 这篇文章就用 “校园图书馆管理系统” 当例子,从需求分析→设计方案→代码实现→测试验证,带你走通 “设计→实现” 的完整链路,每个步骤都贴代码、讲逻辑,看完你会明白:面向对象实现不是 “凭空写代码”,而是 “把设计蓝图拆成可执行的代码块”。
一、先做设计:给实现画好 “施工图纸”
在写一行代码前,先明确 “要做什么” 和 “怎么组织代码”—— 这就是设计阶段的核心。我们先从需求切入,再输出设计成果(类图、交互逻辑),为后续实现打基础。
1.1 需求分析:明确核心角色与功能
校园图书馆系统的核心需求很清晰:
- 读者(学生 / 老师)能查图书、借图书、还图书,老师的借阅期限比学生长
- 图书有基本信息(编号、名称、作者、状态:可借 / 已借),电子图书还需要 “电子版链接”
- 系统要记录借阅记录(谁借了哪本书、借出时间、应还时间、是否超时)
- 管理员能添加图书、查询所有借阅记录
1.2 面向对象设计:定义 “类” 与 “关系”
根据需求,我们设计出 4 个核心类,用简单的类图(非专业 UML,小白也能懂)展示设计结果:
类名 | 属性(设计时定义) | 方法(设计时定义) | 备注 |
Book(图书) | bookId (编号)、name (名称)、author (作者)、status (状态) | getStatus()、setStatus()、getInfo() | 父类,包含所有图书共性 |
EBook(电子图书) | 继承 Book 的所有属性 + url (电子版链接) | 继承 Book 的方法 + getUrl () | 子类,扩展电子图书特性 |
Reader(读者) | readerId (学号 / 工号)、name (姓名)、type (类型:学生 / 老师) | borrowBook ()(借书)、returnBook ()(还书) | 抽象出读者的核心行为 |
BorrowRecord(借阅记录) | recordId (记录 ID)、book (关联图书)、reader (关联读者)、borrowTime (借出时间)、dueTime (应还时间) | calculateOverdue ()(计算超时天数) | 关联图书和读者,记录状态 |
设计时的关键思考:
- 用继承处理 “图书” 的共性与个性:纸质书和电子书都有编号 / 名称,所以 EBook 继承 Book,只加特有的 url
- 用关联表示 “谁借了谁”:BorrowRecord 里包含 Book 和 Reader 对象,体现 “借阅” 这个关系
- 用多态预留扩展:比如不同读者(学生 / 老师)的借阅期限不同,后续实现时让 borrowBook () 方法根据 readerType 返回不同 dueTime
二、从设计到实现第一步:选对语言
设计好 “图纸” 后,先选实现语言。结合案例需求(需要清晰的类继承、强类型校验,小白易上手),我们选Java—— 原因有 3 个:
- 面向对象特性纯粹:类、继承、接口的语法清晰,能精准还原设计的类关系
- 生态友好:有现成的工具类(如java.util.Date处理借阅时间),不用重复造轮子
- 代码可读性高:小白能轻松看懂 “设计的类怎么变成代码”
如果你的项目是快速原型(比如图书馆临时统计工具),也可以选 Python,但 Java 更适合展示 “设计→实现” 的严谨性。
三、核心功能落地:把设计图写成代码
这部分是重点!我们按 “先实现基础类→再实现交互逻辑” 的顺序,一步步把设计转化为代码,每段代码都标注 “对应设计的哪部分”。
3.1 第一步:实现基础类(Book + EBook)
设计时定义了 Book 的属性和方法,现在用 Java 代码落地,重点体现 “封装” 和 “继承”:
// 1. 实现Book类(对应设计中的“图书父类”)
public class Book {
// 设计中的属性:用private封装,隐藏内部状态(符合“封装”设计)
private String bookId;
private String name;
private String author;
// 状态:0=可借,1=已借(设计时定义的状态规则)
private int status;
// 构造方法:创建Book对象时必须传入核心属性(设计时的“必填信息”)
public Book(String bookId, String name, String author) {
this.bookId = bookId;
this.name = name;
this.author = author;
this.status = 0; // 新书默认“可借”
}
// 设计中的方法:getter/setter(控制属性访问,符合封装)
public int getStatus() {
return status;
}
// 改状态时加校验:只能设为0或1(设计时没写,但实现时要补细节)
public void setStatus(int status) {
if (status == 0 || status == 1) {
this.status = status;
} else {
throw new IllegalArgumentException("状态只能是0(可借)或1(已借)");
}
}
// 设计中的getInfo():返回图书信息(方便后续展示)
public String getInfo() {
String statusStr = status == 0 ? "可借" : "已借";
return "编号:" + bookId + ",书名:" + name + ",作者:" + author + ",状态:" + statusStr;
}
// getter(name和bookId需要被其他类访问,比如BorrowRecord)
public String getBookId() {
return bookId;
}
public String getName() {
return name;
}
}
// 2. 实现EBook类(对应设计中的“电子图书子类”)
public class EBook extends Book {
// 设计中的扩展属性:电子版链接
private String url;
// 构造方法:先调用父类构造(必须初始化父类属性),再初始化子类属性
public EBook(String bookId, String name, String author, String url) {
super(bookId, name, author); // 调用Book的构造方法(继承的核心)
this.url = url;
}
// 设计中的扩展方法:获取电子版链接
public String getUrl() {
return url;
}
// 重写getInfo():在父类基础上增加url(体现多态的“重写”特性)
@Override
public String getInfo() {
// 先复用父类的getInfo(),再加子类信息(设计时的“扩展不修改”原则)
return super.getInfo() + ",电子版链接:" + url;
}
}
设计→实现的关键转化点:
- 设计时的 “封装”:用private修饰属性,只通过setter改状态,还加了校验(比如状态只能是 0/1),避免非法值 —— 这是把 “设计的安全性要求” 落地
- 设计时的 “继承”:EBook extends Book,用super()调用父类构造,复用了 Book 的属性和方法 —— 这是把 “设计的共性复用” 落地
3.2 第二步:实现读者类(Reader)
设计时 Reader 有 “借书 / 还书” 方法,实现时要结合 “多态”(不同读者借阅期限不同):
import java.util.Date;
import java.util.Calendar;
public class Reader {
private String readerId;
private String name;
// 设计中的类型:0=学生,1=老师
private int type;
public Reader(String readerId, String name, int type) {
this.readerId = readerId;
this.name = name;
// 实现时补校验:只能是学生或老师
if (type != 0 && type != 1) {
throw new IllegalArgumentException("读者类型只能是0(学生)或1(老师)");
}
this.type = type;
}
// 设计中的核心方法:借书(返回借阅记录,方便后续保存)
public BorrowRecord borrowBook(Book book) {
// 第一步:校验图书状态(设计时的“借阅前校验”)
if (book.getStatus() == 1) {
throw new RuntimeException("图书《" + book.getName() + "》已被借出");
}
// 第二步:改图书状态为“已借”(设计时的“状态更新”)
book.setStatus(1);
// 第三步:计算应还时间(设计时的“多态需求”:学生30天,老师60天)
Date borrowTime = new Date(); // 当前时间作为借出时间
Calendar calendar = Calendar.getInstance();
calendar.setTime(borrowTime);
// 按读者类型加天数(实现多态的“差异化逻辑”)
int days = type == 0 ? 30 : 60;
calendar.add(Calendar.DAY_OF_MONTH, days);
Date dueTime = calendar.getTime();
// 第四步:创建借阅记录(设计时的“关联关系”)
return new BorrowRecord(
"REC" + System.currentTimeMillis(), // 简单生成唯一记录ID
book,
this,
borrowTime,
dueTime
);
}
// 设计中的核心方法:还书(更新图书状态和记录)
public void returnBook(Book book, BorrowRecord record) {
// 校验:确保是自己借的书(实现时补的严谨性逻辑)
if (!record.getReader().getReaderId().equals(this.readerId)) {
throw new RuntimeException("不能归还他人借阅的图书");
}
// 改图书状态为“可借”(设计时的“状态回滚”)
book.setStatus(0);
// 计算超时(调用BorrowRecord的方法,体现类间协作)
int overdueDays = record.calculateOverdue();
if (overdueDays > 0) {
System.out.println("警告:《" + book.getName() + "》已超时" + overdueDays + "天,请尽快处理!");
} else {
System.out.println("图书《" + book.getName() + "》归还成功,无超时");
}
}
// getter(供其他类访问)
public String getReaderId() {
return readerId;
}
public String getName() {
return name;
}
}
设计→实现的关键转化点:
- 设计时的 “多态”:通过type == 0 ? 30 : 60实现不同读者的期限差异,没有用复杂的接口,小白能快速理解
- 设计时的 “类间协作”:borrowBook方法里调用Book的setStatus,创建BorrowRecord对象 —— 这是把 “设计的交互逻辑” 落地为代码调用
3.3 第三步:实现借阅记录类(BorrowRecord)
设计时这个类要 “计算超时天数”,实现时用 Java 的日期工具类完成:
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class BorrowRecord {
private String recordId;
private Book book; // 关联Book对象(设计时的“关联关系”)
private Reader reader; // 关联Reader对象(设计时的“关联关系”)
private Date borrowTime;
private Date dueTime;
// 构造方法(初始化所有属性)
public BorrowRecord(String recordId, Book book, Reader reader, Date borrowTime, Date dueTime) {
this.recordId = recordId;
this.book = book;
this.reader = reader;
this.borrowTime = borrowTime;
this.dueTime = dueTime;
}
// 设计中的核心方法:计算超时天数(当前时间 - 应还时间)
public int calculateOverdue() {
Date now = new Date();
// 如果没到应还时间,超时天数为0
if (now.before(dueTime)) {
return 0;
}
// 计算毫秒差,转成天数(实现时的工具类运用)
long diffMs = now.getTime() - dueTime.getTime();
return (int) TimeUnit.DAYS.convert(diffMs, TimeUnit.MILLISECONDS);
}
// getter(供其他类访问关联对象)
public Book getBook() {
return book;
}
public Reader getReader() {
return reader;
}
// 展示记录信息(方便测试和查看)
public String getRecordInfo() {
return "借阅记录ID:" + recordId +
",读者:" + reader.getName() +
",图书:" + book.getName() +
",应还时间:" + dueTime;
}
}
3.4 第四步:实现系统入口(模拟真实使用)
设计时的 “管理员添加图书、读者借书” 等功能,用一个LibrarySystem类模拟:
import java.util.ArrayList;
import java.util.List;
public class LibrarySystem {
// 系统存储的图书和借阅记录(设计时的“数据存储”需求)
private List<Book> bookList = new ArrayList<>();
private List<BorrowRecord> recordList = new ArrayList<>();
// 管理员添加图书(设计时的“管理员功能”)
public void addBook(Book book) {
// 校验:图书编号唯一(实现时补的业务规则)
boolean isDuplicate = bookList.stream()
.anyMatch(b -> b.getBookId().equals(book.getBookId()));
if (isDuplicate) {
throw new RuntimeException("图书编号" + book.getBookId() + "已存在");
}
bookList.add(book);
System.out.println("添加图书成功:" + book.getInfo());
}
// 读者借书(调用Reader的方法,保存记录)
public void borrowBook(Reader reader, String bookId) {
// 先找到对应图书(实现时的“查询逻辑”)
Book targetBook = bookList.stream()
.filter(b -> b.getBookId().equals(bookId))
.findFirst()
.orElseThrow(() -> new RuntimeException("未找到编号为" + bookId + "的图书"));
// 调用Reader的borrowBook方法(类间协作的落地)
BorrowRecord record = reader.borrowBook(targetBook);
recordList.add(record);
System.out.println("借书成功!" + record.getRecordInfo());
}
// 读者还书(调用Reader的方法)
public void returnBook(Reader reader, String bookId) {
Book targetBook = bookList.stream()
.filter(b -> b.getBookId().equals(bookId))
.findFirst()
.orElseThrow(() -> new RuntimeException("未找到编号为" + bookId + "的图书"));
// 找到该读者的借阅记录(实现时的“关联查询”)
BorrowRecord targetRecord = recordList.stream()
.filter(r -> r.getReader().getReaderId().equals(reader.getReaderId())
&& r.getBook().getBookId().equals(bookId))
.findFirst()
.orElseThrow(() -> new RuntimeException("未找到" + reader.getName() + "借阅《" + targetBook.getName() + "》的记录"));
// 调用Reader的returnBook方法
reader.returnBook(targetBook, targetRecord);
}
// 测试入口(模拟真实场景)
public static void main(String[] args) {
// 1. 创建系统
LibrarySystem library = new LibrarySystem();
// 2. 管理员添加图书(纸质书+电子书)
Book book1 = new Book("B001", "《Java编程思想》", "Bruce Eckel");
EBook book2 = new EBook("B002", "《Python编程:从入门到实践》", "Eric Matthes", "https://example.com/ebook2");
library.addBook(book1);
library.addBook(book2);
// 3. 创建读者(学生+老师)
Reader student = new Reader("S001", "张三", 0); // 0=学生
Reader teacher = new Reader("T001", "李老师", 1); // 1=老师
// 4. 学生借书
System.out.println("\n=== 学生张三借书 ===");
library.borrowBook(student, "B001"); // 借《Java编程思想》
// 5. 老师借书
System.out.println("\n=== 李老师借书 ===");
library.borrowBook(teacher, "B002"); // 借电子图书
// 6. 学生还书(模拟超时:这里手动改dueTime会更明显,实际可通过时间工具模拟)
System.out.println("\n=== 学生张三还书 ===");
library.returnBook(student, "B001");
}
}
运行结果(小白能直观看到效果):
添加图书成功:编号:B001,书名:《Java编程思想》,作者:Bruce Eckel,状态:可借
添加图书成功:编号:B002,书名:《Python编程:从入门到实践》,作者:Eric Matthes,状态:可借,电子版链接:https://example.com/ebook2
=== 学生张三借书 ===
借书成功!借阅记录ID:REC1693800000000,读者:张三,图书:《Java编程思想》,应还时间:Wed Oct 04 10:00:00 CST 2025
=== 李老师借书 ===
借书成功!借阅记录ID:REC1693800000001,读者:李老师,图书:《Python编程:从入门到实践》,应还时间:Fri Nov 03 10:00:00 CST 2025
=== 学生张三还书 ===
图书《Java编程思想》归还成功,无超时
四、设计驱动的程序设计风格:让代码 “好维护”
实现时不能只写 “能跑的代码”,还要结合设计的 “可重用、可扩展” 要求。比如:
4.1 提升可重用性:抽离通用工具
设计时没考虑 “时间格式化”,但实现时发现BorrowRecord的dueTime显示太乱,于是抽一个通用工具类:
import java.text.SimpleDateFormat;
import java.util.Date;
// 通用日期工具类(可重用,其他项目也能拿过来用)
public class DateUtils {
// 格式化日期为“yyyy-MM-dd HH:mm:ss”
public static String formatDate(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
}
然后在BorrowRecord的getRecordInfo()里用:
public String getRecordInfo() {
return "借阅记录ID:" + recordId +
",读者:" + reader.getName() +
",图书:" + book.getName() +
",应还时间:" + DateUtils.formatDate(dueTime); // 用工具类
}
这样改后,所有需要格式化日期的地方都能复用DateUtils,符合设计的 “复用” 要求。
4.2 提升可扩展性:预留扩展点
设计时考虑 “未来可能加‘管理员续借’功能”,实现时在BorrowRecord里加一个extendDueTime方法(先不实现完整逻辑,留接口):
// 预留续借方法(设计时的“扩展需求”)
public void extendDueTime(int days) {
if (days <= 0) {
throw new IllegalArgumentException("续借天数必须大于0");
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(this.dueTime);
calendar.add(Calendar.DAY_OF_MONTH, days);
this.dueTime = calendar.getTime();
System.out.println("续借成功!新应还时间:" + DateUtils.formatDate(this.dueTime));
}
未来需要续借功能时,直接调用这个方法,不用改原有代码 —— 符合 “开闭原则”(设计时的核心原则)。
五、基于设计的测试:验证实现是否符合预期
测试不是 “随便点一点”,而是针对设计的功能点验证。我们用 “单元测试 + 集成测试” 结合案例说明。
5.1 单元测试:测试单个类的方法(以 Book 类为例)
用 JUnit 5 测试Book的setStatus方法,验证 “非法状态是否报错”(对应设计的 “状态校验”):
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class BookTest {
// 测试“设置合法状态”
@Test
public void testSetStatus_Valid() {
Book book = new Book("B001", "测试书", "测试作者");
book.setStatus(1); // 设为“已借”
assertEquals(1, book.getStatus()); // 断言状态正确
}
// 测试“设置非法状态”(预期抛出异常)
@Test
public void testSetStatus_Invalid() {
Book book = new Book("B001", "测试书", "测试作者");
// 断言设置状态2时抛出IllegalArgumentException
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
book.setStatus(2);
});
assertEquals("状态只能是0(可借)或1(已借)", exception.getMessage());
}
}
5.2 集成测试:测试类间协作(以 “借书流程” 为例)
测试 “读者借书→图书状态变→生成记录” 的完整流程(对应设计的 “交互逻辑”):
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class LibraryIntegrationTest {
@Test
public void testBorrowBook_Flow() {
// 1. 准备环境
LibrarySystem library = new LibrarySystem();
Book book = new Book("B001", "测试书", "测试作者");
library.addBook(book);
Reader student = new Reader("S001", "张三", 0);
// 2. 执行借书操作
library.borrowBook(student, "B001");
// 3. 验证结果(设计的预期:图书状态变1,记录存在)
assertEquals(1, book.getStatus()); // 图书已借
// 验证记录列表里有这条记录
boolean hasRecord = library.getRecordList().stream()
.anyMatch(r -> r.getBook().getBookId().equals("B001")
&& r.getReader().getReaderId().equals("S001"));
assertTrue(hasRecord);
}
}
六、小结:设计→实现的核心逻辑
看完图书馆系统的案例,你会发现 “面向对象实现” 的本质是:
- 先有设计,后有代码:设计阶段定好 “有哪些类、类之间怎么协作”,实现时只是把这些逻辑翻译成代码
- 设计是 “蓝图”,实现是 “盖房子”:比如设计时的 “继承关系”→实现时的extends,设计时的 “多态需求”→实现时的条件判断或接口,设计时的 “关联关系”→实现时的对象引用
- 小白也能落地:选贴近生活的案例,每一步都对应设计目标,遇到细节问题(如时间计算、校验逻辑)用工具类或简单判断解决,不用一开始就追求复杂设计
如果你跟着敲一遍代码,会更有感觉 —— 下次再做面向对象项目,先画个简单的类图,再按 “基础类→交互逻辑→测试” 的顺序落地,就能轻松从设计走到实现!
还想看更多干货,关注同名公众昊“奈奈聊成长”!!!